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