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}