Skip to main content

tilo_app/
lib.rs

1//! The Tilo application SDK.
2//!
3//! The golden path is [`framework`]: declare a resource type and its actions
4//! with [`tilo_resource`], write views against a [`framework::ViewCx`], and
5//! [`framework::run`] provides storage, sync, collaboration, history, undo,
6//! permissions, the action palette, the agent and script door, and search.
7//! The CRDT engine is not part of that surface.
8//!
9//! The rendering toolkit is re-exported unchanged for widgets, custom
10//! surfaces and the imperative escape hatches.
11//!
12//! Application manifests normally rename this package to `ui`:
13//!
14//! ```toml
15//! ui = { package = "tilo-app", path = "../../crates/sdk/tilo-app" }
16//! ```
17//!
18//! That one line is the whole manifest an app needs. Every OS affordance is on
19//! by default — history, undo, permissions, collaboration and device sync,
20//! the host file chooser, media controls, notifications and GPU video — so an
21//! app never picks features to get them. A headless tool that wants only the
22//! toolkit sets `default-features = false`.
23//!
24//! Shell and framework internals may depend on the lower `tilo-ui` package directly;
25//! ordinary applications should use this facade.
26
27pub use ui_core::*;
28
29/// The host's raw operation protocol stays the toolkit's: this empty module
30/// takes the place of `ui_core::host_operation` in the glob above, so an app
31/// cannot name the protocol through the facade. Tests deliver operations with
32/// [`framework::HostDelivery`]; the framework reaches `ui_core` directly.
33#[doc(hidden)]
34pub mod host_operation {}
35
36/// The app framework: a workspace's records, their views, and the chains
37/// kept beside them, read and written by every app that opens them.
38pub mod framework;
39pub mod linked_accounts;
40mod runner;
41
42/// The toolkit's launch vocabulary, with `run` resolved to the runner this
43/// build targets: the native event loop on Linux and macOS, registration with
44/// the page runtime in the browser.
45pub mod launch {
46    pub use crate::runner::{run, run_or_exit};
47    pub use ui_core::launch::*;
48}
49
50/// The binary entry point of an app, identical on every target.
51///
52/// ```ignore
53/// ui::main!(hello::App);                           // a `ResourceApp`
54/// ui::main!(hello::App, init = hello::initialize);  // optional process setup
55/// ui::main!(definition = hello::definition);       // fn() -> &'static ApplicationDefinition
56/// // the release binary runs the app from its engine library (`engine_main!`)
57/// ui::main!(hello::App, init = hello::initialize, engine = "tilo_hello_engine");
58/// ```
59///
60/// On Linux and macOS it runs the native runner with the standard launch
61/// modes (`--shots`, `--headless`, …). In the browser it registers the app
62/// for the page runtime to mount.
63#[macro_export]
64macro_rules! main {
65    (definition = $definition:expr) => {
66        fn main() {
67            // A browser page mounts the definition itself; its binary is empty.
68            #[cfg(not(target_arch = "wasm32"))]
69            $crate::launch::run_or_exit(&$crate::framework::definition_launch_spec($definition()));
70        }
71    };
72    ($app:ty) => {
73        $crate::main!($app, init = || {});
74    };
75    ($app:ty, init = $init:expr) => {
76        fn main() {
77            ($init)();
78            #[cfg(not(target_arch = "wasm32"))]
79            $crate::framework::run::<$app>();
80        }
81    };
82    ($app:ty, init = $init:expr, engine = $engine:literal) => {
83        // The app's own build (`tilo-app dev`, tests, the browser) runs it
84        // here, as any app runs.
85        #[cfg(not(feature = "runtime-engine"))]
86        $crate::main!($app, init = $init);
87
88        // The release build's binary is one call into the app's engine
89        // library ([`engine_main!`]), which carries the app's library: the
90        // engine depends on the app's package, so the binary names the
91        // library itself rather than through Cargo.
92        #[cfg(feature = "runtime-engine")]
93        #[link(name = $engine, kind = "dylib")]
94        unsafe extern "C-unwind" {
95            safe fn tilo_app_engine_main();
96        }
97
98        #[cfg(feature = "runtime-engine")]
99        fn main() {
100            tilo_app_engine_main();
101        }
102    };
103}
104
105/// The entry an app's engine library exports for the app's binary: run the
106/// app, after `init`, as [`main!`] would. Invoke it once at the root of the
107/// engine library (a runtime engine that carries the app's library), and
108/// name the library in the app's `ui::main!(App, init = .., engine = "..")`.
109/// The app's package then holds only that call, and its code ships in the
110/// runtime bundle beside the runtime it is built against.
111///
112/// The app enables the engine form with a `runtime-engine` feature; without
113/// it the binary runs the app itself.
114#[macro_export]
115macro_rules! engine_main {
116    ($app:ty, init = $init:expr) => {
117        /// Run the app: the whole of its binary, which names this library and
118        /// calls this function by its unmangled name. A panic unwinds
119        /// through it as through any Rust call.
120        #[unsafe(no_mangle)]
121        pub extern "C-unwind" fn tilo_app_engine_main() {
122            ($init)();
123            $crate::framework::run::<$app>();
124        }
125    };
126}
127
128/// Declare the test that a drive of every scenario of an app reaches every
129/// action it publishes (`framework::testing::check_scenarios_reach_every_action`,
130/// built with `test-support`): none is advertised yet handled only by a
131/// click. The scenarios stage fixtures; the app someone opens acts on this
132/// machine (its files, its processes), so it is not walked.
133///
134/// `reach_tests!(Shell)` declares one test. An app whose scenarios take
135/// long to walk names one test per share, `shares: [share_0, share_1]`,
136/// which run side by side and together walk every scenario; `long:
137/// ["scenario"]` names scenarios publishing so many actions (one set per
138/// row of a long list) that every share asks its own part of them.
139#[macro_export]
140macro_rules! reach_tests {
141    ($app:ty) => {
142        $crate::reach_tests!($app, shares: [a_drive_of_every_scenario_reaches_every_published_action]);
143    };
144    ($app:ty, shares: [$($test:ident),+ $(,)?] $(, long: [$($long:literal),* $(,)?])? $(,)?) => {
145        #[cfg(test)]
146        mod reach_tests {
147            #[allow(unused_imports, reason = "the app type may be named by a path")]
148            use super::*;
149
150            const SHARES: &[&str] = &[$(stringify!($test)),+];
151            const LONG: &[&str] = &[$($($long),*)?];
152
153            $(
154                #[test]
155                fn $test() {
156                    let share = SHARES
157                        .iter()
158                        .position(|test| *test == stringify!($test))
159                        .unwrap_or_default();
160                    if let Err(failures) =
161                        $crate::framework::testing::check_scenarios_reach_every_action::<$app>(
162                            share,
163                            SHARES.len(),
164                            LONG,
165                        )
166                    {
167                        panic!("{failures}");
168                    }
169                }
170            )+
171        }
172    };
173}
174
175/// Export the entry points every platform's host enters the app library by.
176///
177/// Invoke it once at the root of the app library, naming the [`framework::ResourceApp`]
178/// type. The reload symbols `tilo-app dev` loads into the process that already
179/// owns the windows compile only for a `thin` build, the build that links one
180/// shared `ui` so the new view is the same type as the windows already on
181/// screen. On every target it also defines the library's `definition()`, the
182/// entry a web build registers with the browser's toolkit core, as component
183/// apps define theirs. On Android it defines `android_main`, which the
184/// `NativeActivity` of `tilo-app build --platform android` enters: the library
185/// built as a `cdylib` is the whole app there, run as [`framework::run`] runs it.
186#[macro_export]
187macro_rules! dev_reload_entry {
188    ($app:ty) => {
189        $crate::android_main!($crate::framework::run::<$app>);
190
191        #[cfg(feature = "thin")]
192        extern crate runtime_dylib;
193
194        #[cfg(feature = "thin")]
195        #[unsafe(no_mangle)]
196        pub unsafe extern "C" fn tilo_dev_apply(
197            id: *const u8,
198            id_len: usize,
199            state: *const u8,
200            state_len: usize,
201            out: *mut u8,
202            out_cap: usize,
203        ) -> isize {
204            let _ = (id, id_len);
205            unsafe {
206                $crate::framework::dev_reload::copy_window_state(state, state_len, out, out_cap)
207            }
208        }
209
210        #[cfg(feature = "thin")]
211        #[unsafe(no_mangle)]
212        pub extern "C" fn tilo_dev_linked_ui() -> *const () {
213            $crate::framework::dev_reload::linked_ui()
214        }
215
216        #[cfg(feature = "thin")]
217        #[unsafe(no_mangle)]
218        pub extern "C" fn tilo_dev_publish_root() -> i32 {
219            $crate::framework::dev_reload::publish_root::<$app>()
220        }
221
222        /// The app's toolkit definition, which a host running the app in its
223        /// own process (the web build, the shell's built-in apps) mounts. It
224        /// carries the app's committed surface: such a host runs no package
225        /// whose payload could name one. The app's own binary never calls it.
226        #[must_use]
227        pub fn definition() -> &'static $crate::prelude::ApplicationDefinition {
228            $crate::built_in_definition!($app)
229        }
230    };
231}
232
233/// The definition of the [`framework::ResourceApp`] `$app` for a host that
234/// runs it in its own process (the shell's built-in apps, the web desktop, an
235/// agent's participant host), naming the app crate's committed `surface.toml`
236/// as its surface: such a host runs no package whose payload names one. An
237/// app's library defines its `definition()` with it; the app's own binary
238/// uses [`framework::definition`], so it does not carry the file.
239#[macro_export]
240macro_rules! built_in_definition {
241    ($app:ty) => {{
242        $crate::register_built_in_surface(
243            <$app as $crate::framework::ResourceApp>::ID,
244            $crate::tilo_resource::committed_surface!(),
245        );
246        $crate::framework::definition::<$app>()
247    }};
248}
249
250/// Name the committed surface of an app a host runs in its own process,
251/// unless a package already named one. Called by [`built_in_definition!`].
252#[doc(hidden)]
253pub fn register_built_in_surface(app_id: &str, bytes: Option<&'static [u8]>) {
254    if let Some(bytes) = bytes {
255        ui_core::app_surface::register_built_in(app_id, bytes);
256    }
257}
258
259/// The resource layer, re-exported under its own name so `#[resource]` and
260/// `#[derive(Model)]` expansions resolve through `use ui::prelude::*`.
261pub use tilo_resource;
262
263/// The workspace's own emoji and its agents: records every window of the
264/// workspace opens, re-exported for the app that keeps them and for hosts
265/// that show them.
266pub use tilo_workspace_agent;
267pub use tilo_workspace_emoji;
268
269/// References to what apps keep ([`tilo_reference::ResourceRef`]), which
270/// [`framework::ViewCx::reference`] shows and opens, re-exported so an app
271/// names them without a dependency of its own.
272pub use tilo_reference;
273
274/// The package registry's vocabulary (entries, artifacts, moderation cases)
275/// that the SDK's moderation and package APIs speak, re-exported so an app
276/// names those types without a dependency of its own.
277pub use registry_types as registry;
278/// JSON values for action arguments, component props and native-input
279/// snapshots, re-exported so an app needs no dependency of its own.
280pub use serde_json;
281/// Reviewed feedback and durable submission, shared by every host.
282pub use tilo_feedback as feedback;
283pub mod feedback_composer;
284/// Files on every target: capability-scoped directories on Linux and macOS,
285/// the origin-private file system in the browser. Use it where `std::fs`
286/// would fail on the web.
287pub use tilo_files as files;
288
289/// What a hand-written [`Widget`] draws and lays
290/// out with: the renderer's scene and transforms, the layout tree and the
291/// windowing events. Views built from [`framework::ViewCx`] never name them.
292pub mod advanced {
293    pub use ui_core::taffy::{NodeId, TaffyTree};
294    pub use ui_core::vello::Scene;
295    pub use ui_core::vello::kurbo::Affine;
296    pub use ui_core::winit::event::WindowEvent;
297}
298
299/// The usual imports for application code.
300pub mod prelude {
301    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
302    pub use crate::framework::run;
303    #[cfg(not(target_arch = "wasm32"))]
304    pub use crate::framework::testing::{HeadlessApp, HostedApp, TestApp};
305    pub use crate::framework::{
306        ActionControl, Actions, Bindable, Command, Dispatcher, Each, EditsText, EffectCx,
307        EffectStatus, Effected, Fixture, Form, Jobs, LaunchInput, Live, LiveRow, PersonKind,
308        PersonRecord, Query, ReadFailure, Records, ResourceApp, ResourceSelection, ResourceSurface,
309        RunMode, SeedCx, Setting, Submit, SurfaceCx, TextControl, ViewCx, WorkspaceRecord,
310    };
311    pub use tilo_resource::{
312        self, ActionCx, ActionDecl, ActionKind, Atom, Collection, Counter, Delta, Demand, Edit,
313        Effect, Id, Invoke, Model, Outbox, Params, Path, PlainText, Refusal, Replica, Resource,
314        SearchEntry, Set, Settled, TextEdit, Undoing, resource,
315    };
316    pub use ui_core::i18n::LabelKey;
317
318    pub use ui_core::application::{
319        ANY_MEDIA_TYPE, AppComponent, ApplicationCatalog, ApplicationDefinition,
320        ComponentBehaviour, ComponentRunnerInputs, HostContext, HostContextInputs, LaunchTarget,
321        OpenFile, PlaybackHandoff, PreparedComponentInputs, RestoreInput,
322    };
323    pub use ui_core::application_view::{InstanceViewStore, SavedInstanceView};
324    pub use ui_core::arriving::{Arrival, Arriving};
325    pub use ui_core::launch::ComponentHostPreparer;
326    pub use ui_core::record::{RecordCheckout, RecordId, RecordKind, RecordSaveStatus};
327    pub use ui_core::storage::{DeviceClass, Scope, SettingsSchema};
328    pub use ui_core::tilo_actions;
329    pub use ui_core::tilo_actions::{ActionError, ActionNeed, ActionRegistry, ExecutionLocus};
330    pub use ui_core::tokens::TextRole;
331    pub use ui_core::vello::kurbo::{Point, Rect, Size};
332    pub use ui_core::widget::data_table::{
333        ColumnDef, ColumnWidth, DataTable, TableModel, TableModelChange,
334    };
335    pub use ui_core::widget::resource_actions;
336    pub use ui_core::widget::resource_actions::{ActionForm, ActionValue};
337    pub use ui_core::widget::virtual_list::{ListModel, ListModelChange, VirtualList};
338    pub use ui_core::widget::{
339        ActionEmitter, AudiencePresentationProvider, Banner, Button, Card, Checkbox, Collaborative,
340        Column, CompleteContentProvider, Container, ContentKind, Continuable, ControlBindingKind,
341        ControlBindingMetadata, Dropdown, Element, EmptyState, EventCtx, FilterCount, Filterable,
342        Filtered, FindBar, FindMatch, FindOptions, FindScope, Findable, FloatingSurfaceProvider,
343        FullscreenProvider, ModelView, NativeViewLifecycle, NavItem, NavList, NavMode, NavPane,
344        NavSection, NavShell, NumberInput, QrWidget, Row, Scrollable, SearchField,
345        SearchResultsView, Slider, Spacer, Spinner, Stack, Text, TextInput, Toggle, Widget,
346        labeled_check, labeled_field,
347    };
348    pub use ui_core::{
349        LatestRequest, LatestTask, LocalState, PresenceCtx, SearchRegistry, State, TextRenderer,
350        Theme, UndoAccess, UndoRequest, UndoStatus, collaboration, column, history_source,
351        presence, row, search, stack,
352    };
353}
354
355#[cfg(test)]
356mod tests {
357    use super::prelude::*;
358    // Implementing the toolkit's raw Widget contract is an escape hatch; the
359    // document types it names are imported explicitly, not from the prelude.
360
361    fn custom_event_handler(_event: &ui_core::winit::event::WindowEvent, ctx: &mut EventCtx<'_>) {
362        // App-defined widgets receive the same event and CRDT context as
363        // built-ins; implementing Widget requires no adapter framework.
364        let _ = (&mut ctx.state, ctx.path, ctx.node_id);
365    }
366
367    /// Extending a widget costs only the hooks it changes: a decorator
368    /// forwards every other `Widget` method to the widget it wraps.
369    struct CustomWidget {
370        inner: Text,
371    }
372
373    impl crate::widget::Decorator for CustomWidget {
374        type Inner = Text;
375
376        fn inner(&self) -> &Text {
377            &self.inner
378        }
379
380        fn inner_mut(&mut self) -> &mut Text {
381            &mut self.inner
382        }
383
384        fn native_input_snapshot(
385            &self,
386            _doc: &ui_core::widget::AppModel,
387            _path: &ui_core::widget::ModelPath,
388        ) -> Option<serde_json::Value> {
389            Some(serde_json::json!({ "width": self.inner.get_rect().width() }))
390        }
391
392        fn name(&self) -> &'static str {
393            "CustomWidget"
394        }
395    }
396
397    #[test]
398    fn prelude_exposes_the_integrated_custom_widget_surface() {
399        std::hint::black_box(custom_event_handler);
400        let custom = CustomWidget {
401            inner: Text::new("custom-widget"),
402        };
403        let widget: &dyn Widget = &custom;
404        assert_eq!(widget.name(), "CustomWidget");
405        assert_eq!(widget.get_rect(), custom.inner.get_rect());
406        std::hint::black_box(SearchRegistry::new());
407        std::hint::black_box(ActionForm::new("note", "Note", "save", "Save"));
408    }
409}