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//! * **The person's own data** — what they watched, the words they taught
190//! the keyboard, where they stopped reading: data that grows, has history
191//! and is undone, but belongs to the person rather than the room — is a
192//! resource kept in the person's own room ([`RecordEnv::person`]). The [`PersonRecord`] is one record per kind on
193//! all of the person's devices, edited with the resource's actions,
194//! attributed, in its history and undone per step, and synced to the
195//! person's devices only — never to a collaborator, whatever room the app
196//! is open in. A preference is still a setting.
197//! * **Effects** — work with no document delta (reload, back up now,
198//! connect, run a query) — are a typed vocabulary: `#[derive(Effect)]` on
199//! an enum, one variant per effect. A view answers it with
200//! [`ViewCx::effects`], a custom surface with
201//! [`ResourceSurface::effect`] (`impl ResourceSurface<R, MyEffect>`); the
202//! framework publishes it beside the resource's actions, with the
203//! availability and labels the handler reports now ([`EffectStatus`]),
204//! and every caller — a button ([`ViewCx::effect_button`],
205//! [`Dispatcher::effect`]), the palette, an agent, a script — reaches the
206//! handler as a typed value. Long work is a job started through the
207//! [`EffectCx`] ([`thread`](EffectCx::thread), [`spawn`](EffectCx::spawn),
208//! [`reply`](EffectCx::reply)): it reports progress, may be cancelled, and
209//! finishes on the UI thread, changing the document with ordinary actions
210//! and answering its caller. Importing and exporting is such a job: the
211//! host's file chooser opens through [`EffectCx::choose_file`] and
212//! [`EffectCx::save_file`], the same on every host, a browser page
213//! included.
214//! An effect whose harm depends on its arguments (formatting the system
215//! disk) classifies each invocation (`#[effect(risk = "…")]`), and asks
216//! for confirmation only when it is destructive. A surface withholds one
217//! of the resource's own actions while its live state forbids it
218//! ([`ResourceSurface::availability`]). Effects never run in History mode. A variant
219//! that only changes what its caller sees (open, reveal, show) is
220//! `#[effect(view)]`, published as a View operation and answered by the
221//! same handler.
222//! * **Operations on one item** — open *this* snippet, reveal *this* tag,
223//! connect *this* connection, run *this* saved query — are effects bound
224//! to the item: the resource's `children` carry
225//! `.with_action(MyEffect::Open { id }.bound())`, a live row
226//! [`LiveRow::effect`]. The palette, an agent or a script invokes it on
227//! the item through the same door as every other action, and the handler
228//! of `MyEffect` receives that value. An effect only ever offered this way
229//! is `#[effect(row)]`, so the resource does not also offer it with an id
230//! to type. No view or surface answers a raw `Widget::start_operation`
231//! for it.
232//! * **Jobs nobody invoked** — a route fetched when the destination
233//! changes, a feed refreshed when it opens — start through the view's or
234//! surface's [`Jobs`] ([`ViewCx::jobs`], [`SurfaceCx::jobs`]), under a key
235//! the latest request takes over, and finish like an effect's.
236//! * **Projections** — an outside consequence an action recorded in the
237//! document (an outbox, a filesystem operation, a device's configuration)
238//! — are drained with [`ViewCx::drain`]: the framework runs the drain in
239//! the live app after every change, on every participant's device, and
240//! the drain acknowledges each entry with an action.
241//! * **Files of the mounted filesystem** an app shows (a file manager's
242//! selected file) have a history of their own, kept by the filesystem's
243//! projection rather than the app's document. The app declares it
244//! ([`ResourceApp::FILE_HISTORY`]) and its surface names the file
245//! ([`SurfaceCx::file_history`]); History offers it beside the resource's.
246//! * **Run mode.** A view is built for a window someone uses, a past version
247//! in History, or a screenshot ([`RunMode`], from [`RecordEnv::mode`]). Only a live view starts work that reaches outside
248//! the process (a mail engine, a socket, a journal reader, a hardware
249//! probe): History rebuilds the view for every version it previews, and a
250//! screenshot must come out the same on every machine.
251//! * **Edits bound to where the user is** — the selection, the caret, the
252//! playhead, the folder shown — are typed builders of the resource's
253//! action ([`ActionDecl`](tilo_resource::ActionDecl)), published on the
254//! resource beside its declarations:
255//! [`ResourceSurface::bound_declarations`] from a surface's view state,
256//! [`ViewCx::bind_declarations`] from a view's local state, and
257//! `Resource::bound_declarations` from the resource state itself.
258//! * **Live external rows** — processes, devices, containers, journal
259//! entries, fetched catalogue rows, running jobs — are [`LiveRow`]s from
260//! [`ResourceSurface::live_rows`], merged by id into the resource's
261//! published children. A row carries only view and effect operations
262//! (an effect bound to the row with `LiveRow::effect`) and may say how
263//! its entity stands right now (`LiveRow::presentation`). An edit of the
264//! resource bound to a row (rename *this* file) is one of
265//! [`ResourceSurface::row_declarations`], published on the row.
266//! Neither rows nor children need to be published whole: a bounded page is
267//! published and the rest resolve by id (`ResourceSurface::live_row`,
268//! `Resource::child`).
269//! * **One search source** — what the app's search finds is the resource's
270//! entries ([`Resource::search`](tilo_resource::Resource::search)) and,
271//! for what a surface shows that the resource does not hold (a page's
272//! text, a book's outline, processes), the surface's own
273//! ([`ResourceSurface::search_entries`]), indexed together. An entry says
274//! how its hit reads — its result group, subtitle, icon and the action it
275//! invokes — so the app needs no search provider beside it.
276//! * **One published tree** — the palette, agents and scripts see the
277//! resource's publication and nothing beside it that changes the document:
278//! a document action a content widget publishes through its own
279//! `PublishesResources` is dropped (a debug build stops at it).
280//! * **A participant without a window** — an agent, a service, a script —
281//! acts through the app's own view mounted over the record and never
282//! drawn ([`AttachedView`]): the same door as a person, checked against
283//! its permissions, attributed to it (an agent acting for its person) and
284//! recorded in history with the task it belongs to.
285//! * **Screenshot scenarios** (`--shots --scenario NAME`, every one with
286//! `--shots-dir DIR`, each at [`ResourceApp::shot_viewport`]) are prepared
287//! in three parts. What the resource does not hold is the scenario's
288//! [`Fixture`] ([`ResourceApp::fixture`]), prepared before the view is
289//! built: typed stand-ins for live sources that views and surfaces read
290//! with [`RecordEnv::fixture`], scratch
291//! files, content-store blobs, versions other people wrote at other times,
292//! and collaborators shown where they are. The resource is staged with
293//! actions ([`ResourceApp::scenario`]), and view state where it lives,
294//! once the actions are laid out: [`ViewCx::on_stage`] for a view's local
295//! state, `ResourceSurface::stage` for a custom surface's. What a person
296//! does in the scene (typing, a drag held over its target) is
297//! [`ResourceApp::scenario_input`], driven through the window's own event
298//! path. The capture then waits until no surface reports unfinished visual work (an async
299//! page render, a decode). [`stage`] runs a scenario on any harness, and
300//! [`TestApp::capture`](testing::HeadlessApp::capture) composes the exact
301//! frame `--shots` writes in a test.
302
303mod app;
304mod attached;
305mod controls;
306// In-place reload swaps native code into a running process; a browser build
307// has neither the thin shared library nor the host that loads it.
308mod delivery;
309#[cfg(not(target_arch = "wasm32"))]
310pub mod dev_reload;
311mod dispatch;
312mod each;
313mod effect;
314mod env;
315mod form;
316pub mod history;
317mod kinds;
318mod launch;
319mod live;
320mod people;
321mod person;
322mod preview;
323mod reactive;
324mod record;
325mod root;
326mod rows;
327mod scenario;
328mod section;
329mod settings;
330mod share_link;
331mod stage;
332mod surface;
333mod switch;
334// The test hosts discover native presentation, which the browser has not got.
335#[cfg(not(target_arch = "wasm32"))]
336pub mod testing;
337mod text;
338mod view;
339
340#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
341pub use app::definition_launch_spec;
342pub use app::launch_spec;
343#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
344pub use app::run;
345pub use app::{FileHistory, ResourceApp, SURFACE_FLAG, SeedCx, definition, run_helper};
346pub use attached::{AttachedView, OpenView};
347pub use controls::{ActionControl, Actions};
348pub use delivery::HostDelivery;
349pub use dispatch::{DispatchGroup, Dispatcher, Landed, Refused, Submit};
350pub use each::Each;
351pub use effect::{
352 ChosenFile, DocumentVersion, EffectAnswer, EffectCx, EffectReply, EffectStatus, Effected,
353 JobProgress, Jobs, Progress, Projection,
354};
355pub use env::RecordEnv;
356pub use form::{Form, FormSubmit};
357pub use kinds::{hosted_kinds, resource_kind, seed_record, seed_record_in_room};
358pub use launch::{Command, Helper, LaunchInput, OpenHere};
359pub use live::Live;
360pub use people::People;
361pub use person::PersonRecord;
362pub use preview::PastVersion;
363pub use record::{OtherRecord, Records};
364pub use root::{reserved_action_ids, with_surface};
365pub use rows::LiveRow;
366pub use scenario::{Fixture, RunMode};
367pub use settings::Setting;
368pub use share_link::host_link;
369pub use stage::{stage, time_frames};
370pub use surface::{ResourceSelection, ResourceSurface, SurfaceCx};
371pub use text::{EditsText, TextControl};
372pub use tilo_resource::{Effect, NoEffect};
373pub use view::{Bindable, EffectsHandle, ViewCx};
374
375#[cfg(test)]
376#[cfg(not(target_arch = "wasm32"))]
377mod headless_results_tests;
378
379/// Record content, canonical hashing and picture headers. Use the store
380/// borrowed through `SurfaceCx::content` or `LaunchInput::content` so the host
381/// binds reads and writes to the resource's content lane.
382pub mod content {
383 pub use tilo_content::{
384 ContentStore, ImageFormat, hash_bytes, image_format, image_size, valid_hash,
385 };
386}