Skip to main content

tilo_app/framework/
surface.rs

1//! The contract a custom widget or renderer implements to take part in a
2//! resource: collaboration, history, undo, permissions and the action door.
3
4use std::cell::RefCell;
5use std::rc::{Rc, Weak};
6use ui_core::widget::{AppModel, ModelPath};
7
8use tilo_resource::{ActionDecl, Effect, NoEffect, Resource, SearchEntry};
9use ui_core::automerge::transaction::Transactable as _;
10use ui_core::automerge::{AutoCommit, ObjId};
11use ui_core::host_operation::{ResourceOperation, ResourceOperationStart};
12use ui_core::presence::PresenceKey;
13use ui_core::widget::{Decorator, Widget};
14use ui_core::{PresencePayload, WakeHandle};
15
16use super::controls::Actions;
17use super::dispatch::Dispatcher;
18use super::effect::{EffectAnswer, EffectCx, EffectRunner, EffectStatus, Effects, Jobs, OwnedJobs};
19use super::live::Live;
20use super::rows::{LiveRow, outside_document, resolved_outside_document};
21
22/// A custom widget or renderer — a timeline, a canvas, a GPU viewport, a page
23/// layout engine — taking part in a resource.
24///
25/// Pixels, layout and input stay the toolkit's `Widget` contract, unchanged.
26/// This trait adds only what the framework cannot infer:
27///
28/// * **Read** — [`present`](Self::present) receives the resource state
29///   whenever it changes: a local edit, a collaborator's, an undo. In History
30///   mode the framework builds the surface again from a read-only `ViewCx`
31///   and presents a *past* version, so time travel needs no extra code as
32///   long as the surface draws from what it was presented. A surface over a
33///   large resource reads through [`SurfaceCx::live`] instead of copying,
34///   and demands the parts it shows ([`Live::demand`]) from the document its
35///   `sync` receives — the live one, or the past version in History mode.
36/// * **Write** — edits go through the [`Dispatcher`] handed to
37///   [`attach`](Self::attach): a named action, or any typed action value via
38///   [`Dispatcher::apply`]. The host checks permissions, applies, records one
39///   undo step per action, attributes it in history and replicates it. A
40///   surface never writes the document.
41/// * **Undo granularity** — one action is one undo step. Preview a gesture
42///   locally (a drag, a scrub) and dispatch one action when it ends; a
43///   gesture that is several edits goes in one [`Dispatcher::group`] (or
44///   holds a [`Dispatcher::begin_group`] guard for the input event), so one
45///   Ctrl+Z takes all of it back.
46/// * **Permissions** — [`RecordEnv::read_only`](super::RecordEnv::read_only) says the participant may
47///   only view; present accordingly. It follows role changes while the app
48///   runs, and [`read_only_changed`](Self::read_only_changed) announces each
49///   one. Refused actions change nothing.
50/// * **Confirmation** — a typed value whose
51///   [`risk`](tilo_actions::Action::risk) is destructive (deleting a clip)
52///   waits for the user's confirmation, like a declared destructive action.
53///   When an edit is refused or held for confirmation, the surface is
54///   presented the stored state again, undoing any optimistic preview; a
55///   confirmed edit then arrives as an ordinary change.
56/// * **Presence** — report where the user is as a [`ResourceSelection`] from
57///   the toolkit's `Widget::presence_payload` (one line:
58///   `selection.presence::<R>(app_id)`), and draw collaborators from the
59///   `PresenceCtx` every draw receives, under the same keys.
60/// * **Effects** — work with no document delta (back up now, connect, run a
61///   query) is the surface's effect vocabulary `E`, a `#[derive(Effect)]`
62///   enum: implement `ResourceSurface<R, E>` and answer each effect in
63///   [`effect`](Self::effect) with `&mut self`, starting a job through the
64///   [`EffectCx`] when it takes time. Every caller — a button
65///   ([`Dispatcher::effect`]), the palette, an agent — reaches it the same
66///   way; [`effects`](Self::effects) says which are unavailable right now.
67/// * **Run mode** — start work that reaches outside the process (a decoder
68///   service, a socket, a probe) only when [`RecordEnv::mode`](super::RecordEnv::mode) is live. A
69///   capture reads [`RecordEnv::fixture`](super::RecordEnv::fixture) instead; a past version starts
70///   nothing. Work still in flight that the frame shows (a page render, a
71///   decode) is reported by the toolkit's `Widget::has_pending_visual_work`;
72///   a capture waits for it.
73/// * **One published tree** — what the surface offers the palette, agents
74///   and scripts joins the resource's own publication:
75///   [`bound_declarations`](Self::bound_declarations) for edits bound to
76///   where the user is (delete the selected clip, bookmark the current
77///   folder), and [`live_rows`](Self::live_rows) for the live external
78///   entities it shows (processes, containers), which carry only view and
79///   effect operations. A document action the surface's own
80///   `PublishesResources` offers is dropped, so nothing reaches the
81///   document beside the resource's actions.
82pub trait ResourceSurface<R: Resource, E: Effect = NoEffect>: Widget {
83    /// The resource state to show.
84    fn present(&mut self, state: &Rc<R::State>);
85
86    fn supports_device_shell(&self) -> bool {
87        false
88    }
89
90    /// Answer a private owner operation from the authenticated hosting shell.
91    ///
92    /// # Errors
93    /// Refuses unsupported or unavailable device-shell operations.
94    fn device_shell_operation(
95        &mut self,
96        _operation: &ui_core::floating_surface::DeviceShellOperation,
97    ) -> Result<serde_json::Value, String> {
98        Err("This surface has no device shell".into())
99    }
100
101    /// A trusted hosting shell owner selected the device-shell target. No grant is implied.
102    fn request_device_shell(&mut self) -> bool {
103        false
104    }
105
106    /// The latest bounded query from the platform's canonical search source.
107    /// Runs on the UI thread before sync, coalescing queries between frames.
108    /// A surface may refresh remote results through its attached jobs; expose
109    /// their entries through `search_entries`, rather than another provider.
110    /// Never called for a History view and lends no document.
111    fn search_query(&mut self, query: &ui_core::search::SearchQuery) {
112        let _ = query;
113    }
114
115    /// Consume a request to retry this window's failed record save, once.
116    /// The framework's record owner performs the retry after releasing its
117    /// document loan; the surface never writes or flushes storage itself.
118    /// History surfaces cannot request a retry.
119    fn take_save_retry(&mut self) -> bool {
120        false
121    }
122
123    /// A pending device-local request for the hosting shell, consumed once.
124    /// The host answers it through its permission flow; this callback lends no document.
125    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
126        None
127    }
128
129    /// The channel edits go through, and whether they will be accepted.
130    /// Called once, before the first [`present`](Self::present).
131    fn attach(&mut self, cx: SurfaceCx<R>) {
132        let _ = cx;
133    }
134
135    /// A participant's action — an agent's verb, a collaborator's edit
136    /// taken through the door — left them at `place`, as the resource says
137    /// it lands ([`Resource::lands`](tilo_resource::Resource::lands)).
138    /// Called after [`present`](Self::present) showed the state holding it.
139    /// Presence draws them there already; a surface that acts at a
140    /// participant's place (typing at an agent's caret) reads it here, or
141    /// any time through [`Live::place_of`](super::Live::place_of), instead
142    /// of keeping its own copy.
143    fn landed(&mut self, participant: &tilo_actions::ParticipantId, place: &tilo_resource::Place) {
144        let _ = (participant, place);
145    }
146
147    /// The participant's permission to edit changed while the app runs: a
148    /// role granted or taken away. Present accordingly. The dispatcher already
149    /// refuses or accepts edits under the new permissions.
150    fn read_only_changed(&mut self, read_only: bool) {
151        let _ = read_only;
152    }
153
154    /// Stage view state for a screenshot scenario (`None` for a plain
155    /// `--shots`): a selection, an open menu, a drag in flight. Called at the
156    /// start of the sync after the scenario's actions applied and were laid
157    /// out, so the surface has been presented their result, knows its
158    /// geometry, and its own `sync` follows with the document. Edits it dispatches here are submitted like any other and
159    /// applied before the frame is captured. Must be deterministic: the same
160    /// scenario stages the same scene on every run. Never called in a live
161    /// window.
162    fn stage(&mut self, scenario: Option<&str>) {
163        let _ = scenario;
164    }
165
166    /// Answer `effect`, invoked by a control, the palette, an agent or a
167    /// script: finish now ([`Effected::done`](super::Effected::done)), or
168    /// start a job with [`EffectCx::thread`], [`EffectCx::spawn`] or
169    /// [`EffectCx::reply`] and answer [`Effected::Pending`](super::Effected::Pending)
170    /// (only for an effect declared `job`). A job's completion runs on the
171    /// UI thread with `&mut self` again, and changes the document through
172    /// the context's dispatcher. Never called in History mode.
173    ///
174    /// # Errors
175    /// Returns why the effect failed, which its caller is told.
176    fn effect(&mut self, effect: E, cx: &mut EffectCx<R, Self>) -> EffectAnswer
177    where
178        Self: Sized,
179    {
180        let _ = cx;
181        Err(format!("`{}` has no handler", effect.id()))
182    }
183
184    /// The effects that differ from their declaration right now, as the
185    /// surface's state says: unavailable while the engine is offline,
186    /// relabelled while recording. Read after every sync, so the published
187    /// actions follow the surface.
188    fn effects(&self) -> Vec<EffectStatus> {
189        Vec::new()
190    }
191
192    /// The resource's own actions that differ from their declaration right
193    /// now, as the surface's live state says (`Camera::SET_MODE` unavailable
194    /// while recording, with why). Read whenever the resource is published,
195    /// so the palette, agents and the door follow the surface: an action
196    /// unavailable here is refused with its reason.
197    fn availability(&self) -> Vec<EffectStatus> {
198        Vec::new()
199    }
200
201    /// Edits bound to the surface's view state — the selection, the caret,
202    /// the playhead, the folder or page shown — as typed builders of the
203    /// resource's action, published on the resource beside its declared
204    /// actions. Invoking one dispatches the action it builds through the
205    /// same permission check, confirmation and undo as any other. An id the
206    /// resource already publishes keeps the resource's action, unless the
207    /// bound one refines it ([`tilo_resource::bind_declaration`]): same
208    /// parameters, permission and risk, adding only where it was invoked
209    /// from, such as the caret an append leaves its writer at.
210    fn bound_declarations(&self) -> Vec<ActionDecl<R>> {
211        Vec::new()
212    }
213
214    /// The live external entities the surface shows, published as children
215    /// of the resource and merged by id with its document children. Keep it
216    /// to what fits a published tree (the rows on screen, a bounded page)
217    /// and resolve the rest in [`live_row`](Self::live_row).
218    fn live_rows(&self) -> Vec<LiveRow> {
219        Vec::new()
220    }
221
222    /// The live row `id`, when [`live_rows`](Self::live_rows) leaves it out,
223    /// so an action addressed to any entity the surface knows still reaches
224    /// it.
225    fn live_row(&self, id: &str) -> Option<LiveRow> {
226        let _ = id;
227        None
228    }
229
230    /// Edits of the resource bound to the live row `row` (rename *this*
231    /// file): typed builders of the resource's action, published on that row
232    /// and applied through the same permission check, confirmation and undo
233    /// as the resource's own declarations. A row itself carries only view
234    /// and effect operations; this is how it offers a document change.
235    fn row_declarations(&self, row: &str) -> Vec<ActionDecl<R>> {
236        let _ = row;
237        Vec::new()
238    }
239
240    /// Search entries for what the surface shows that the resource does not
241    /// hold — a page's text, a book's outline, the processes running —
242    /// indexed with the resource's own ([`Resource::search`]) as the app's
243    /// one search source. Read whenever someone searches, so keep it to what
244    /// the surface already has at hand.
245    fn search_entries(&self) -> Vec<SearchEntry> {
246        Vec::new()
247    }
248}
249
250/// Run `forward`, a pass of a surface over the document, keeping every change
251/// it makes outside its dispatcher out of the document
252/// ([`refuse_raw_writes`]).
253fn only_through_actions(
254    state: &mut ui_core::widget::Edits<'_>,
255    path: &ModelPath,
256    pass: &str,
257    forward: impl FnOnce(&mut ui_core::widget::Edits<'_>, &ModelPath),
258) {
259    let before = ui_core::doc::document(state, path).0.pending_ops();
260    forward(state, path);
261    refuse_raw_writes(ui_core::doc::document(state, path).0, before, pass);
262}
263
264/// A surface changes its resource only through its dispatcher: every action
265/// it applies is committed as one change, attributed and undoable. Anything
266/// else would remain uncommitted; the typed loan prevents such a write.
267/// A debug build also checks this invariant at the callback boundary.
268fn refuse_raw_writes(doc: &AutoCommit, before: usize, pass: &str) {
269    if before != 0 || doc.pending_ops() == 0 {
270        return;
271    }
272    let dropped = doc.pending_ops();
273    tracing::error!(
274        pass,
275        dropped,
276        "a surface wrote the document outside an action; the write was refused"
277    );
278    debug_assert_eq!(
279        dropped, 0,
280        "a surface wrote the document outside an action during `{pass}`"
281    );
282}
283
284/// What a hosted surface adds to its resource's publication, as the root
285/// collects it.
286pub(crate) trait SurfacePublication {
287    /// The resource type the surface is hosted on; a root collects only its
288    /// own type's surfaces.
289    fn resource(&self) -> std::any::TypeId;
290    /// Add the edits the surface binds to where the user is to `into`, a
291    /// `Vec<ActionDecl<R>>` of its resource type `R`.
292    fn bound_declarations(&self, into: &mut dyn std::any::Any);
293    fn availability(&self) -> Vec<EffectStatus>;
294    fn live_rows(&self) -> Vec<LiveRow>;
295    fn live_row(&self, id: &str) -> Option<LiveRow>;
296    /// Whether the surface binds any edit on the row `row`.
297    fn offers_on_row(&self, row: &str) -> bool;
298    /// Add the edits the surface binds on the row `row` to `into`, a
299    /// `Vec<ActionDecl<R>>` of its resource type `R`; whether it bound any.
300    fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool;
301    fn search_entries(&self) -> Vec<SearchEntry>;
302    fn supports_device_shell(&self) -> bool;
303    fn device_shell_operation(
304        &mut self,
305        operation: &ui_core::floating_surface::DeviceShellOperation,
306    ) -> Result<serde_json::Value, String>;
307    fn request_device_shell(&mut self) -> bool;
308    fn search_queries(&self) -> SearchQueries;
309    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent>;
310    fn take_save_retry(&mut self) -> bool;
311}
312
313/// Coalesced query delivery owned by the canonical provider and surface host.
314#[derive(Clone, Default)]
315pub(crate) struct SearchQueries(Rc<RefCell<QueryDelivery>>);
316
317#[derive(Default)]
318struct QueryDelivery {
319    last: Option<ui_core::search::SearchQuery>,
320    pending: Option<ui_core::search::SearchQuery>,
321}
322
323impl SearchQueries {
324    pub(crate) fn submit(&self, query: &ui_core::search::SearchQuery) {
325        if query
326            .text()
327            .chars()
328            .nth(ui_core::search::MAX_SEARCH_QUERY_SCALARS)
329            .is_some()
330        {
331            return;
332        }
333        let mut delivery = self.0.borrow_mut();
334        if delivery.last.as_ref() != Some(query) {
335            delivery.last = Some(query.clone());
336            delivery.pending = Some(query.clone());
337        }
338    }
339
340    fn take(&self) -> Option<ui_core::search::SearchQuery> {
341        self.0.borrow_mut().pending.take()
342    }
343}
344
345/// What a surface receives when attached.
346pub struct SurfaceCx<R: Resource> {
347    pub(crate) env: super::env::RecordEnv<R>,
348    pub(crate) jobs: Jobs<R>,
349    pub(crate) actions: Actions<R>,
350}
351
352impl<R: Resource> std::ops::Deref for SurfaceCx<R> {
353    type Target = super::env::RecordEnv<R>;
354    fn deref(&self) -> &super::env::RecordEnv<R> {
355        &self.env
356    }
357}
358
359impl<R: Resource> SurfaceCx<R> {
360    /// The host-owned identity of the record this surface shows. A durable
361    /// activation link can name it without relying on the last-open window.
362    /// Absent in a historical view that cannot launch live work.
363    pub fn record(&self) -> Option<&ui_core::record::RecordId> {
364        self.run.launched.record.as_ref()
365    }
366
367    /// What [`RecordEnv::open_here`](super::env::RecordEnv::open_here) does, kept for later: the input that asks
368    /// for another record arrives after attaching (a file chosen in a
369    /// picker). `None` in a past version and a capture.
370    pub fn opener(&self) -> Option<super::OpenHere> {
371        self.run.opener.clone()
372    }
373
374    /// Jobs the surface starts on its own, with nobody invoking an effect;
375    /// their completions get `&mut` the surface (`jobs.start::<Self>(…)`).
376    /// Keep the handle. See [`Jobs`].
377    pub fn jobs(&self) -> Jobs<R> {
378        self.jobs.clone()
379    }
380
381    /// The resource's published actions and the surface's effects, for
382    /// binding the toolkit controls a surface builds itself (a toggle, a
383    /// dropdown, a button deep in its own widget tree) the way a view's
384    /// controls are bound: [`Actions::bind`], [`Actions::bind_effect`].
385    pub fn actions(&self) -> Actions<R> {
386        self.actions.clone()
387    }
388
389    /// The live state, for a surface that reads it where it needs it (in
390    /// `draw`, in an event) rather than copying what it was presented, and
391    /// that asks for the parts of a large resource it shows
392    /// ([`Live::demand`]).
393    pub fn live(&self) -> Live<R> {
394        self.live.clone()
395    }
396
397    /// The file whose history History shows beside the resource's, for an
398    /// app that declares [`ResourceApp::FILE_HISTORY`](super::ResourceApp::FILE_HISTORY):
399    /// set its target to the file the surface shows (the selected one) and
400    /// open it from the surface's own History command. `None` for any other
401    /// app, and in a past version.
402    pub fn file_history(&self) -> Option<ui_core::history_mode::checked_files::FileHistorySource> {
403        self.run
404            .file_history
405            .as_ref()
406            .map(|(source, _)| source.clone())
407    }
408
409    /// The id of the app the surface is part of, which its presence keys
410    /// name ([`ResourceSelection::presence`]).
411    pub fn app_id(&self) -> &'static str {
412        self.dispatch.app_id()
413    }
414
415    /// The app's secrets. A secret of one document item (a profile's
416    /// password) is `cx.secrets().item(purpose, id)`.
417    pub fn secrets(&self) -> ui_core::secrets::SecretStore {
418        self.storage.storage().secrets()
419    }
420
421    /// The app's private file areas.
422    pub fn files(&self) -> ui_core::storage::AppFiles {
423        self.storage.storage().files()
424    }
425
426    /// Wakes the app from another thread — a worker's result, a socket's
427    /// message — so the surface syncs and draws it without waiting for
428    /// input.
429    pub fn waker(&self) -> WakeHandle {
430        self.effects
431            .env()
432            .map_or_else(WakeHandle::new, |env| env.wake.clone())
433    }
434}
435
436/// Where a participant is inside a resource, in the resource's own terms, so
437/// it means the same thing on every collaborator's screen.
438#[derive(Clone, Debug, PartialEq)]
439pub enum ResourceSelection {
440    /// An item (a clip, a card, a row) is selected.
441    Item(String),
442    /// A caret at a position inside an item (a text field, a cell).
443    Caret { item: String, at: usize },
444    /// A range inside an item: characters of a field, frames of a clip.
445    Range {
446        item: String,
447        start: usize,
448        end: usize,
449    },
450    /// A pointer over an item, at a point of the shared window (logical
451    /// pixels): where a collaborator is aiming in a surface that draws
452    /// their cursor itself.
453    Pointer { item: String, x: f32, y: f32 },
454}
455
456impl ResourceSelection {
457    /// The toolkit presence frame for this selection, keyed to the resource
458    /// item so peers resolve it against their own layout.
459    pub fn presence<R: Resource>(&self, app_id: &str) -> (PresenceKey, PresencePayload) {
460        self.presence_of(R::KIND, app_id)
461    }
462
463    /// [`Self::presence`] for a resource known by its `kind` alone.
464    pub fn presence_of(&self, kind: &str, app_id: &str) -> (PresenceKey, PresencePayload) {
465        let key = |item: &str| PresenceKey::Resource {
466            app: app_id.to_owned(),
467            id: item.to_owned(),
468            kind: kind.to_owned(),
469        };
470        match self {
471            Self::Item(item) => (key(item), PresencePayload::Selection { start: 0, end: 0 }),
472            Self::Caret { item, at } => (key(item), PresencePayload::Caret { pos: *at }),
473            Self::Range { item, start, end } => (
474                key(item),
475                PresencePayload::Selection {
476                    start: *start,
477                    end: *end,
478                },
479            ),
480            Self::Pointer { item, x, y } => (key(item), PresencePayload::Pointer { x: *x, y: *y }),
481        }
482    }
483}
484
485impl From<tilo_resource::Place> for ResourceSelection {
486    fn from(place: tilo_resource::Place) -> Self {
487        match place {
488            tilo_resource::Place::Item(item) => Self::Item(item),
489            tilo_resource::Place::Caret { item, at } => Self::Caret { item, at },
490            tilo_resource::Place::Range { item, start, end } => Self::Range { item, start, end },
491        }
492    }
493}
494
495/// Presents resource changes to a hosted surface, routes its effects to it,
496/// and otherwise stays out of its way: layout, drawing and input are the
497/// surface's own.
498///
499/// The surface is laid out first, so the host and the surface it forwards
500/// its name to are one address: the window's map of named widgets, which
501/// keys a widget by address and name, finds the surface's own node when a
502/// test or a headless drive names it.
503#[repr(C)]
504pub(crate) struct SurfaceHost<R: Resource, S, E> {
505    surface: S,
506    search_queries: SearchQueries,
507    live: Live<R>,
508    presented: u64,
509    /// The number of the next landing to tell the surface of.
510    landings: u64,
511    dispatch: Dispatcher<R>,
512    read_only: bool,
513    /// A scenario to stage on the next sync.
514    staging: Staging,
515    /// Runs the surface's effects; `None` in History mode, where they never
516    /// run.
517    effects: Option<EffectRunner<R, S, E>>,
518    /// What the surface last said of its effects, which the root publishes.
519    statuses: Rc<RefCell<Vec<EffectStatus>>>,
520    /// The jobs the surface started on its own.
521    jobs: OwnedJobs<R, S>,
522}
523
524/// The scenario a surface stages on its next sync: outer `None` is nothing
525/// to stage, inner `None` is a plain `--shots`.
526pub(crate) type Staging = Rc<RefCell<Option<Option<String>>>>;
527
528impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceHost<R, S, E> {
529    pub(crate) fn new(
530        live: Live<R>,
531        dispatch: Dispatcher<R>,
532        effects: &Effects<R>,
533        jobs: Jobs<R>,
534        alive: Weak<()>,
535        surface: S,
536    ) -> Self {
537        let statuses = Rc::new(RefCell::new(surface.effects()));
538        let runner = effects.env().map(|env| EffectRunner::new(env.clone()));
539        // Captures and History describe their controls too. Publication is
540        // inert metadata; only the runner below can execute an effect.
541        if !E::declarations().is_empty() {
542            let published = Rc::clone(&statuses);
543            effects.publish::<E>(alive, move || {
544                super::effect::published_specs::<E>(&published.borrow())
545            });
546        }
547        Self {
548            presented: live.revision(),
549            landings: live.landings_since(u64::MAX).1,
550            live,
551            read_only: dispatch.read_only(),
552            dispatch,
553            staging: Staging::default(),
554            effects: runner,
555            statuses,
556            jobs: OwnedJobs::new(jobs),
557            surface,
558            search_queries: SearchQueries::default(),
559        }
560    }
561
562    pub(crate) fn staging(&self) -> Staging {
563        Rc::clone(&self.staging)
564    }
565}
566
567/// What a [`HostedSurface`] asks of the host of a surface of particular
568/// types: the only surface-hosting code compiled per surface.
569pub(crate) trait SurfaceDriver: SurfacePublication {
570    /// The surface, as a widget.
571    fn surface(&self) -> &(dyn Widget + 'static);
572    fn surface_mut(&mut self) -> &mut (dyn Widget + 'static);
573    /// The surface by its own type, for whoever reaches it so (a test
574    /// driving it, a host that embeds it).
575    fn surface_any(&self) -> &dyn std::any::Any;
576    fn surface_any_mut(&mut self) -> &mut dyn std::any::Any;
577    /// Whether `event` only scrolls the view, with nothing for the host to
578    /// present first.
579    fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool;
580    /// Present what changed, then sync the surface.
581    fn sync(&mut self, state: &AppModel, path: &ModelPath);
582    /// Start `op` as one of the surface's effects, if it is one.
583    ///
584    /// # Errors
585    /// The effect's refusal.
586    fn start_effect(
587        &mut self,
588        op: &ResourceOperation<'_>,
589        doc: &AutoCommit,
590    ) -> Result<Option<ResourceOperationStart>, String>;
591    /// Progress of one of the surface's effects, if `invocation` is one.
592    fn poll_effect(
593        &mut self,
594        invocation: tilo_actions::InvocationId,
595        cancellation_requested: bool,
596    ) -> Option<tilo_actions::InvocationUpdate>;
597}
598
599impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceDriver
600    for SurfaceHost<R, S, E>
601{
602    fn surface(&self) -> &(dyn Widget + 'static) {
603        &self.surface
604    }
605
606    fn surface_mut(&mut self) -> &mut (dyn Widget + 'static) {
607        &mut self.surface
608    }
609
610    fn surface_any(&self) -> &dyn std::any::Any {
611        &self.surface
612    }
613
614    fn surface_any_mut(&mut self) -> &mut dyn std::any::Any {
615        &mut self.surface
616    }
617
618    fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
619        self.live.revision() == self.presented
620            && self.staging.borrow().is_none()
621            && self.search_queries.0.borrow().pending.is_none()
622            && self.dispatch.read_only() == self.read_only
623            && self.surface.view_only_scroll(event)
624    }
625
626    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
627        let read_only = self.dispatch.read_only();
628        if read_only != self.read_only {
629            self.read_only = read_only;
630            self.surface.read_only_changed(read_only);
631        }
632        if self.live.revision() != self.presented {
633            self.presented = self.live.revision();
634            self.surface.present(&self.live.get());
635        }
636        let (landed, next) = self.live.landings_since(self.landings);
637        self.landings = next;
638        for (participant, place) in &landed {
639            self.surface.landed(participant, place);
640        }
641        let staged = self.staging.borrow_mut().take();
642        if let Some(scenario) = staged {
643            self.surface.stage(scenario.as_deref());
644        }
645        if let Some(query) = self.search_queries.take()
646            && self.effects.is_some()
647        {
648            self.surface.search_query(&query);
649        }
650        self.jobs.poll(&mut self.surface);
651        self.surface.sync(state, path);
652        if self.effects.is_some() {
653            let now = self.surface.effects();
654            if *self.statuses.borrow() != now {
655                *self.statuses.borrow_mut() = now;
656            }
657        }
658    }
659    fn start_effect(
660        &mut self,
661        op: &ResourceOperation<'_>,
662        doc: &AutoCommit,
663    ) -> Result<Option<ResourceOperationStart>, String> {
664        match self.effects.as_mut() {
665            Some(runner) => runner.start(op, &mut self.surface, Some(doc), S::effect),
666            None => Ok(None),
667        }
668    }
669
670    fn poll_effect(
671        &mut self,
672        invocation: tilo_actions::InvocationId,
673        cancellation_requested: bool,
674    ) -> Option<tilo_actions::InvocationUpdate> {
675        self.effects
676            .as_mut()?
677            .poll(invocation, cancellation_requested, &mut self.surface)
678    }
679}
680
681/// A hosted surface as mounted in the view: its [`SurfaceHost`] behind the
682/// one widget type every surface shares.
683pub(crate) struct HostedSurface {
684    host: Box<dyn SurfaceDriver>,
685}
686
687impl HostedSurface {
688    /// `host` mounted as a widget. The widget is made here, outside any
689    /// generic code, so its implementation of the widget contract is
690    /// compiled once in the shared runtime rather than in every app.
691    #[inline(never)]
692    pub(crate) fn element(host: Box<dyn SurfaceDriver>) -> ui_core::widget::Element {
693        Box::new(Self { host })
694    }
695}
696
697// The host adds three things to the surface — presenting new resource state
698// before the surface syncs, answering its effects, and publishing what it
699// says of them — and forwards everything else to it.
700impl Decorator for HostedSurface {
701    fn provide<'a>(&'a self, request: &mut ui_core::widget::capability::Request<'a, '_>) {
702        if request.wants::<dyn SurfacePublication>() {
703            request.provide::<dyn SurfacePublication>(self);
704            return;
705        }
706        if request.wants::<dyn ui_core::resources::PublishesResources>() {
707            request.provide::<dyn ui_core::resources::PublishesResources>(self);
708            return;
709        }
710        self.host.surface().provide(request);
711    }
712
713    fn provide_mut<'a>(
714        &'a mut self,
715        request: &mut ui_core::widget::capability::RequestMut<'a, '_>,
716    ) {
717        if request.wants::<dyn SurfacePublication>() {
718            request.provide::<dyn SurfacePublication>(self);
719            return;
720        }
721        if request.wants::<dyn ui_core::resources::PublishesResources>() {
722            request.provide::<dyn ui_core::resources::PublishesResources>(self);
723            return;
724        }
725        self.host.surface_mut().provide_mut(request);
726    }
727
728    type Inner = dyn Widget;
729
730    fn inner(&self) -> &(dyn Widget + 'static) {
731        self.host.surface()
732    }
733
734    fn inner_mut(&mut self) -> &mut (dyn Widget + 'static) {
735        self.host.surface_mut()
736    }
737
738    fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
739        self.host.view_only_scroll(event)
740    }
741
742    // The host adds no structure of its own: a change its surface can
743    // describe stays described.
744    fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
745        self.host.surface_mut().take_structure_change()
746    }
747
748    // The host is invisible to whoever reaches the surface by its type (a
749    // test driving it, a host that embeds it): it answers as the surface.
750    fn as_any(&self) -> Option<&dyn std::any::Any> {
751        Some(self.host.surface_any())
752    }
753
754    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
755        Some(self.host.surface_any_mut())
756    }
757
758    fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ObjId) {
759        self.host.surface_mut().settle(edits, path);
760    }
761
762    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
763        self.host.sync(state, path);
764    }
765
766    fn start_operation(
767        &mut self,
768        op: &ResourceOperation<'_>,
769        doc: &mut ui_core::widget::Edits<'_>,
770        path: &ObjId,
771    ) -> Result<Option<ResourceOperationStart>, String> {
772        if let Some(started) = self.host.start_effect(op, doc.doc())? {
773            return Ok(Some(started));
774        }
775        // The surface answers its operations as its effect vocabulary; only
776        // the toolkit's own widgets inside it (a button bound to a view)
777        // answer theirs, never the surface by hand.
778        let before = doc.pending_ops();
779        let started =
780            ui_core::host_operation::start_in_children(self.host.surface_mut(), op, doc, path);
781        refuse_raw_writes(doc, before, "start_operation");
782        started
783    }
784
785    fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
786        self.host.surface_mut().update(state, path)
787    }
788
789    fn handle_event(
790        &mut self,
791        event: &ui_core::winit::event::WindowEvent,
792        ctx: &mut ui_core::widget::EventCtx<'_>,
793    ) {
794        let before = ctx.state.pending_ops();
795        self.host.surface_mut().handle_event(event, ctx);
796        refuse_raw_writes(ctx.state.doc(), before, "handle_event");
797    }
798
799    fn perform_semantic_action(
800        &mut self,
801        target: &str,
802        action: &ui_core::a11y::Action,
803        state: &mut ui_core::widget::Edits<'_>,
804        path: &ModelPath,
805    ) -> bool {
806        let surface = self.host.surface_mut();
807        let mut performed = false;
808        only_through_actions(state, path, "perform_semantic_action", |state, path| {
809            performed = surface.perform_semantic_action(target, action, state, path);
810        });
811        performed
812    }
813
814    fn poll_resource_operation(
815        &mut self,
816        invocation: tilo_actions::InvocationId,
817        cancellation_requested: bool,
818        doc: &AutoCommit,
819        path: &ObjId,
820    ) -> Option<tilo_actions::InvocationUpdate> {
821        if let Some(update) = self.host.poll_effect(invocation, cancellation_requested) {
822            return Some(update);
823        }
824        self.host
825            .surface_mut()
826            .children_mut()
827            .into_iter()
828            .find_map(|child| {
829                child.poll_resource_operation(invocation, cancellation_requested, doc, path)
830            })
831    }
832}
833
834impl SurfacePublication for HostedSurface {
835    fn resource(&self) -> std::any::TypeId {
836        self.host.resource()
837    }
838    fn bound_declarations(&self, into: &mut dyn std::any::Any) {
839        self.host.bound_declarations(into);
840    }
841    fn availability(&self) -> Vec<EffectStatus> {
842        self.host.availability()
843    }
844    fn live_rows(&self) -> Vec<LiveRow> {
845        self.host.live_rows()
846    }
847    fn live_row(&self, id: &str) -> Option<LiveRow> {
848        self.host.live_row(id)
849    }
850    fn offers_on_row(&self, row: &str) -> bool {
851        self.host.offers_on_row(row)
852    }
853    fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool {
854        self.host.row_declarations(row, into)
855    }
856    fn search_entries(&self) -> Vec<SearchEntry> {
857        self.host.search_entries()
858    }
859    fn supports_device_shell(&self) -> bool {
860        self.host.supports_device_shell()
861    }
862    fn device_shell_operation(
863        &mut self,
864        operation: &ui_core::floating_surface::DeviceShellOperation,
865    ) -> Result<serde_json::Value, String> {
866        self.host.device_shell_operation(operation)
867    }
868    fn request_device_shell(&mut self) -> bool {
869        self.host.request_device_shell()
870    }
871    fn search_queries(&self) -> SearchQueries {
872        self.host.search_queries()
873    }
874    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
875        self.host.take_host_intent()
876    }
877    fn take_save_retry(&mut self) -> bool {
878        self.host.take_save_retry()
879    }
880}
881
882impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfacePublication
883    for SurfaceHost<R, S, E>
884{
885    fn resource(&self) -> std::any::TypeId {
886        std::any::TypeId::of::<R>()
887    }
888
889    fn supports_device_shell(&self) -> bool {
890        self.surface.supports_device_shell()
891    }
892    fn device_shell_operation(
893        &mut self,
894        operation: &ui_core::floating_surface::DeviceShellOperation,
895    ) -> Result<serde_json::Value, String> {
896        self.surface.device_shell_operation(operation)
897    }
898    fn request_device_shell(&mut self) -> bool {
899        self.surface.request_device_shell()
900    }
901
902    fn take_save_retry(&mut self) -> bool {
903        self.effects.is_some() && self.surface.take_save_retry()
904    }
905
906    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
907        self.surface.take_host_intent()
908    }
909
910    fn bound_declarations(&self, into: &mut dyn std::any::Any) {
911        if let Some(into) = into.downcast_mut::<Vec<ActionDecl<R>>>() {
912            into.extend(self.surface.bound_declarations());
913        }
914    }
915
916    fn availability(&self) -> Vec<EffectStatus> {
917        self.surface.availability()
918    }
919
920    fn live_rows(&self) -> Vec<LiveRow> {
921        self.surface.live_rows()
922    }
923
924    fn live_row(&self, id: &str) -> Option<LiveRow> {
925        self.surface.live_row(id)
926    }
927
928    fn offers_on_row(&self, row: &str) -> bool {
929        !self.surface.row_declarations(row).is_empty()
930    }
931
932    fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool {
933        let offered = self.surface.row_declarations(row);
934        match into.downcast_mut::<Vec<ActionDecl<R>>>() {
935            Some(into) if !offered.is_empty() => {
936                into.extend(offered);
937                true
938            }
939            _ => false,
940        }
941    }
942
943    fn search_entries(&self) -> Vec<SearchEntry> {
944        self.surface.search_entries()
945    }
946
947    fn search_queries(&self) -> SearchQueries {
948        self.search_queries.clone()
949    }
950}
951
952// What the surface publishes as a widget passes through here, without
953// document actions: those are the resource's, through its dispatcher.
954impl ui_core::resources::PublishesResources for HostedSurface {
955    fn resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::Resource> {
956        ui_core::resources::resources(self.host.surface(), doc)
957            .into_iter()
958            .map(outside_document)
959            .collect()
960    }
961
962    fn caller_view_resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::Resource> {
963        ui_core::resources::caller_view_resources(self.host.surface(), doc)
964            .into_iter()
965            .map(outside_document)
966            .collect()
967    }
968
969    fn action_directory_resource_page(
970        &self,
971        doc: &AutoCommit,
972        cursor: &[u8],
973        max_results: usize,
974    ) -> Option<ui_core::widget::ActionDirectoryResourcePage> {
975        let mut page = ui_core::resources::action_directory_resource_page(
976            self.host.surface(),
977            doc,
978            cursor,
979            max_results,
980        )?;
981        page.resources = page.resources.into_iter().map(outside_document).collect();
982        Some(page)
983    }
984
985    fn action_directory_revision(&self, doc: &AutoCommit) -> Option<String> {
986        ui_core::resources::action_directory_revision(self.host.surface(), doc)
987    }
988
989    fn action_directory_invocation_domain(
990        &self,
991        doc: &AutoCommit,
992    ) -> ui_core::widget::ActionDirectoryInvocationDomain {
993        ui_core::resources::action_directory_invocation_domain(self.host.surface(), doc)
994    }
995
996    fn resource(&self, doc: &AutoCommit, id: &str) -> Option<tilo_actions::Resource> {
997        ui_core::resources::resource(self.host.surface(), doc, id).map(resolved_outside_document)
998    }
999
1000    fn resources_for(
1001        &self,
1002        participant: Option<&tilo_actions::ParticipantId>,
1003        doc: &AutoCommit,
1004    ) -> Vec<tilo_actions::Resource> {
1005        ui_core::resources::resources_for(self.host.surface(), participant, doc)
1006            .into_iter()
1007            .map(outside_document)
1008            .collect()
1009    }
1010
1011    fn resource_for(
1012        &self,
1013        participant: Option<&tilo_actions::ParticipantId>,
1014        doc: &AutoCommit,
1015        id: &str,
1016    ) -> Option<tilo_actions::Resource> {
1017        ui_core::resources::resource_for(self.host.surface(), participant, doc, id)
1018            .map(resolved_outside_document)
1019    }
1020
1021    fn action_directory_visibility(
1022        &self,
1023        doc: &AutoCommit,
1024        resource_id: &str,
1025    ) -> Option<tilo_actions::ActionAddress> {
1026        ui_core::resources::action_directory_visibility(self.host.surface(), doc, resource_id)
1027    }
1028
1029    fn document_action_result(
1030        &self,
1031        doc: &AutoCommit,
1032        address: &tilo_actions::ActionAddress,
1033        changes: &[ui_core::automerge::Patch],
1034    ) -> Option<serde_json::Value> {
1035        ui_core::resources::document_action_result(self.host.surface(), doc, address, changes)
1036    }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042    use ui_core::automerge::ROOT;
1043    use ui_core::automerge::transaction::Transactable;
1044
1045    #[test]
1046    fn what_a_surface_applied_through_actions_stays() {
1047        let mut doc = AutoCommit::new();
1048        doc.put(ROOT, "title", "applied").unwrap();
1049        // The executor commits every action it applies.
1050        doc.commit();
1051        let heads = doc.get_heads();
1052        refuse_raw_writes(&doc, 0, "sync");
1053        assert_eq!(doc.get_heads(), heads);
1054    }
1055
1056    #[test]
1057    #[should_panic(expected = "outside an action")]
1058    fn a_surface_writing_around_its_dispatcher_is_stopped() {
1059        let mut doc = AutoCommit::new();
1060        doc.put(ROOT, "title", "raw").unwrap();
1061        refuse_raw_writes(&doc, 0, "sync");
1062    }
1063}