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
36pub mod framework;
37mod runner;
38
39/// The toolkit's launch vocabulary, with `run` resolved to the runner this
40/// build targets: the native event loop on Linux and macOS, registration with
41/// the page runtime in the browser.
42pub mod launch {
43    pub use crate::runner::{run, run_or_exit};
44    pub use ui_core::launch::*;
45}
46
47/// The binary entry point of an app, identical on every target.
48///
49/// ```ignore
50/// ui::main!(hello::App);                           // a `ResourceApp`
51/// ui::main!(definition = hello::definition);       // fn() -> &'static ApplicationDefinition
52/// ```
53///
54/// On Linux and macOS it runs the native runner with the standard launch
55/// modes (`--shots`, `--headless`, …). In the browser it registers the app
56/// for the page runtime to mount.
57#[macro_export]
58macro_rules! main {
59    (definition = $definition:expr) => {
60        fn main() {
61            // A browser page mounts the definition itself; its binary is empty.
62            #[cfg(not(target_arch = "wasm32"))]
63            $crate::launch::run_or_exit(&$crate::framework::definition_launch_spec($definition()));
64        }
65    };
66    ($app:ty) => {
67        fn main() {
68            #[cfg(not(target_arch = "wasm32"))]
69            $crate::framework::run::<$app>();
70        }
71    };
72}
73
74/// Export the entry points every platform's host enters the app library by.
75///
76/// Invoke it once at the root of the app library, naming the [`framework::ResourceApp`]
77/// type. The reload symbols `tilo-app dev` loads into the process that already
78/// owns the windows compile only for a `thin` build, the build that links one
79/// shared `ui` so the new view is the same type as the windows already on
80/// screen. On every target it also defines the library's `definition()`, the
81/// entry a web build registers with the browser's toolkit core, as component
82/// apps define theirs. On Android it defines `android_main`, which the
83/// `NativeActivity` of `tilo-app build --platform android` enters: the library
84/// built as a `cdylib` is the whole app there, run as [`framework::run`] runs it.
85#[macro_export]
86macro_rules! dev_reload_entry {
87    ($app:ty) => {
88        $crate::android_main!($crate::framework::run::<$app>);
89
90        #[cfg(feature = "thin")]
91        extern crate runtime_dylib;
92
93        #[cfg(feature = "thin")]
94        #[unsafe(no_mangle)]
95        pub unsafe extern "C" fn tilo_dev_apply(
96            id: *const u8,
97            id_len: usize,
98            state: *const u8,
99            state_len: usize,
100            out: *mut u8,
101            out_cap: usize,
102        ) -> isize {
103            let _ = (id, id_len);
104            unsafe {
105                $crate::framework::dev_reload::copy_window_state(state, state_len, out, out_cap)
106            }
107        }
108
109        #[cfg(feature = "thin")]
110        #[unsafe(no_mangle)]
111        pub extern "C" fn tilo_dev_linked_ui() -> *const () {
112            $crate::framework::dev_reload::linked_ui()
113        }
114
115        #[cfg(feature = "thin")]
116        #[unsafe(no_mangle)]
117        pub extern "C" fn tilo_dev_publish_root() -> i32 {
118            $crate::framework::dev_reload::publish_root::<$app>()
119        }
120
121        /// The app's toolkit definition, which a host running the app in its
122        /// own process (the web build, the shell's built-in apps) mounts. It
123        /// carries the app's committed surface: such a host runs no package
124        /// whose payload could name one. The app's own binary never calls it.
125        #[must_use]
126        pub fn definition() -> &'static $crate::prelude::ApplicationDefinition {
127            $crate::built_in_definition!($app)
128        }
129    };
130}
131
132/// The definition of the [`framework::ResourceApp`] `$app` for a host that
133/// runs it in its own process (the shell's built-in apps, the web desktop, an
134/// agent's participant host), naming the app crate's committed `surface.toml`
135/// as its surface: such a host runs no package whose payload names one. An
136/// app's library defines its `definition()` with it; the app's own binary
137/// uses [`framework::definition`], so it does not carry the file.
138#[macro_export]
139macro_rules! built_in_definition {
140    ($app:ty) => {{
141        $crate::register_built_in_surface(
142            <$app as $crate::framework::ResourceApp>::ID,
143            $crate::tilo_resource::committed_surface!(),
144        );
145        $crate::framework::definition::<$app>()
146    }};
147}
148
149/// Name the committed surface of an app a host runs in its own process,
150/// unless a package already named one. Called by [`built_in_definition!`].
151#[doc(hidden)]
152pub fn register_built_in_surface(app_id: &str, bytes: Option<&'static [u8]>) {
153    if let Some(bytes) = bytes {
154        ui_core::app_surface::register_built_in(app_id, bytes);
155    }
156}
157
158/// The resource layer, re-exported under its own name so `#[resource]` and
159/// `#[derive(Model)]` expansions resolve through `use ui::prelude::*`.
160pub use tilo_resource;
161
162/// JSON values for action arguments, component props and native-input
163/// snapshots, re-exported so an app needs no dependency of its own.
164pub use serde_json;
165/// Reviewed feedback and durable submission, shared by every host.
166pub use tilo_feedback as feedback;
167pub mod feedback_composer;
168/// Files on every target: capability-scoped directories on Linux and macOS,
169/// the origin-private file system in the browser. Use it where `std::fs`
170/// would fail on the web.
171pub use tilo_files as files;
172
173/// What a hand-written [`Widget`](ui_core::widget::Widget) draws and lays
174/// out with: the renderer's scene and transforms, the layout tree and the
175/// windowing events. Views built from [`framework::ViewCx`] never name them.
176pub mod advanced {
177    pub use ui_core::taffy::{NodeId, TaffyTree};
178    pub use ui_core::vello::Scene;
179    pub use ui_core::vello::kurbo::Affine;
180    pub use ui_core::winit::event::WindowEvent;
181}
182
183/// The usual imports for application code.
184pub mod prelude {
185    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
186    pub use crate::framework::run;
187    #[cfg(not(target_arch = "wasm32"))]
188    pub use crate::framework::testing::{HeadlessApp, TestApp};
189    pub use crate::framework::{
190        ActionControl, Actions, Bindable, Command, Dispatcher, Each, EditsText, EffectCx,
191        EffectStatus, Effected, Fixture, Form, Jobs, LaunchInput, Live, LiveRow, PersonRecord,
192        Records, ResourceApp, ResourceSelection, ResourceSurface, RunMode, SeedCx, Setting, Submit,
193        SurfaceCx, TextControl, ViewCx,
194    };
195    pub use tilo_resource::{
196        self, ActionCx, ActionDecl, ActionKind, Atom, Collection, Counter, Delta, Demand, Edit,
197        Effect, Id, Invoke, Json, Model, Outbox, Params, Path, PlainText, Refusal, Replica,
198        Resource, SearchEntry, Set, Settled, TextEdit, Undoing, resource,
199    };
200    pub use ui_core::i18n::LabelKey;
201
202    pub use ui_core::application::{
203        ANY_MEDIA_TYPE, AppComponent, ApplicationCatalog, ApplicationDefinition,
204        ComponentBehaviour, ComponentRunnerInputs, HostContext, HostContextInputs, LaunchTarget,
205        OpenFile, PlaybackHandoff, PreparedComponentInputs, RestoreInput,
206    };
207    pub use ui_core::application_view::{InstanceViewStore, SavedInstanceView};
208    pub use ui_core::launch::ComponentHostPreparer;
209    pub use ui_core::record::{RecordCheckout, RecordId, RecordKind, RecordSaveStatus};
210    pub use ui_core::storage::{Scope, SettingsSchema};
211    pub use ui_core::tilo_actions;
212    pub use ui_core::tilo_actions::{ActionError, ActionNeed, ActionRegistry, ExecutionLocus};
213    pub use ui_core::tokens::TextRole;
214    pub use ui_core::vello::kurbo::{Point, Rect, Size};
215    pub use ui_core::widget::data_table::{
216        ColumnDef, ColumnWidth, DataTable, TableModel, TableModelChange, TableRowState,
217    };
218    pub use ui_core::widget::resource_actions;
219    pub use ui_core::widget::resource_actions::{ActionForm, ActionValue};
220    pub use ui_core::widget::virtual_list::{
221        ListModel, ListModelChange, ListRowState, VirtualList,
222    };
223    pub use ui_core::widget::{
224        ActionEmitter, AudiencePresentationProvider, Banner, Button, Card, Checkbox, Collaborative,
225        Column, CompleteContentProvider, Container, Continuable, ControlBindingKind,
226        ControlBindingMetadata, Dropdown, Element, EmptyState, EventCtx, FilterCount, Filterable,
227        Filtered, FindBar, FindMatch, FindOptions, FindScope, Findable, FloatingSurfaceProvider,
228        FullscreenProvider, ModelView, NativeViewLifecycle, NavItem, NavList, NavMode, NavPane,
229        NavSection, NavShell, NumberInput, QrWidget, Row, Scrollable, SearchField,
230        SearchResultsView, Slider, Spacer, Spinner, Stack, Text, TextInput, Toggle, TreeNode,
231        TreeView, Widget, labeled_check, labeled_field,
232    };
233    pub use ui_core::{
234        LatestRequest, LatestTask, LocalState, PresenceCtx, RevisionHistory, SearchRegistry, State,
235        TextRenderer, Theme, UndoAccess, UndoRequest, UndoStatus, collaboration, column,
236        history_source, presence, row, search, stack,
237    };
238}
239
240#[cfg(test)]
241mod tests {
242    use super::prelude::*;
243    // Implementing the toolkit's raw Widget contract is an escape hatch; the
244    // document types it names are imported explicitly, not from the prelude.
245
246    fn custom_event_handler(_event: &ui_core::winit::event::WindowEvent, ctx: &mut EventCtx<'_>) {
247        // App-defined widgets receive the same event and CRDT context as
248        // built-ins; implementing Widget requires no adapter framework.
249        let _ = (&mut ctx.state, ctx.path, ctx.node_id);
250    }
251
252    /// Extending a widget costs only the hooks it changes: a decorator
253    /// forwards every other `Widget` method to the widget it wraps.
254    struct CustomWidget {
255        inner: Text,
256    }
257
258    impl crate::widget::Decorator for CustomWidget {
259        type Inner = Text;
260
261        fn inner(&self) -> &Text {
262            &self.inner
263        }
264
265        fn inner_mut(&mut self) -> &mut Text {
266            &mut self.inner
267        }
268
269        fn native_input_snapshot(
270            &self,
271            _doc: &ui_core::widget::AppModel,
272            _path: &ui_core::widget::ModelPath,
273        ) -> Option<serde_json::Value> {
274            Some(serde_json::json!({ "width": self.inner.get_rect().width() }))
275        }
276
277        fn name(&self) -> &'static str {
278            "CustomWidget"
279        }
280    }
281
282    #[test]
283    fn prelude_exposes_the_integrated_custom_widget_surface() {
284        std::hint::black_box(custom_event_handler);
285        let custom = CustomWidget {
286            inner: Text::new("custom-widget"),
287        };
288        let widget: &dyn Widget = &custom;
289        assert_eq!(widget.name(), "CustomWidget");
290        assert_eq!(widget.get_rect(), custom.inner.get_rect());
291        std::hint::black_box(SearchRegistry::new());
292        std::hint::black_box(ActionForm::new("note", "Note", "save", "Save"));
293    }
294}