Skip to main content

Module framework

Module framework 

Source
Expand description

The app framework: a resource, its actions, and views bound to it.

An app author writes three things:

  1. a resource type — a #[derive(Model)] record (or any type implementing Resource by hand, such as a timeline or a rich-text document);
  2. its actions — #[action] methods in a #[resource] impl: typed, replayable, and exposed to the palette, agents and scripts;
  3. views — a function from a ViewCx to widgets, where every value shown is read from the resource and every edit is an action.

run hosts that on the toolkit’s component runtime, so without writing any of it the app gets persistence and sync of its record, collaboration and presence, versioned history (a History mode that renders past versions with the app’s own view and restores any of them), per-author undo, permissions enforced on every action (controls of forbidden actions disable themselves, and a role change takes effect while the app runs), the action palette and agent/script door, and search indexing.

ⓘ
#[derive(Model, Default)]
pub struct Note { pub body: String }

#[resource(kind = "example.note", label = "Note")]
impl Note {
    /// Edit the note.
    #[action]
    pub fn edit(&mut self, text: String) { self.body = text; }
}

struct NoteApp;
impl ResourceApp for NoteApp {
    type Resource = Note;
    const ID: &'static str = "example.note";
    const TITLE: &'static str = "Note";
    fn view(cx: &mut ViewCx<Note>) -> Element {
        Box::new(Column::new().push(Box::new(
            cx.input(|note| note.body.clone(), Note::EDIT).multiline(true),
        )))
    }
}

fn main() { tilo_app::framework::run::<NoteApp>() }

§Test the public workflow

testing::TestApp mounts the real app on an isolated profile. Start with actions to discover the participant’s complete action contracts, then call_on using the discovered address and JSON arguments. Test a refusal before call_confirmed_on for a destructive operation, and verify that a live role change still refuses a saved address. The runnable resource-journey example exercises that path alongside a bound input control, authored task identity, undo and reopen without a display.

§Extension contracts

Custom parts do not leave the framework. Each contract asks only for what the framework cannot know:

  • A custom resource type implements Resource: read its state from a document (the same call renders history), name its replayable Action type, and optionally declare named actions. Its own merge semantics live in its Action::apply.
  • A custom widget or renderer implements the toolkit’s Widget for pixels and input, plus ResourceSurface to take part: present the resource’s state, submit edits through its Dispatcher, and report where its user is with ResourceSelection. Built from a ViewCx, the same surface renders past versions read-only in History mode.
  • Extending an existing widget — adding one hook to a toolkit widget, a view or a surface — implements Decorator instead of Widget: name the wrapped widget and override only the hooks that change; every other Widget method, including the ones containers must forward, reaches the wrapped widget by default.
  • A resource nested in another — a note’s rich-text body, a draft’s message, a saved query’s SQL — is a child resource: a resource type of its own stored at a place in its parent’s document, whose edits the parent carries in one of its own actions (EditBody { page, edit }). The parent publishes it from children with publish_child; a view or surface edits it through RecordEnv::child, a Dispatcher of the child type that wraps each edit into the parent’s action. The child is addressed, permissioned, undone (by its own coalescing) and named in history like a root, and applies as the parent. Rich text is tilo_rich_text::RichTextField over such a dispatcher; plain text is PlainText, whose TextControl binds a TextInput or CodeEditor so every keystroke is a splice action.
  • Handing a result to another app — a picture to place, a file to attach — is an effect declared content: it answers Effected::content with the hash of bytes in the device’s content store, its declaration’s result is a tilo.content resource, and an operation that takes content (ActionSpec::prompted_content) receives it as its input. The caller chains the two by those types and never reads the producing app’s document.
  • Local view state is a LocalState: never persisted, never shared, never undone. A surface whose window should reopen as it was left (the open note, the caret) also provides ApplicationViewState: the root saves and restores it with the window.
  • Launching on something — a path or URL on the command line, a file a host hands over — asks the app which record that is (ResourceApp::launch_record: a file’s own record, one derived from it, a minted one) and which of the resource’s actions the launch takes there (ResourceApp::launch, through the same door as every control), or which of its effects, for a launch that shows something (a link to one item) rather than editing (Invoke::effect); the view reads the same launch (RecordEnv::launch) to show what those actions made. ResourceApp::OPENS names the media types it is handed and ResourceApp::ARGUMENTS how many arguments it takes. A handed file’s bytes are kept in the launch’s content store (LaunchInput::content), which the actions name by hash, so the record never carries the file. The app’s own Open is the same launch in the window it runs in (RecordEnv::open_here, SurfaceCx::opener): the window shows the record that launch names in place of its own, so a document has one record however it was opened. An app that offers private windows (ResourceApp::PRIVATE_WINDOWS, --private) runs one on a profile thrown away with the process, attached to no runtime, and its view knows (RecordEnv::is_private). A command the binary runs instead of a window (--emit-photos DIR) is a Command in ResourceApp::commands; a helper the binary also is (a privileged writer started with its own command line) is a Helper in ResourceApp::helpers, asked before anything of the app starts; and the app’s messages are its ResourceApp::catalog, loaded in every host that mounts it. A launch that continues playback another device or app handed over says where it stood (LaunchInput::handoff): the media is what the launch names, the position and play state are the view’s to restore. The app’s own options — how the window runs rather than what it opens, such as the layout an on-screen keyboard starts in — are ResourceApp::OPTIONS and ResourceApp::FLAGS, read with RecordEnv::launch_option.
  • A system surface — the session’s on-screen keyboard — declares its window’s role in ResourceApp::BEHAVIOUR (WindowRole::InputSurface): the window announces the app’s id to the compositor and no headless drive operates it. The connections it serves the session through (an input method, a virtual keyboard) are the surface’s own, opened in a live run (RecordEnv::mode) and woken through SurfaceCx::waker.
  • Mounted in another process — the shell hosting a terminal tab in its own — the app is the same component, on a record the host opens for it, publishing into the host’s palette and agent door. What only the host can provide (the session’s process supervisor) it lends (HostContext::lend); the view takes it with RecordEnv::take_lent and otherwise uses its own.
  • Who is editing is RecordEnv::me for the reader of a view, and ActionCx::author inside an action: the same id, pinned with the action so every replica reads it. The resource state stays the same for everyone; a view compares the authors it records with me, and an action signs what it writes with its author rather than carrying one in its payload. What a view shows for an id is the name the host’s roster has for it (RecordEnv::people): the document keeps ids, so a name that changes or arrives late reads right everywhere.
  • A brand-new record starts with the resource’s initialize, its genesis: the same change on every device, so two people creating the same record converge on one. What depends on who creates it or where (the creator as owner, a first-run import of existing settings) is ResourceApp::seed: actions applied once after the genesis, by the creator, that are nobody’s undo step.
  • Which changes are undo steps is declared by the action (tilo_actions::Action::change_kind, #[action(external)]): content from outside — fetched rows, a process’s output — syncs to everyone but no undo takes it back. Values applied immediately that continue one another (Resource::coalesces) are one step — a word typed, a slider dragged — until the surface settles the gesture.
  • Settings — preferences that follow a person or stay on a device, never seen by collaborators — are a typed SettingsSchema whose Scope says how far they travel: Personal roams across the person’s own devices, DeviceLocal never leaves this one, Shared reads a per-user section every app follows (locale, text_assistance). RecordEnv::settings hand out a Setting kept current while the view lives; a write from another window or device reaches it on the next frame. Files and secrets come from the same context (RecordEnv::storage, SurfaceCx::secrets, SurfaceCx::files); a secret that belongs to one document item (a profile’s password) is secrets().item(purpose, id), so the document names the item and the secret stays on the device.
  • The person’s own data — what they watched, the words they taught the keyboard, where they stopped reading: data that grows, has history and is undone, but belongs to the person rather than the room — is a resource kept in the person’s own room (RecordEnv::person). The PersonRecord is one record per kind on all of the person’s devices, edited with the resource’s actions, attributed, in its history and undone per step, and synced to the person’s devices only — never to a collaborator, whatever room the app is open in. A preference is still a setting.
  • Effects — work with no document delta (reload, back up now, connect, run a query) — are a typed vocabulary: #[derive(Effect)] on an enum, one variant per effect. A view answers it with ViewCx::effects, a custom surface with ResourceSurface::effect (impl ResourceSurface<R, MyEffect>); the framework publishes it beside the resource’s actions, with the availability and labels the handler reports now (EffectStatus), and every caller — a button (ViewCx::effect_button, Dispatcher::effect), the palette, an agent, a script — reaches the handler as a typed value. Long work is a job started through the EffectCx (thread, spawn, reply): it reports progress, may be cancelled, and finishes on the UI thread, changing the document with ordinary actions and answering its caller. Importing and exporting is such a job: the host’s file chooser opens through EffectCx::choose_file and EffectCx::save_file, the same on every host, a browser page included. An effect whose harm depends on its arguments (formatting the system disk) classifies each invocation (#[effect(risk = "…")]), and asks for confirmation only when it is destructive. A surface withholds one of the resource’s own actions while its live state forbids it (ResourceSurface::availability). Effects never run in History mode. A variant that only changes what its caller sees (open, reveal, show) is #[effect(view)], published as a View operation and answered by the same handler.
  • Operations on one item — open this snippet, reveal this tag, connect this connection, run this saved query — are effects bound to the item: the resource’s children carry .with_action(MyEffect::Open { id }.bound()), a live row LiveRow::effect. The palette, an agent or a script invokes it on the item through the same door as every other action, and the handler of MyEffect receives that value. An effect only ever offered this way is #[effect(row)], so the resource does not also offer it with an id to type. No view or surface answers a raw Widget::start_operation for it.
  • Jobs nobody invoked — a route fetched when the destination changes, a feed refreshed when it opens — start through the view’s or surface’s Jobs (ViewCx::jobs, SurfaceCx::jobs), under a key the latest request takes over, and finish like an effect’s.
  • Projections — an outside consequence an action recorded in the document (an outbox, a filesystem operation, a device’s configuration) — are drained with [ViewCx::drain]: the framework runs the drain in the live app after every change, on every participant’s device, and the drain acknowledges each entry with an action.
  • Files of the mounted filesystem an app shows (a file manager’s selected file) have a history of their own, kept by the filesystem’s projection rather than the app’s document. The app declares it (ResourceApp::FILE_HISTORY) and its surface names the file (SurfaceCx::file_history); History offers it beside the resource’s.
  • Run mode. A view is built for a window someone uses, a past version in History, or a screenshot (RunMode, from RecordEnv::mode). Only a live view starts work that reaches outside the process (a mail engine, a socket, a journal reader, a hardware probe): History rebuilds the view for every version it previews, and a screenshot must come out the same on every machine.
  • Edits bound to where the user is — the selection, the caret, the playhead, the folder shown — are typed builders of the resource’s action (ActionDecl), published on the resource beside its declarations: ResourceSurface::bound_declarations from a surface’s view state, ViewCx::bind_declarations from a view’s local state, and Resource::bound_declarations from the resource state itself.
  • Live external rows — processes, devices, containers, journal entries, fetched catalogue rows, running jobs — are LiveRows from ResourceSurface::live_rows, merged by id into the resource’s published children. A row carries only view and effect operations (an effect bound to the row with LiveRow::effect) and may say how its entity stands right now (LiveRow::presentation). An edit of the resource bound to a row (rename this file) is one of ResourceSurface::row_declarations, published on the row. Neither rows nor children need to be published whole: a bounded page is published and the rest resolve by id (ResourceSurface::live_row, Resource::child).
  • One search source — what the app’s search finds is the resource’s entries (Resource::search) and, for what a surface shows that the resource does not hold (a page’s text, a book’s outline, processes), the surface’s own (ResourceSurface::search_entries), indexed together. An entry says how its hit reads — its result group, subtitle, icon and the action it invokes — so the app needs no search provider beside it.
  • One published tree — the palette, agents and scripts see the resource’s publication and nothing beside it that changes the document: a document action a content widget publishes through its own PublishesResources is dropped (a debug build stops at it).
  • A participant without a window — an agent, a service, a script — acts through the app’s own view mounted over the record and never drawn (AttachedView): the same door as a person, checked against its permissions, attributed to it (an agent acting for its person) and recorded in history with the task it belongs to.
  • Screenshot scenarios (--shots --scenario NAME, every one with --shots-dir DIR, each at ResourceApp::shot_viewport) are prepared in three parts. What the resource does not hold is the scenario’s Fixture (ResourceApp::fixture), prepared before the view is built: typed stand-ins for live sources that views and surfaces read with RecordEnv::fixture, scratch files, content-store blobs, versions other people wrote at other times, and collaborators shown where they are. The resource is staged with actions (ResourceApp::scenario), and view state where it lives, once the actions are laid out: ViewCx::on_stage for a view’s local state, ResourceSurface::stage for a custom surface’s. What a person does in the scene (typing, a drag held over its target) is ResourceApp::scenario_input, driven through the window’s own event path. The capture then waits until no surface reports unfinished visual work (an async page render, a decode). stage runs a scenario on any harness, and TestApp::capture composes the exact frame --shots writes in a test.

Modules§

content
Record content, canonical hashing and picture headers. Use the store borrowed through SurfaceCx::content or LaunchInput::content so the host binds reads and writes to the resource’s content lane.
dev_reload
The symbols a thin app library exports so tilo-app dev can run new code inside the windows the process already has.
history
The resource’s history as actions every framework app answers, so the people, scripts and agents working on it can look back, compare and take back through the same door they edit through:
testing
Testing a framework app the way it runs: the real component runtime, storage, permissions, undo and history, without a window.

Structs§

Actions
The actions a resource publishes, for binding controls that are built outside the view function: rows a table, list or tree creates on demand. Cheap to clone; clones share the published descriptor.
AttachedView
One record held in its app’s view, attached and never drawn. See the module docs.
ChosenFile
A file the person chose with the host’s chooser (EffectCx::choose_file).
Command
A command the app’s binary runs instead of opening a window: a developer utility (writing fixtures, staging a repository) that shares the app’s code but is not part of using it.
DispatchGroup
An open Dispatcher::begin_group: every edit submitted until it drops becomes one action.
Dispatcher
Submits a view’s edits. Cloneable; clones share one queue.
DocumentVersion
The resource as it was at a past version a view asked for: its document there, the heads that name it and the name a caller gives it back (EffectCx::version).
Each
Rows for the items of a collection, in collection order, built by ViewCx::each.
EffectCx
What an effect handler, a job’s completion and a projection drain receive: the resource, its dispatcher, and the means to start a job.
EffectReply
The answer of an effect, given later from wherever its work settles — a reply from another service, a callback, a worker. Send. Dropping it unanswered fails the effect.
EffectStatus
What an effect, or one of the resource’s own actions, looks like to callers right now, when it differs from its declaration: unavailable while the engine is offline, labelled “Stop recording” while recording.
EffectsHandle
A registered effect handler (ViewCx::effects).
FileHistory
The History scope of ResourceApp::FILE_HISTORY: the files of the mounted filesystem an app shows.
Fixture
The scene of one screenshot scenario that its actions cannot stage: handed to ResourceApp::fixture before the view is built.
Form
The parameters of the verb P as they are being filled in (ViewCx::form). Clones share the values.
FormSubmit
A form’s submission (Form::submit).
Helper
A helper the app’s binary is besides the app: a privileged writer, a staging step, a packaging probe. Asked first, with the process’s whole command line, before anything of the app starts (no window, no session, no portal), it answers None for a command line that is not its own, else runs and answers the exit code the process ends with. A refused command line is an error, which ends the process with code 1.
JobProgress
Where a running job last said it is, as its Progress reported it.
Jobs
Work a view or surface starts on its own, with nobody invoking an effect: a route fetched when the destination changes, a feed refreshed when it opens. Each job runs under a key; starting another under a key still running stops the earlier one, so the latest request wins. A job is started like an effect’s (EffectCx::thread, EffectCx::spawn) and finishes the same way, on the UI thread, with ordinary actions; a surface’s completion gets &mut the surface. Jobs run only in a window someone uses: a past version and a capture start none.
Landed
A submission the door applied (Dispatcher::landed).
LaunchInput
What a launch handed the app: the arguments of its command line or of an “open with” (paths, URLs), the app’s own options it named, the file a host lent it, the content store where the app keeps what a launch hands it, and where playback of the media it names stood on the device or app that handed it over.
Live
A shared handle to the current state of one resource.
LiveRow
One live external entity a surface publishes beneath its resource, with the view and effect operations it offers (inspect a process, stop a container).
OpenHere
Opens what a launch naming some arguments opens in the window an app runs in, in place of the record it shows: what RecordEnv::open_here does, kept by a custom surface for the input that later asks for it (a file chosen in a picker).
OpenView
An AttachedView about to open (AttachedView::opening): whom it is for, what they may do, and what the host lends the view.
PastVersion
A past version drawn by the app’s own view, exactly as History mode’s preview draws it: read-only, over the frozen document. Built by HeadlessApp::past_version.
People
The names the host knows for the participants of the window’s room. Cloneable; clones read the same roster.
PersonRecord
A resource of the person’s own, kept in their own room: what they watched, the words they taught the keyboard, where they stopped reading. It follows them to their other devices and never reaches a collaborator, whatever room the app is open in.
Progress
A running job’s channel back to the app: report progress, notice cancellation. Cloneable and Send, for the worker.
Projection
A registered projection drain (ViewCx::drain), as the view or surface that registered it holds it.
RecordEnv
The record, run and storage a view or surface works in. See the module docs.
Records
The records an app keeps besides the one it shows, declared in ResourceApp::records: each is opened with the window and published beside the app’s resource, so the palette and agents reach it whether or not a view has shown it, and the app’s committed surface lists it.
Refused
An edit or effect the door refused (Dispatcher::refused).
SeedCx
What ResourceApp::seed may read: who is creating the record, and the app’s host — its settings, files and secrets on this device — when the record is created in a window. A participant without one (an agent, a room service: seed_record) creates it with no host: what a seed imports from the device is the device’s own to seed.
Setting
One typed settings value, kept current while the view lives: a write from this window, another window, the Settings app or another of the person’s devices (for Scope::Personal) reaches Self::state on the next frame.
SurfaceCx
What a surface receives when attached.
TextControl
A text field or code editor over collaborative text — a PlainText resource or child (a SQL buffer, a report note), or a text field of a record at its Path (Self::at) — whose every edit is a splice through dispatcher.
ViewCx
Everything a view needs: reads of the resource state, bindings of controls to actions, local state, and custom surfaces.

Enums§

Effected
What an effect handler (or a job’s completion) answers.
NoEffect
The vocabulary of a surface that has no effects.
ResourceSelection
Where a participant is inside a resource, in the resource’s own terms, so it means the same thing on every collaborator’s screen.
RunMode
What a view is built for. See the module docs.

Constants§

SURFACE_FLAG
The flag that makes an app’s binary print its surface_report instead of running: what tilo-app surface and tilo-app check read.

Traits§

ActionControl
A toolkit control whose committed value I can invoke an action.
Bindable
What a control shows and changes: a field of the record, by its Path, or a field of a form (ViewCx::form), by the path into the verb’s parameters. M tells the two apart and is always inferred.
EditsText
A text control a TextControl drives: it shows the text object it is bound to and keeps the user’s edits local, for its owner to submit.
Effect
A typed vocabulary of effects. Derive it (#[derive(Effect)]); see the module docs.
HostDelivery
Delivering an admitted invocation to a widget, as its host does. Answers how the widget started it — done at once with its output, or pending to finish later — or None when nothing in the widget answers it.
OtherRecord
A record other than the app’s own that views and surfaces edit: the person’s own record of P (PersonRecord), or a settings section P that is a resource model (Setting).
ResourceApp
A framework app: one resource type and a view of it.
ResourceSurface
A custom widget or renderer — a timeline, a canvas, a GPU viewport, a page layout engine — taking part in a resource.
Submit
Something a dispatcher submits and a button is bound to: a field edit made at a path (p.favorite.set(true)), a verb with its arguments, or an invocation built some other way. No app writes an action id or an argument map.

Functions§

definition
The toolkit application definition of A, created once per process.
definition_launch_spec
The launch specification of any application definition: the standard component host, record and launch modes, without a resource type’s capture scenarios.
host_link
The link to the resource request names in app’s record, for a room’s host that answers share-link itself on the records it runs headless: the link an app’s root mints, under the host’s own locator.
hosted_kinds
The resource type of every framework app this process has the definition of (definition): what a host that holds records of those apps without mounting them — an agent, a room service — reads, publishes and edits them by. Look one up by the kind a record names ([Kinds::get]) or by its app ([Kinds::hosted_by]).
launch_spec
The launch specification of A, for apps that add options to it.
reserved_action_ids
The ids of the actions the framework publishes on every app’s resource beside the app’s own: the generic apply, group and field-edit, the views open and read, history, and the presentation operations. An app verb, effect or view under one of these ids shadows the framework’s or is shadowed by it, so tilo-app check refuses it.
resource_kind
A’s resource type as a kind a host can look up: hosted by A, and an open resource named as A names it (ResourceApp::label). Every framework app gets its kind from here, so a host working with whatever a room holds reads, publishes and replays it without any of the app’s own code.
run
Run A: the live window, or --shots, --headless and the other standard launch modes. Needs the native runner (the default native feature); a host without it drives launch_spec itself.
run_helper
Run the first of A’s helpers that args is for, answering the exit code the process ends with; None when the command line is the app’s own.
seed_record
Seed a record of kind its genesis just created, as me creating it on device without a window: the app’s ResourceApp::seed with no host, applied as in a window. A kind no app registered has nothing to seed.
seed_record_in_room
Seed a new room record in its host-selected creation scope, without lending ambient device services. The route supplies context, not permission.
stage
Stage scenario (None for a plain --shots) on a running A: apply its resource actions through the host’s door, then let its views and surfaces stage their view state, and run the frames that settles.
time_frames
How long each of frames frames of A’s capture of scenario takes to sync, lay out and draw, after one frame that warms the text caches: what a --bench command reports. Where no native runner is linked (a browser page) there is no headless capture to time.
with_surface
Visit the surface of type S the root of a resource app of R shows: for a test, or a host that mounted the app, reaching the surface between frames (App::with_component_root lends the root of a mounted one). None when root is not such a root or shows no S.

Type Aliases§

EffectAnswer
What a handler or a job’s completion returns: the answer, or why the effect failed.

Derive Macros§

Effect
Derive Effect: an enum’s variants become the app’s published effects — work with no document delta, invoked by buttons, the palette, agents and scripts alike. See tilo_resource::effect.