tilo_app/framework/mod.rs
1//! The app framework: a resource, its actions, and views bound to it.
2//!
3//! An app author writes three things:
4//!
5//! 1. **a resource type** — a `#[derive(Model)]` record (or any type
6//! implementing [`Resource`](tilo_resource::Resource) by hand, such as a
7//! timeline or a rich-text document);
8//! 2. **its actions** — `#[action]` methods in a `#[resource]` impl: typed,
9//! replayable, and exposed to the palette, agents and scripts;
10//! 3. **views** — a function from a [`ViewCx`] to widgets, where every value
11//! shown is read from the resource and every edit is an action.
12//!
13//! [`run`] hosts that on the toolkit's component runtime, so without writing
14//! any of it the app gets persistence and sync of its record, collaboration
15//! and presence, versioned history (a History mode that renders past versions
16//! with the app's own view and restores any of them), per-author undo,
17//! permissions enforced on every action (controls of forbidden actions disable
18//! themselves, and a role change takes effect while the app runs), the action
19//! palette and agent/script door, and search indexing.
20//!
21//! ```ignore
22//! #[derive(Model, Default)]
23//! pub struct Note { pub body: String }
24//!
25//! #[resource(kind = "example.note", label = "Note")]
26//! impl Note {
27//! /// Edit the note.
28//! #[action]
29//! pub fn edit(&mut self, text: String) { self.body = text; }
30//! }
31//!
32//! struct NoteApp;
33//! impl ResourceApp for NoteApp {
34//! type Resource = Note;
35//! const ID: &'static str = "example.note";
36//! const TITLE: &'static str = "Note";
37//! fn view(cx: &mut ViewCx<Note>) -> Element {
38//! Box::new(Column::new().push(Box::new(
39//! cx.input(|note| note.body.clone(), Note::EDIT).multiline(true),
40//! )))
41//! }
42//! }
43//!
44//! fn main() { tilo_app::framework::run::<NoteApp>() }
45//! ```
46//!
47//! # Test the public workflow
48//!
49//! [`testing::TestApp`] mounts the real app on an isolated profile. Start with
50//! [`actions`](testing::HeadlessApp::actions) to discover the participant's
51//! complete action contracts, then [`call_on`](testing::HeadlessApp::call_on)
52//! using the discovered address and JSON arguments. Test a refusal before
53//! [`call_confirmed_on`](testing::HeadlessApp::call_confirmed_on) for a destructive
54//! operation, and verify that a live role change still refuses a saved address.
55//! The runnable `resource-journey` example exercises that path alongside a bound
56//! input control, authored task identity, undo and reopen without a display.
57//!
58//! # Extension contracts
59//!
60//! Custom parts do not leave the framework. Each contract asks only for what
61//! the framework cannot know:
62//!
63//! * **A custom resource type** implements
64//! [`Resource`](tilo_resource::Resource): `read` its state from a document
65//! (the same call renders history), name its replayable `Action` type, and
66//! optionally declare named actions. Its own merge semantics live in its
67//! `Action::apply`.
68//! * **A custom widget or renderer** implements the toolkit's `Widget` for
69//! pixels and input, plus [`ResourceSurface`] to take part: `present` the
70//! resource's state, submit edits through its [`Dispatcher`], and report
71//! where its user is with [`ResourceSelection`]. Built from a `ViewCx`, the
72//! same surface renders past versions read-only in History mode.
73//! * **Extending an existing widget** — adding one hook to a toolkit widget,
74//! a view or a surface — implements
75//! [`Decorator`](crate::widget::Decorator) instead of `Widget`: name the
76//! wrapped widget and override only the hooks that change; every other
77//! `Widget` method, including the ones containers must forward, reaches
78//! the wrapped widget by default.
79//! * **A resource nested in another** — a note's rich-text body, a draft's
80//! message, a saved query's SQL — is a child resource: a resource type of
81//! its own stored at a place in its parent's document, whose edits the
82//! parent carries in one of its own actions (`EditBody { page, edit }`).
83//! The parent publishes it from `children` with
84//! [`publish_child`](tilo_resource::publish_child); a view or surface
85//! edits it through [`RecordEnv::child`], a
86//! [`Dispatcher`] of the child type that wraps each edit into the
87//! parent's action. The child is addressed, permissioned, undone (by its
88//! own coalescing) and named in history like a root, and applies as the
89//! parent. Rich text is `tilo_rich_text::RichTextField` over such a
90//! dispatcher; plain text is [`PlainText`](tilo_resource::PlainText),
91//! whose [`TextControl`] binds a `TextInput` or `CodeEditor` so every
92//! keystroke is a splice action.
93//! * **Handing a result to another app** — a picture to place, a file to
94//! attach — is an effect declared `content`: it answers
95//! [`Effected::content`] with the hash of bytes in the device's content
96//! store, its declaration's result is a `tilo.content` resource, and an
97//! operation that takes content (`ActionSpec::prompted_content`) receives
98//! it as its input. The caller chains the two by those types and never
99//! reads the producing app's document.
100//! * **Local view state** is a [`LocalState`](crate::LocalState): never
101//! persisted, never shared, never undone.
102//! A surface whose window should reopen as it was left (the open note,
103//! the caret) also provides
104//! [`ApplicationViewState`](ui_core::application_view::ApplicationViewState):
105//! the root saves and restores it with the window.
106//! * **Launching on something** — a path or URL on the command line, a file
107//! a host hands over — asks the app which record that is
108//! ([`ResourceApp::launch_record`]: a file's own record, one derived from
109//! it, a minted one) and which of the resource's actions the launch takes
110//! there ([`ResourceApp::launch`], through the same door as every
111//! control), or which of its effects, for a launch that shows something
112//! (a link to one item) rather than editing
113//! ([`Invoke::effect`](tilo_resource::Invoke::effect)); the view reads the
114//! same launch ([`RecordEnv::launch`]) to show what those actions made.
115//! [`ResourceApp::OPENS`] names the media types it is handed and
116//! [`ResourceApp::ARGUMENTS`] how many arguments it takes. A handed file's
117//! bytes are kept in the launch's content store ([`LaunchInput::content`]),
118//! which the actions name by hash, so the record never carries the file.
119//! The app's own Open is the same launch in the window it runs in
120//! ([`RecordEnv::open_here`], [`SurfaceCx::opener`]): the window shows the
121//! record that launch names in place of its own, so a document has one
122//! record however it was opened.
123//! An app that offers private windows ([`ResourceApp::PRIVATE_WINDOWS`],
124//! `--private`) runs one on a profile thrown away with the process,
125//! attached to no runtime, and its view knows ([`RecordEnv::is_private`]).
126//! A command the binary runs instead of a window (`--emit-photos DIR`) is a
127//! [`Command`] in [`ResourceApp::commands`]; a helper the binary also is
128//! (a privileged writer started with its own command line) is a
129//! [`Helper`] in [`ResourceApp::helpers`], asked before anything of the
130//! app starts; and the app's messages are
131//! its [`ResourceApp::catalog`], loaded in every host that mounts it. A
132//! launch that continues playback another device or app handed over says
133//! where it stood ([`LaunchInput::handoff`]): the media is what the launch
134//! names, the position and play state are the view's to restore.
135//! The app's own options — how the window runs rather than what it opens,
136//! such as the layout an on-screen keyboard starts in — are
137//! [`ResourceApp::OPTIONS`] and [`ResourceApp::FLAGS`], read with
138//! [`RecordEnv::launch_option`].
139//! * **A system surface** — the session's on-screen keyboard — declares its
140//! window's role in [`ResourceApp::BEHAVIOUR`]
141//! ([`WindowRole::InputSurface`](ui_core::application::WindowRole)): the
142//! window announces the app's id to the compositor and no headless drive
143//! operates it. The connections it serves the session through (an input
144//! method, a virtual keyboard) are the surface's own, opened in a live run
145//! ([`RecordEnv::mode`]) and woken through [`SurfaceCx::waker`].
146//! * **Mounted in another process** — the shell hosting a terminal tab in
147//! its own — the app is the same component, on a record the host opens
148//! for it, publishing into the host's palette and agent door. What only
149//! the host can provide (the session's process supervisor) it lends
150//! ([`HostContext::lend`](ui_core::application::HostContext::lend)); the
151//! view takes it with [`RecordEnv::take_lent`] and otherwise uses its own.
152//! * **Who is editing** is [`RecordEnv::me`] for the reader
153//! of a view, and [`ActionCx::author`](tilo_resource::ActionCx::author)
154//! inside an action: the same id, pinned with the action so every replica
155//! reads it. The resource state stays the same for everyone; a view
156//! compares the authors it records with `me`, and an action signs what it
157//! writes with its author rather than carrying one in its payload. What a
158//! view shows for an id is the name the host's roster has for it
159//! ([`RecordEnv::people`]): the document keeps ids,
160//! so a name that changes or arrives late reads right everywhere.
161//! * **A brand-new record** starts with the resource's
162//! [`initialize`](tilo_resource::Resource::initialize), its genesis: the
163//! same change on every device, so two people creating the same record
164//! converge on one. What depends on who creates it or where (the creator
165//! as owner, a first-run import of existing settings) is
166//! [`ResourceApp::seed`]: actions applied once after the genesis, by the
167//! creator, that are nobody's undo step.
168//! * **Which changes are undo steps** is declared by the action
169//! ([`tilo_actions::Action::change_kind`], `#[action(external)]`): content
170//! from outside — fetched rows, a process's output — syncs to everyone but
171//! no undo takes it back. Values applied immediately that continue one
172//! another ([`Resource::coalesces`](tilo_resource::Resource::coalesces)) are
173//! one step — a word typed, a slider dragged — until the surface
174//! [settles](Dispatcher::settle) the gesture.
175//! * **Settings** — preferences that follow a person or stay on a device,
176//! never seen by collaborators — are a typed
177//! [`SettingsSchema`](crate::storage::SettingsSchema) whose
178//! [`Scope`](crate::storage::Scope) says how far they travel: `Personal`
179//! roams across the person's own devices, `DeviceLocal` never leaves this
180//! one, `Shared` reads a per-user section every app follows (`locale`,
181//! `text_assistance`). [`RecordEnv::settings`]
182//! hand out a [`Setting`] kept current while the view lives; a write from
183//! another window or device reaches it on the next frame. Files and
184//! secrets come from the same context ([`RecordEnv::storage`],
185//! [`SurfaceCx::secrets`], [`SurfaceCx::files`]); a secret that belongs to
186//! one document item (a profile's password) is
187//! `secrets().item(purpose, id)`, so the document names the item and the
188//! secret stays on the device.
189//! * **One record of a kind beside the one shown** — what was watched, the
190//! words taught to the keyboard, where reading stopped: data that grows,
191//! has history and is undone — is a resource the app keeps with its other
192//! records in the workspace it is open in ([`RecordEnv::person`]). The
193//! [`PersonRecord`] is one record per kind and workspace, the same on
194//! every device, edited with the resource's actions, attributed, in its
195//! history and undone per step, and synced with the workspace. A
196//! preference is still a setting. A record that is the person's rather
197//! than a workspace's — their accounts, their contacts, the models their
198//! agents offer — is declared with the app
199//! ([`ResourceApp::PERSON_RECORDS`]), kept in their own scope and carried
200//! to their other devices by their personal store ([`PersonKind`]).
201//! * **What grows without end** — a chat's messages, a log — is a chain of
202//! part records of another resource type kept beside the app's own in its
203//! workspace ([`Chain`], [`Chained`]): declared in [`ResourceApp::records`],
204//! read from a view or surface with [`RecordEnv::chain`], and published
205//! beside the app's resource as one resource whose actions land on the
206//! part they change (the head for something new, the part holding an
207//! item for a change of it). A workspace's host replays a joining device the
208//! head only; earlier parts are held when someone opens them.
209//! * **Effects** — work with no document delta (reload, back up now,
210//! connect, run a query) — are a typed vocabulary: `#[derive(Effect)]` on
211//! an enum, one variant per effect. A view answers it with
212//! [`ViewCx::effects`], a custom surface with
213//! [`ResourceSurface::effect`] (`impl ResourceSurface<R, MyEffect>`); the
214//! framework publishes it beside the resource's actions, with the
215//! availability and labels the handler reports now ([`EffectStatus`]),
216//! and every caller — a button ([`ViewCx::effect_button`],
217//! [`Dispatcher::effect`]), the palette, an agent, a script — reaches the
218//! handler as a typed value. Long work is a job started through the
219//! [`EffectCx`] ([`thread`](EffectCx::thread), [`spawn`](EffectCx::spawn),
220//! [`reply`](EffectCx::reply)): it reports progress, may be cancelled, and
221//! finishes on the UI thread, changing the document with ordinary actions
222//! and answering its caller. Importing and exporting is such a job: the
223//! host's file chooser opens through [`EffectCx::choose_file`] and
224//! [`EffectCx::save_file`], the same on every host, a browser page
225//! included.
226//! An effect whose harm depends on its arguments (formatting the system
227//! disk) classifies each invocation (`#[effect(risk = "…")]`), and asks
228//! for confirmation only when it is destructive. A view withholds one
229//! of the resource's own actions while its live state forbids it
230//! ([`Publishes::availability`]). Effects never run in History mode. A variant
231//! that only changes what its caller sees (open, reveal, show) is
232//! `#[effect(view)]`, published as a View operation and answered by the
233//! same handler.
234//! * **Operations on one item** — open *this* snippet, reveal *this* tag,
235//! connect *this* connection, run *this* saved query — are effects bound
236//! to the item: the resource's `children` carry
237//! `.with_action(MyEffect::Open { id }.bound())`, a live row
238//! [`LiveRow::effect`]. The palette, an agent or a script invokes it on
239//! the item through the same door as every other action, and the handler
240//! of `MyEffect` receives that value. An effect only ever offered this way
241//! is `#[effect(row)]`, so the resource does not also offer it with an id
242//! to type. No view or surface answers a raw `ResourceOperations::start_operation`
243//! for it.
244//! * **Jobs nobody invoked** — a route fetched when the destination
245//! changes, a feed refreshed when it opens — start through the view's or
246//! surface's [`Jobs`] ([`ViewCx::jobs`], [`SurfaceCx::jobs`]), under a key
247//! the latest request takes over, and finish like an effect's.
248//! * **Projections** — an outside consequence an action recorded in the
249//! document (an outbox, a filesystem operation, a device's configuration)
250//! — are drained with [`RecordEnv::outbox`]: the framework runs the drain in
251//! the live app after every change, on every participant's device, and
252//! the drain acknowledges each entry with an action.
253//! * **Files of the mounted filesystem** an app shows (a file manager's
254//! selected file) have a history of their own, kept by the filesystem's
255//! projection rather than the app's document. The app declares it
256//! ([`ResourceApp::FILE_HISTORY`]) and its view names the file
257//! ([`RecordEnv::file_history`]); History offers it beside the resource's.
258//! * **Run mode.** A view is built for a window someone uses, a past version
259//! in History, or a screenshot ([`RunMode`], from [`RecordEnv::mode`]). Only a live view starts work that reaches outside
260//! the process (a mail engine, a socket, a journal reader, a hardware
261//! probe): History rebuilds the view for every version it previews, and a
262//! screenshot must come out the same on every machine.
263//! * **Edits bound to where the user is** — the selection, the caret, the
264//! playhead, the folder shown — are typed builders of the resource's
265//! action ([`ActionDecl`](tilo_resource::ActionDecl)), published on the
266//! resource beside its declarations:
267//! [`ViewCx::bind_declarations`] from a view's local state, and
268//! `Resource::bound_declarations` from the resource state itself.
269//! * **Live external rows** — processes, devices, containers, journal
270//! entries, fetched catalogue rows, running jobs — are [`LiveRow`]s from
271//! [`ResourceSurface::live_rows`], merged by id into the resource's
272//! published children. A row is an item of a declared `#[resource]` type
273//! ([`LiveRow::typed`]), so it says what it is to search (its type's noun,
274//! its app's name) as a tilolang item does, with nothing the app writes,
275//! and what a press does is one of the type's actions or `open`. A row
276//! carries only view and effect operations (an effect bound to the row
277//! with `LiveRow::effect`) and may say how its entity stands right now
278//! (`LiveRow::presentation`). An edit of the
279//! resource bound to a row (rename *this* file) is one of
280//! [`Publishes::row_edits`], published on the row.
281//! Neither rows nor children need to be published whole: a bounded page is
282//! published and the rest resolve by id (`ResourceSurface::live_row`,
283//! `Resource::child`).
284//! * **One search source** — what the app's search finds is the resource's
285//! entries ([`Resource::search`](tilo_resource::Resource::search)) and,
286//! for what a view shows that the resource does not hold (a page's
287//! text, a book's outline, processes), the view's own
288//! ([`Publishes::search_entries`]), indexed together. An entry says
289//! how its hit reads — its result group, subtitle, icon and the action it
290//! invokes — so the app needs no search provider beside it.
291//! * **One published tree** — the palette, agents and scripts see the
292//! resource's publication and nothing beside it that changes the document:
293//! a document action a content widget publishes through its own
294//! `PublishesResources` is dropped (a debug build stops at it).
295//! * **A participant without a window** — an agent, a service, a script —
296//! acts through the app's own view mounted over the record and never
297//! drawn ([`AttachedView`]): the same door as a person, checked against
298//! its permissions, attributed to it (an agent acting for its person) and
299//! recorded in history with the task it belongs to.
300//! * **Screenshot scenarios** (`--shots --scenario NAME`, every one with
301//! `--shots-dir DIR`, each at [`ResourceApp::shot_viewport`]) are prepared
302//! in three parts. What the resource does not hold is the scenario's
303//! [`Fixture`] ([`ResourceApp::fixture`]), prepared before the view is
304//! built: typed stand-ins for live sources that views and surfaces read
305//! with [`RecordEnv::fixture`], scratch
306//! files, content-store blobs, versions other people wrote at other times,
307//! and collaborators shown where they are. The resource is staged with
308//! actions ([`ResourceApp::scenario`]), and view state where it lives,
309//! once the actions are laid out: [`ViewCx::on_stage`] for a view's local
310//! state, `ResourceSurface::stage` for a custom surface's. What a person
311//! does in the scene (typing, a drag held over its target) is
312//! [`ResourceApp::scenario_input`], driven through the window's own event
313//! path. The capture then waits until no surface reports unfinished visual work (an async
314//! page render, a decode). [`stage`] runs a scenario on any harness, and
315//! [`TestApp::capture`](testing::HeadlessApp::capture) composes the exact
316//! frame `--shots` writes in a test.
317
318mod adopt_windows;
319mod app;
320mod attached;
321mod bar_actions;
322#[cfg(test)]
323mod bar_actions_tests;
324mod chain;
325mod chain_list;
326mod children;
327mod controls;
328// In-place reload swaps native code into a running process; a browser build
329// has neither the thin shared library nor the host that loads it.
330mod delivery;
331#[cfg(not(target_arch = "wasm32"))]
332pub mod dev_reload;
333mod dispatch;
334mod each;
335mod effect;
336mod endpoint;
337mod env;
338mod form;
339mod hosted;
340mod hosted_actions;
341mod hosted_readings;
342// The headless host discovers native presentation, which the browser has not got.
343#[cfg(not(target_arch = "wasm32"))]
344pub mod headless;
345pub mod history;
346mod kinds;
347mod launch;
348mod live;
349mod opening;
350pub mod page_part;
351mod people;
352mod person;
353mod preview;
354mod publishing;
355mod query;
356mod reactions;
357mod reactive;
358mod record;
359#[cfg(not(target_arch = "wasm32"))]
360pub mod record_index;
361pub mod reference;
362mod root;
363mod rows;
364mod save_notice;
365mod saves;
366mod scenario;
367mod section;
368mod settings;
369mod share_link;
370mod source;
371mod stage;
372mod surface;
373mod switch;
374// The test hosts discover native presentation, which the browser has not got.
375#[cfg(not(target_arch = "wasm32"))]
376pub mod testing;
377mod text;
378mod upkeep;
379mod view;
380pub mod views;
381mod window_bar;
382#[cfg(test)]
383mod window_bar_tests;
384mod workspace;
385mod workspace_emoji;
386
387pub use app::launch_spec;
388pub use app::{
389 AppName, FileHistory, OpeningCx, OpeningView, ResourceApp, SURFACE_FLAG, SeedCx, definition,
390 run_helper,
391};
392#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
393pub use app::{definition_launch_spec, run};
394pub use attached::{AttachedView, OpenView};
395pub use bar_actions::{BarActions, BarCommands};
396pub use chain::{Chain, Chained, PartArrival};
397pub use chain_list::{ChainList, ChainPager, ChainRow, Paged};
398pub use children::{ChildRecord, Children, Listed, child_resource_id};
399pub use controls::{ActionControl, Actions};
400pub use delivery::HostDelivery;
401pub use dispatch::{DispatchGroup, Dispatcher, Landed, Refused, Submit};
402pub use each::Each;
403pub use effect::{
404 ChosenFile, DocumentVersion, EffectAnswer, EffectCx, EffectReply, EffectStatus, Effected,
405 JobProgress, Jobs, Progress, Projection,
406};
407pub use endpoint::{EffectCaller, EffectEndpoint};
408pub use env::{RecordEnv, WindowDocument};
409pub use form::{Form, FormSubmit};
410pub use hosted::{Answers, Ask, Connection};
411pub use hosted_actions::{HostedActions, HostedActionsLink, HostedInvocation};
412pub use kinds::{hosted_kinds, resource_kind, seed_record, seed_record_in_workspace};
413pub use launch::{Command, Helper, LaunchInput, OpenHere};
414pub use live::Live;
415pub use opening::Opened;
416pub use people::People;
417pub use person::{PersonKind, PersonRecord};
418pub use preview::PastVersion;
419pub use publishing::Publishes;
420pub use query::{Query, ReadFailure};
421pub use record::{Declare, OtherRecord, Records};
422pub use reference::Call;
423pub use root::{reserved_action_ids, with_surface};
424pub use rows::LiveRow;
425pub use saves::Saves;
426pub use scenario::{Fixture, RunMode};
427pub use section::SettingsRecords;
428pub use settings::Setting;
429pub use share_link::host_link;
430pub(crate) use source::Source;
431pub use stage::{stage, time_frames};
432pub use surface::{ResourceSelection, ResourceSurface, SurfaceCx};
433pub use text::{EditsText, TextControl};
434pub use tilo_resource::{Effect, NoEffect};
435pub use ui_core::record::{chain_of_resource_id, chain_resource_id};
436pub use upkeep::Upkeep;
437pub use view::{Bindable, EffectsHandle, Mounts, ViewCx};
438pub use window_bar::{Binding, Choice, WindowBar};
439pub use workspace::{WorkspaceHandle, WorkspaceRecord};
440
441/// Record content, canonical hashing and picture headers. Use the store
442/// borrowed through `SurfaceCx::content` or `LaunchInput::content` so the host
443/// binds reads and writes to the resource's content lane.
444pub mod content {
445 pub use tilo_content::{
446 ContentStore, ImageFormat, hash_bytes, image_format, image_size, valid_hash,
447 };
448}