Skip to main content

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}