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//! * **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}