Skip to main content

tilo_app/framework/
root.rs

1//! The root widget of a framework app: keeps the resource state current,
2//! publishes the resource and its actions, forwards edits to the host, and
3//! indexes the resource for search.
4
5use std::collections::HashSet;
6use std::rc::Rc;
7use std::sync::Arc;
8use tilo_actions::ResourceSpec;
9use ui_core::widget::{AppModel, ModelPath};
10
11use crate::runner::with_native_presentations;
12use tilo_resource::{Resource, SearchEntry};
13use ui_core::PresencePayload;
14use ui_core::application_view::{
15    ApplicationFocusRestore, ApplicationLocalView, ApplicationViewState,
16};
17use ui_core::automerge::AutoCommit;
18use ui_core::permissions::{LivePermissions, PermissionScope};
19use ui_core::presence::{PresenceCtx, PresenceKey};
20use ui_core::record::RecordSaveStatus;
21use ui_core::search::SearchProvider;
22use ui_core::text_render::TextRenderer;
23use ui_core::vello::Scene;
24use ui_core::vello::kurbo::Affine;
25use ui_core::view::ThawError;
26use ui_core::widget::capability::{capability, capability_mut};
27use ui_core::widget::{AppKind, AppWidget, Decorator, Element, IconDescriptor, Widget};
28use ui_core::{LocalState, Theme};
29
30use super::dispatch::DispatchCore;
31use super::rows::{find, join, merge, outside_document};
32use super::scenario::Run;
33use super::stage::Stages;
34use super::surface::SurfacePublication;
35use super::view::ViewCx;
36
37mod controls;
38mod facts;
39mod live_head;
40mod read;
41mod search;
42mod shared_tree;
43mod shown_item;
44mod typed;
45mod widget;
46use live_head::LiveHead;
47pub(crate) use read::READ_TYPE;
48use read::{offer_document_reads, offer_item_reads, read, read_spec};
49pub(crate) use search::{
50    BodyText, IndexedEntries, LiveSearch, ResourceSearch, field_weight, index_entry,
51};
52use shared_tree::{
53    DocumentChildren, ItemChildren, NestedViews, PublishedChildren, RecordChildren, RowChildren,
54    SharedTreeCache, mark_document_jobs,
55};
56use typed::TypedRoot;
57pub(crate) use typed::{ResourceType, Typed};
58pub(crate) use widget::{RootApp, RootModel, RootPublishes, RootSearch, RootWidget};
59
60pub(crate) struct ResourceRoot {
61    title: &'static str,
62    icon: IconDescriptor,
63    label: String,
64    /// The resource type the root shows, and what it says of itself.
65    resource: ResourceType,
66    /// What depends on the resource type: its live state, the views'
67    /// updaters and bound edits, the dispatcher and the effects. Everything
68    /// else here is compiled once in the shared runtime.
69    typed: Box<dyn TypedRoot>,
70    /// What bound controls look up ([`super::controls::Actions`]).
71    action_bindings: super::controls::ActionTable,
72    /// The live revision bound controls last followed.
73    followed: std::cell::Cell<u64>,
74    _alive: Rc<()>,
75    pub(crate) dispatch: DispatchCore,
76    saved: LocalState<RecordSaveStatus>,
77    /// The window's save status over the view: the record's durability and
78    /// the app's own saves, silent unless a save stalls or fails.
79    save: super::saves::SaveIndicator,
80    /// The reactions to the record, in the window's bar, where the record
81    /// takes them.
82    reactions: super::reactions::ReactionsIndicator,
83    /// The participant's permissions, re-checked every sync so a role change
84    /// while the app runs takes effect at once. `None` for a read-only view
85    /// of a past version.
86    permissions: Option<LivePermissions>,
87    write: Option<PermissionScope>,
88    stages: Stages,
89    feeds: super::history::HistoryFeeds,
90    /// What the view was built for, and the fixtures a capture's views read.
91    pub(crate) run: Run,
92    /// The settings the view follows, re-read when their document changes.
93    pub(crate) storage: Rc<super::settings::ViewStorage>,
94    /// The app's view: the custom surface itself when the view is one.
95    pub(crate) content: Element,
96    /// What is shown instead of the view while the record does not read as
97    /// the resource, and why.
98    unreadable: Option<(String, Element)>,
99    /// The view was swapped for the unreadable state or back since layout.
100    swapped: bool,
101    /// What keeps the bindings of the app's view alive once it stands in
102    /// the place of the window's arrival ([`Self::settle_arrival`]).
103    arrived: Option<Rc<()>>,
104    /// View and effect operations of the app's presentation, attached to the
105    /// published resource. Document specs are dropped.
106    presentation: Vec<tilo_actions::ActionSpec>,
107    /// The resource's and its surfaces' search entries, indexed.
108    search: Rc<std::cell::RefCell<SearchCache>>,
109    /// The document-owned tree at one live and translation revision, shared
110    /// with the last one wherever the change left it as it was. Surface
111    /// bindings, local rows and effect availability are decorated afresh
112    /// for every caller.
113    publication_tree: std::cell::RefCell<Option<SharedTreeCache>>,
114    /// The item children last published, kept so the next tree builds only
115    /// the items whose label changed.
116    item_children: std::cell::RefCell<Option<ItemChildren>>,
117    /// The document children as the shared publication last carried them.
118    published_children: std::cell::RefCell<Option<PublishedChildren>>,
119    /// The children with the records kept beside the resource, as the shared
120    /// publication last carried them.
121    record_children: std::cell::RefCell<Option<RecordChildren>>,
122    /// The children with the surfaces' live rows merged in, as the shared
123    /// publication last carried them.
124    row_children: std::cell::RefCell<Option<RowChildren>>,
125    /// The head bound controls were last refreshed from, kept between syncs
126    /// for a resource whose publication may be reused
127    /// ([`Resource::publication_cache_epoch`]).
128    binding_head: std::cell::RefCell<Option<BindingHead>>,
129    /// What bound controls share of the publication at one head: built
130    /// once per head, never copied per refresh.
131    binding_base: std::cell::RefCell<Option<Rc<BindingBase>>>,
132    /// The head of the live state and what it was published from.
133    live_head: std::cell::RefCell<Option<LiveHead>>,
134    /// The document children of the live state as last published, kept
135    /// while they are unchanged.
136    document_children: std::cell::RefCell<Option<DocumentChildren>>,
137    /// The view operation types of the nested resources at one live revision.
138    nested_views: std::cell::RefCell<Option<NestedViews>>,
139    /// Whether the resource keeps document state History shows.
140    history: bool,
141    /// Whether the window stands on the framework's ground: every window but
142    /// one shown over other apps' content — a sheet over the desktop
143    /// ([`WindowRole::Sheet`]), or the on-screen keyboard over the app it
144    /// types into ([`WindowRole::InputSurface`]).
145    ///
146    /// [`WindowRole::Sheet`]: ui_core::application::WindowRole::Sheet
147    /// [`WindowRole::InputSurface`]: ui_core::application::WindowRole::InputSurface
148    ground: bool,
149    /// Platform presentation actions; the browser implementation has no state.
150    native_presentation: crate::runner::Presentation,
151    /// The links `share-link` is minting.
152    share: super::share_link::ShareLinks,
153    /// Where the resource's state comes from and where its edits go: the
154    /// document the host lends each pass, or the session over the app's one
155    /// connection ([`super::Source`]).
156    source: super::Source,
157}
158
159impl ResourceRoot {
160    /// Adopt the session's newest reading of the resource, when it moved:
161    /// what the view binds follows it as any change does.
162    fn sync_remote_state(&self) {
163        let (Some(resource), Some(connection)) =
164            (self.source.remote_resource(), self.source.connection())
165        else {
166            return;
167        };
168        let Some(reading) = connection.reading(resource) else {
169            // Nothing publishes it yet, or the person may not read it:
170            // denial is absence (`apps.md` §6), and the last state stands.
171            return;
172        };
173        if let Err(error) = self.typed.adopt(&reading) {
174            tracing::warn!(%error, kind = self.resource.kind, "the session's reading is not this resource's state");
175        }
176    }
177
178    fn sync_over(&mut self, state: &AutoCommit, path: &automerge::ObjId) {
179        self.refresh_access();
180        if self.dispatch.take_represent() {
181            self.typed.represent();
182        }
183        let head = self.refresh(state);
184        for (action, reason) in self.dispatch.opener.take_refusals() {
185            self.dispatch.refuse(&action, reason);
186        }
187        self.follow_readability();
188        self.storage.refresh();
189        self.typed
190            .poll_window_jobs(effect_view(&mut self.unreadable, &mut self.content));
191        {
192            let _profile = tilo_resource::profiling::Phase::start("framework.content_sync");
193            self.content.sync(state, path);
194        }
195        self.reveal_launched();
196        self.settle_arrival();
197        self.follow_search();
198        // What the view's sync changed (its bound edits, its availability)
199        // reaches the bindings; the document-owned head is rebuilt only when
200        // the sync moved the state.
201        let head = self.refresh_bindings(Some(head));
202        *self.binding_head.borrow_mut() = Some(head);
203        // The save status says itself in the window's bar.
204        self.save.sync(ui_core::time::now());
205        self.sync_reactions();
206    }
207}
208
209impl ui_core::component_lifecycle::WindowActivation for ResourceRoot {
210    /// Asked through the root's own door, or by the app's view: a view whose app runs
211    /// elsewhere (a tilolang host's instance) hears the request there.
212    fn take_window_activation_intent(&self) -> bool {
213        let own = self.native_presentation.take_window_activation_intent();
214        let view = app_view_in(self.content.as_ref())
215            .is_some_and(ui_core::component_lifecycle::take_window_activation_intent);
216        own || view
217    }
218}
219
220/// The operation type of the framework's `open` view: show the item at an
221/// anchor, as picking a search hit does.
222pub(crate) const OPEN_TYPE: &str = tilo_actions::remote::APPLICATION_OPEN_TYPE;
223
224/// The parameter of `open` naming what to show.
225const OPEN_ANCHOR: &str = "anchor";
226
227/// `open`: show the item at an anchor (a search entry's anchor). Published
228/// when a view shows items ([`ViewCx::on_open`](super::ViewCx::on_open)), so
229/// an agent opens the app where a person's search hit would.
230fn open_spec() -> tilo_actions::ActionSpec {
231    tilo_actions::ActionSpec::typed_operation(
232        "open",
233        "Open",
234        "Item to open",
235        tilo_actions::OperationKind::View,
236        OPEN_TYPE,
237        vec![tilo_actions::ActionParam::required(
238            OPEN_ANCHOR,
239            tilo_actions::ParamType::Text,
240            "Item to open: the anchor of a search hit",
241        )],
242        |args| {
243            tilo_actions::EncodedAction::encode_operation(
244                OPEN_TYPE,
245                &serde_json::json!({ OPEN_ANCHOR: args.text(OPEN_ANCHOR) }),
246            )
247            .unwrap_or_else(|_| tilo_actions::EncodedAction {
248                type_id: OPEN_TYPE.into(),
249                payload: Vec::new(),
250            })
251        },
252    )
253    .of_kind(tilo_actions::ActionKind::Read)
254    .with_description("Open the app at one item: the anchor of a search hit.")
255}
256
257/// The label of the item view the framework gives an item resource that
258/// presents no view of its own.
259const ITEM_VIEW_LABEL: &str = "Item";
260
261/// `open` on one item resource: show that item, as its search hit would.
262fn item_open_spec(anchor: &str) -> tilo_actions::ActionSpec {
263    let encoded = tilo_actions::EncodedAction::encode_operation(
264        OPEN_TYPE,
265        &serde_json::json!({ OPEN_ANCHOR: anchor }),
266    )
267    .unwrap_or_else(|_| tilo_actions::EncodedAction {
268        type_id: OPEN_TYPE.into(),
269        payload: Vec::new(),
270    });
271    tilo_actions::ActionSpec::typed_operation(
272        "open",
273        "Open",
274        "Open",
275        tilo_actions::OperationKind::View,
276        OPEN_TYPE,
277        Vec::new(),
278        move |_| encoded.clone(),
279    )
280    .of_kind(tilo_actions::ActionKind::Read)
281    .with_description("Show this item.")
282}
283
284/// Offer `open` on every item resource under `resource` (an item of the
285/// collections of the resource `item_of` knows the items of), bound to that
286/// item.
287fn offer_item_open(item_of: fn(&str) -> bool, resource: &mut ResourceSpec) {
288    if item_of(resource.id.as_str())
289        && !resource
290            .actions
291            .iter()
292            .any(|existing| existing.id == "open")
293    {
294        resource.actions.push(item_open_spec(resource.id.as_str()));
295        // The opener is also a presentation binding, not just a verb in the
296        // catalogue. Keep an app's explicit views; otherwise advertise the
297        // item view that this exact bound opener actually shows. The view
298        // names what it shows, the item, and not which item: the item's own
299        // label is the resource's, published once with it. Every item's
300        // `open` is then one catalogue entry however many items there are,
301        // where a copy of the item's label made each item's `open`, and its
302        // whole action template, an entry of its own.
303        if resource.views.is_empty() {
304            resource.views.push(
305                tilo_actions::ResourceViewSpec::new("item", ITEM_VIEW_LABEL, "open")
306                    .with_primary(true),
307            );
308        }
309    }
310    for child in &mut resource.children {
311        offer_item_open(item_of, child);
312    }
313}
314
315/// The ids of the actions the framework publishes on every app's resource
316/// beside the app's own: the generic `apply`, `group` and `field-edit`, the
317/// views `open`, `read` and `facts`, history, and the presentation operations. An app
318/// verb, effect or view under one of these ids shadows the framework's or is
319/// shadowed by it, so `tilo-app check` refuses it.
320#[must_use]
321pub fn reserved_action_ids() -> Vec<String> {
322    let mut ids: Vec<String> = [
323        tilo_resource::APPLY,
324        tilo_resource::GROUP,
325        tilo_resource::FIELD_EDIT,
326    ]
327    .into_iter()
328    .map(str::to_owned)
329    .collect();
330    ids.push(open_spec().id.clone());
331    ids.push(read_spec().id.clone());
332    ids.push(facts::FACTS.to_owned());
333    ids.push(tilo_resource::share::SHARE_LINK.to_owned());
334    ids.push(tilo_resource::share::SHARE_INVITE_LINK.to_owned());
335    ids.extend(super::history::specs().iter().map(|spec| spec.id.clone()));
336    ids.extend(
337        with_native_presentations(ResourceSpec::new("", "", ""))
338            .actions
339            .iter()
340            .map(|spec| spec.id.clone()),
341    );
342    ids.sort();
343    ids.dedup();
344    ids
345}
346
347/// The window's arrival: the app's view, shown once the workspace's holdings
348/// arrived ([`super::app`]'s `build_root`).
349type WorkspaceArrival = ui_core::widget::ArrivingView;
350
351/// The app's view in `content`, once it shows: `content` itself, or the
352/// view standing in the window's arrival once that arrived. The arrival
353/// answers for nothing of the view (a traversal meets the view as its
354/// descendant, once), so what the root asks of the app's view it asks here.
355fn app_view_in(content: &dyn Widget) -> Option<&dyn Widget> {
356    let Some(arrival) = content
357        .as_any()
358        .and_then(|any| any.downcast_ref::<WorkspaceArrival>())
359    else {
360        return Some(content);
361    };
362    let switch = arrival
363        .answer()?
364        .as_any()?
365        .downcast_ref::<super::switch::Switch>()?;
366    Some(switch.part())
367}
368
369/// `visit` the app's view in `content` ([`app_view_in`]); `content` itself
370/// while the view is on its way, where nothing of it answers.
371fn with_app_view_mut<R>(content: &mut Element, visit: impl FnOnce(&mut dyn Widget) -> R) -> R {
372    let arrived = content
373        .as_any_mut()
374        .and_then(|any| any.downcast_mut::<WorkspaceArrival>())
375        .and_then(WorkspaceArrival::answer_mut)
376        .and_then(|answer| answer.as_any_mut()?.downcast_mut::<super::switch::Switch>());
377    match arrived {
378        Some(switch) => visit(switch.part_mut()),
379        None => visit(content.as_mut()),
380    }
381}
382
383/// The view effects answer on: the app's `content`, or what stands in for
384/// it while the resource is `unreadable`.
385fn effect_view<'a>(
386    unreadable: &'a mut Option<(String, Element)>,
387    content: &'a mut Element,
388) -> &'a mut dyn Widget {
389    match unreadable {
390        Some((_, shown)) => shown.as_mut(),
391        None => content.as_mut(),
392    }
393}
394
395impl ResourceRoot {
396    /// Once the window's arrival settled (the app's view shows, its
397    /// cross-fade over), the view stands in the arrival's place: the root
398    /// is the app's view plus what the framework adds again, and nothing of
399    /// the arrival stays between them.
400    fn settle_arrival(&mut self) {
401        let Some(arrival) = self
402            .content
403            .as_any_mut()
404            .and_then(|any| any.downcast_mut::<WorkspaceArrival>())
405        else {
406            return;
407        };
408        let Some(mut answer) = arrival.take_answer(ui_core::motion::frame_now()) else {
409            return;
410        };
411        let empty = || -> Element { Box::new(ui_core::widget::Spacer::fixed(0.0)) };
412        self.content = match answer
413            .as_any_mut()
414            .and_then(|any| any.downcast_mut::<super::switch::Switch>())
415        {
416            Some(switch) => {
417                let (view, alive) = switch.take_part(empty());
418                self.arrived = Some(alive);
419                view
420            }
421            None => answer,
422        };
423        self.swapped = true;
424    }
425
426    /// The app's view: what the root asks the view for ([`app_view_in`]).
427    pub(crate) fn app_view(&self) -> &dyn Widget {
428        app_view_in(self.content.as_ref()).unwrap_or_else(|| self.content.as_ref())
429    }
430
431    /// The window ground is the framework's: an app never fills it, and one
432    /// that paints nothing still stands on it. A sheet over the desktop
433    /// stands on nothing ([`Self::without_ground`]).
434    fn paint_ground(
435        &mut self,
436        scene: &mut Scene,
437        transform: Affine,
438        theme: &Theme,
439        presence: &PresenceCtx,
440    ) {
441        self.native_presentation.track_theme(theme);
442        if self.ground {
443            scene.fill(
444                ui_core::vello::peniko::Fill::NonZero,
445                transform,
446                theme.tokens.colors.surfaces.content,
447                None,
448                &self.content.get_rect(),
449            );
450        }
451        self.run.people.follow(presence);
452    }
453
454    /// A root over `content`, the view built in `cx`, whose handles the root
455    /// keeps; `alive` is the scope `cx`'s bindings are tied to. Only this
456    /// constructor knows `R`.
457    pub(crate) fn new<R: Resource>(
458        title: &'static str,
459        icon: IconDescriptor,
460        label: String,
461        cx: ViewCx<R>,
462        alive: Rc<()>,
463        content: Element,
464    ) -> Self {
465        let native_presentation = crate::runner::Presentation::new(&cx);
466        cx.dispatch.name_resource(&label);
467        let ViewCx {
468            env,
469            updaters,
470            action_bindings,
471            stages,
472            feeds,
473            bindings,
474            ..
475        } = cx;
476        let dispatch = env.dispatch.core().clone();
477        let typed = Typed {
478            live: env.live,
479            reading: std::cell::RefCell::new(None),
480            updaters,
481            dispatch: env.dispatch,
482            bindings,
483            effects: env.effects,
484        };
485        Self::assemble(
486            (title, icon, label),
487            ResourceType::of::<R>(),
488            Box::new(typed),
489            action_bindings.table(),
490            Parts {
491                alive,
492                dispatch,
493                saved: env.saved,
494                saves: env.saves,
495                stages,
496                feeds,
497                run: env.run,
498                storage: env.storage,
499                content,
500                native_presentation,
501            },
502        )
503    }
504
505    /// [`Self::new`] past what names the resource type.
506    fn assemble(
507        (title, icon, label): (&'static str, IconDescriptor, String),
508        resource: ResourceType,
509        typed: Box<dyn TypedRoot>,
510        action_bindings: super::controls::ActionTable,
511        parts: Parts,
512    ) -> Self {
513        let Parts {
514            alive,
515            dispatch,
516            saved,
517            saves,
518            stages,
519            feeds,
520            run,
521            storage,
522            content,
523            native_presentation,
524        } = parts;
525        Self {
526            title,
527            icon,
528            label,
529            resource,
530            typed,
531            action_bindings,
532            followed: std::cell::Cell::new(0),
533            _alive: alive,
534            dispatch,
535            saved,
536            save: super::saves::SaveIndicator::new(saves, title),
537            reactions: super::reactions::ReactionsIndicator::default(),
538            permissions: None,
539            write: None,
540            stages,
541            feeds,
542            run,
543            storage,
544            content,
545            unreadable: None,
546            swapped: false,
547            arrived: None,
548            presentation: Vec::new(),
549            search: Rc::default(),
550            publication_tree: std::cell::RefCell::new(None),
551            item_children: std::cell::RefCell::new(None),
552            published_children: std::cell::RefCell::new(None),
553            record_children: std::cell::RefCell::new(None),
554            row_children: std::cell::RefCell::new(None),
555            binding_head: std::cell::RefCell::new(None),
556            binding_base: std::cell::RefCell::new(None),
557            live_head: std::cell::RefCell::new(None),
558            document_children: std::cell::RefCell::new(None),
559            nested_views: std::cell::RefCell::new(None),
560            history: true,
561            ground: true,
562            native_presentation,
563            share: super::share_link::ShareLinks::default(),
564            source: super::Source::Document,
565        }
566    }
567
568    /// Take the source the dispatcher submits over as the root's too, so the
569    /// view syncs from the reading the session publishes when the record is
570    /// the session's ([`super::Source::Connection`]).
571    pub(crate) fn reading_from_source(mut self) -> Self {
572        self.source = self.dispatch.source().clone();
573        self
574    }
575
576    /// A root whose resource keeps no document state: it publishes no
577    /// History actions and its window offers no History.
578    pub(crate) fn without_history(mut self) -> Self {
579        self.history = false;
580        self
581    }
582
583    /// Stand on nothing: what the view does not paint shows what is under
584    /// the window, in the live window and in the picture a host draws from
585    /// it alike.
586    pub(crate) fn without_ground(mut self) -> Self {
587        self.ground = false;
588        self.native_presentation.without_ground();
589        self
590    }
591
592    #[cfg(not(target_family = "wasm"))]
593    pub(crate) fn drawing_past(mut self, past: crate::runner::DrawPast) -> Self {
594        self.native_presentation.drawing_past(past);
595        self
596    }
597
598    pub(crate) fn with_presentation(mut self, operations: Vec<tilo_actions::ActionSpec>) -> Self {
599        self.presentation = operations;
600        self
601    }
602
603    /// Stage a screenshot scenario's view state: every view's
604    /// [`on_stage`](super::ViewCx::on_stage) callback runs now, and every
605    /// hosted surface stages on its next sync.
606    /// Stage the views' state for `scenario`, and answer the collaborators
607    /// the capture shows as the presence frames their windows would send.
608    pub(crate) fn stage(
609        &self,
610        scenario: Option<&str>,
611    ) -> Vec<(String, PresenceKey, PresencePayload)> {
612        self.stages.run(scenario);
613        self.run
614            .collaborators()
615            .iter()
616            .map(|(name, at)| {
617                let (key, payload) = at.presence_of(self.resource.kind, self.dispatch.app_id());
618                (name.clone(), key, payload)
619            })
620            .collect()
621    }
622
623    /// Settle the window's save status for a capture
624    /// ([`SaveIndicator::settle_for_capture`](super::saves::SaveIndicator::settle_for_capture)).
625    pub(crate) fn settle_saves_for_capture(&mut self) {
626        self.save.settle_for_capture(ui_core::time::now());
627    }
628
629    /// Follow `permissions`: the resource is editable while they grant
630    /// `write`.
631    pub(crate) fn following(
632        mut self,
633        permissions: Option<LivePermissions>,
634        write: PermissionScope,
635    ) -> Self {
636        if let Some(permissions) = &permissions {
637            self.dispatch.follow(permissions.clone());
638        }
639        self.permissions = permissions;
640        self.write = Some(write);
641        self
642    }
643
644    /// Show the record's reactions in the window's bar, and run those
645    /// pressed there ([`super::reactions`]). Nothing while the record does
646    /// not read as the resource: its last state is not what it holds.
647    fn sync_reactions(&mut self) {
648        let state = self.typed.state();
649        let readable = self.unreadable.is_none();
650        self.reactions.sync(&super::reactions::ReactionsInput {
651            table: self.resource.record_table.filter(|_| readable),
652            state: state.as_ref(),
653            dispatch: &self.dispatch,
654            people: &self.run.people,
655            private: self.run.private,
656        });
657    }
658
659    /// Show the item the launch asked for once a view that opens items is
660    /// up. An item the record does not hold yet (it is still arriving) is
661    /// asked for again at the next sync.
662    fn reveal_launched(&self) {
663        let mut reveal = self.run.launched.reveal.borrow_mut();
664        if let Some(anchor) = reveal.as_deref()
665            && self.stages.opens()
666            && self.stages.open(anchor).is_ok()
667        {
668            *reveal = None;
669        }
670    }
671
672    /// Apply a permission change made while the app runs: dispatchers stop
673    /// (or resume) submitting, bound state and surfaces see the new value.
674    fn refresh_access(&self) {
675        let _profile = tilo_resource::profiling::Phase::start("framework.access_refresh");
676        if let (Some(permissions), Some(write)) = (&self.permissions, &self.write) {
677            let read_only = !permissions.check(write) && !self.may_change_shown_item(permissions);
678            self.dispatch.set_read_only(read_only);
679            // Its person sees, in the window's bar, that they may only look.
680            ui_core::window_toolbar::show_view_only(read_only);
681        }
682        self.dispatch.refresh_permission_states();
683    }
684
685    /// The root as the widget a host mounts.
686    pub(crate) fn into_widget(self) -> Box<dyn AppWidget> {
687        RootWidget::boxed(Box::new(self))
688    }
689
690    /// The root as the widget a past version's preview draws. Boxed here,
691    /// not where the resource's type is known, so its widget table is
692    /// compiled once for every app.
693    pub(crate) fn into_root_widget(self) -> Box<dyn Widget> {
694        Box::new(RootWidget::new(Box::new(self)))
695    }
696
697    /// The handles of the root that name its resource type, when it is `R`.
698    pub(crate) fn typed<R: Resource>(&self) -> Option<&Typed<R>> {
699        self.typed.as_any().downcast_ref::<Typed<R>>()
700    }
701
702    /// What the launch that opened the window named.
703    pub(crate) fn launch(&self) -> super::launch::LaunchInput<'_> {
704        self.run.launch()
705    }
706
707    /// Every hosted surface of this resource in the view, in tree order.
708    fn surfaces(&self, visit: &mut dyn FnMut(&dyn SurfacePublication)) {
709        fn walk(
710            widget: &dyn Widget,
711            resource: std::any::TypeId,
712            visit: &mut dyn FnMut(&dyn SurfacePublication),
713        ) {
714            if let Some(surface) =
715                ui_core::widget::capability::capability::<dyn SurfacePublication>(widget)
716                && surface.resource() == resource
717            {
718                visit(surface);
719            }
720            widget.for_each_child(&mut |child| walk(child, resource, visit));
721        }
722        walk(self.content.as_ref(), self.resource.id, visit);
723    }
724
725    /// Bind the edits views offer on the rows `rows` name
726    /// ([`Publishes::row_edits`](super::Publishes::row_edits)).
727    ///
728    /// One walk of the view asks every surface about every row, so binding
729    /// costs the view plus surfaces × rows, never the view once per row. Each
730    /// row binds its edits in tree order of the surfaces offering them.
731    #[inline(never)]
732    fn bind_row_declarations(&self, rows: &mut [ResourceSpec]) {
733        // Every row, nested ones too (a playlist's tracks), asked of the
734        // surfaces in one walk.
735        fn ids(rows: &[ResourceSpec], out: &mut Vec<String>) {
736            for row in rows {
737                out.push(row.id.as_str().to_owned());
738                ids(&row.children, out);
739            }
740        }
741        fn bind(
742            typed: &dyn TypedRoot,
743            rows: &mut [ResourceSpec],
744            offered: &mut std::collections::HashMap<String, Box<dyn std::any::Any>>,
745        ) {
746            for row in rows {
747                if let Some(decls) = offered.remove(row.id.as_str()) {
748                    let bound = std::mem::replace(row, ResourceSpec::new("", "", ""));
749                    *row = typed.bind(bound, decls);
750                }
751                bind(typed, &mut row.children, offered);
752            }
753        }
754        if rows.is_empty() {
755            return;
756        }
757        let mut all = Vec::new();
758        ids(rows, &mut all);
759        let mut offered: std::collections::HashMap<String, Box<dyn std::any::Any>> =
760            std::collections::HashMap::new();
761        let mut spare: Option<Box<dyn std::any::Any>> = None;
762        self.surfaces(&mut |surface| {
763            for id in &all {
764                if let Some(decls) = offered.get_mut(id.as_str()) {
765                    surface.row_declarations(id, decls.as_mut());
766                    continue;
767                }
768                let decls = spare.get_or_insert_with(|| self.typed.no_declarations());
769                if surface.row_declarations(id, decls.as_mut())
770                    && let Some(decls) = spare.take()
771                {
772                    offered.insert(id.clone(), decls);
773                }
774            }
775        });
776        bind(self.typed.as_ref(), rows, &mut offered);
777    }
778
779    /// The resource as published: its own actions and children, the edits
780    /// views bound to where the user is, the live rows views and surfaces
781    /// show, and the framework's operations every app answers.
782    #[inline(never)]
783    fn publication(&self) -> ResourceSpec {
784        let _profile = tilo_resource::profiling::Phase::start("framework.publication");
785        let state = self.typed.state();
786        let mut tree = self.shared_document_tree(&*state).to_resource();
787        if let Some(reason) = self.typed.unreadable() {
788            // Read out of the last state that did read: the record publishes
789            // why its document actions do not apply, never that state as the
790            // one it holds.
791            tilo_resource::withhold_document_actions(&mut tree, &reason);
792        }
793        self.decorate_publication(&*state, tree, Decoration::Tree)
794    }
795
796    /// What the live state holds: its revision and how much of the document
797    /// it read ([`super::Live::loads`]).
798    fn version(&self) -> (u64, u64) {
799        (self.typed.revision(), self.typed.loads())
800    }
801
802    /// The document-owned part of what controls bind to: the live
803    /// resource's identity and own actions, without its children, published
804    /// without building a child, so it costs the same however many items
805    /// the resource holds.
806    #[inline(never)]
807    fn binding_head(&self) -> BindingHead {
808        let state = self.typed.state();
809        let version = (self.version(), ui_core::i18n::I18nManager::revision());
810        let epoch = self.typed.cache_epoch(&*state);
811        let resource = if let Some(reason) = &self.typed.unreadable() {
812            // The state is the last that read, not what the document holds,
813            // so even the fields bound controls were bound to are no longer
814            // offered: every document action says why it does not apply.
815            let mut head = self
816                .typed
817                .head(self.dispatch.resource_id(), &self.label, &*state);
818            tilo_resource::withhold_document_actions(&mut head, reason);
819            head.actions
820                .retain(|spec| spec.id != tilo_resource::FIELD_EDIT);
821            Rc::new(head)
822        } else if let Some(epoch) = &epoch {
823            self.live_head(&*state, epoch)
824        } else {
825            let _profile = tilo_resource::profiling::Phase::start("framework.resource_head");
826            Rc::new(
827                self.typed
828                    .head(self.dispatch.resource_id(), &self.label, &*state),
829            )
830        };
831        BindingHead {
832            version,
833            epoch,
834            resource,
835        }
836    }
837
838    /// The head bound controls were last refreshed from, while it still
839    /// holds: the resource may reuse its publication
840    /// ([`Resource::publication_cache_epoch`]) and nothing it depends on
841    /// moved since. A resource that may not is published afresh each sync.
842    fn retained_head(&self) -> Option<BindingHead> {
843        let head = self.binding_head.borrow_mut().take()?;
844        let current = self.typed.cache_epoch(&*self.typed.state());
845        (head.epoch.is_some() && head.epoch == current).then_some(head)
846    }
847
848    /// What bound controls look up ([`super::controls::Actions`]), as one
849    /// resource: the published resource's identity and actions over `head`,
850    /// exactly as [`Self::publication`] publishes them, without building or
851    /// decorating its children.
852    #[cfg(test)]
853    fn binding_publication(&self, head: &BindingHead) -> ResourceSpec {
854        let base = self.binding_base(head);
855        let overlay = self.binding_overlay(&base);
856        let mut resource = (*base.resource).clone();
857        for spec in overlay {
858            match resource.actions.iter_mut().find(|own| own.id == spec.id) {
859                Some(own) => *own = spec,
860                None => resource.actions.push(spec),
861            }
862        }
863        resource
864    }
865
866    /// The part of what bound controls look up that only `head` and the
867    /// framework decide: the head with the presentation's, the framework's
868    /// and History's actions, as [`Self::decorate_publication`] adds them.
869    /// Built once per head and kept, so a refresh shares it.
870    fn binding_base(&self, head: &BindingHead) -> Rc<BindingBase> {
871        let key = BaseKey {
872            head: Rc::clone(&head.resource),
873            locale: head.version.1,
874            epoch: head.epoch.clone(),
875            causal: self.typed.has_causal_drains(),
876            unreadable: self.typed.unreadable(),
877        };
878        if let Some(base) = self
879            .binding_base
880            .borrow()
881            .as_ref()
882            .filter(|base| base.key == key)
883        {
884            return Rc::clone(base);
885        }
886        let mut resource = (*head.resource).clone();
887        let head_len = resource.actions.len();
888        resource = self.with_presentation_specs(resource);
889        let shadow_len = resource.actions.len();
890        resource = self.with_framework_specs(resource);
891        if key.causal {
892            mark_document_jobs(&mut resource);
893        }
894        if let Some(unreadable) = &key.unreadable {
895            tilo_resource::withhold_document_actions(&mut resource, unreadable);
896        }
897        let base = Rc::new(BindingBase {
898            key,
899            resource: Rc::new(resource),
900            head_len,
901            shadow_len,
902        });
903        *self.binding_base.borrow_mut() = Some(Rc::clone(&base));
904        base
905    }
906
907    /// What the views add over `base` now, as
908    /// [`Self::decorate_publication`] adds it: the edits they bound, their
909    /// availability, and the app's effects. Few actions, built per refresh.
910    fn binding_overlay(&self, base: &BindingBase) -> Vec<tilo_actions::ActionSpec> {
911        let state = self.typed.state();
912        let bound = {
913            let _profile = tilo_resource::profiling::Phase::start("framework.bound_declarations");
914            self.typed.declarations(&*state)
915        };
916        let mut availability = Vec::new();
917        self.surfaces(&mut |surface| {
918            let _profile = tilo_resource::profiling::Phase::start("framework.surface_publication");
919            availability.extend(surface.availability());
920        });
921        let own = base
922            .resource
923            .actions
924            .get(..base.head_len)
925            .unwrap_or_default();
926        let shadows = base
927            .resource
928            .actions
929            .get(..base.shadow_len)
930            .unwrap_or_default();
931        // The head's actions the edits bind over, refined where they say so.
932        let ids = self.typed.declaration_ids(&*bound);
933        let mut overlay = ResourceSpec::new("", "", "");
934        overlay.actions = own
935            .iter()
936            .filter(|spec| ids.contains(&spec.id.as_str()))
937            .cloned()
938            .collect();
939        overlay = self.typed.bind(overlay, bound);
940        for status in &availability {
941            if !overlay.actions.iter().any(|spec| spec.id == status.id())
942                && let Some(spec) = own.iter().find(|spec| spec.id == status.id())
943            {
944                overlay.actions.push(spec.clone());
945            }
946            for spec in overlay
947                .actions
948                .iter_mut()
949                .filter(|spec| spec.id == status.id())
950            {
951                status.overlay(spec);
952            }
953        }
954        let effects = {
955            let _profile = tilo_resource::profiling::Phase::start("framework.effect_specs");
956            self.typed.effect_specs()
957        };
958        for spec in effects {
959            let shadowed = shadows
960                .iter()
961                .chain(&overlay.actions)
962                .any(|existing| existing.id == spec.id);
963            debug_assert!(!shadowed, "effect `{}` shadows an action", spec.id);
964            if shadowed {
965                tracing::warn!(effect = spec.id, "effect shadows an action; not published");
966                continue;
967            }
968            overlay.actions.push(spec);
969        }
970        if base.key.causal {
971            mark_document_jobs(&mut overlay);
972        }
973        if let Some(unreadable) = &base.key.unreadable {
974            tilo_resource::withhold_document_actions(&mut overlay, unreadable);
975        }
976        overlay.actions
977    }
978
979    /// Refresh the actions bound controls look up, reusing `head` while the
980    /// live state and locale are those it was built at. Answers the head
981    /// they were refreshed from.
982    #[inline(never)]
983    fn refresh_bindings(&self, head: Option<BindingHead>) -> BindingHead {
984        let _profile = tilo_resource::profiling::Phase::start("framework.action_bindings");
985        let version = (self.version(), ui_core::i18n::I18nManager::revision());
986        let head = match head {
987            Some(head) if head.version == version => head,
988            _ => self.binding_head(),
989        };
990        let base = self.binding_base(&head);
991        let overlay = self.binding_overlay(&base);
992        // Read while the document did not: the fields these edits were bound
993        // to are of the last state that read, not what it holds now.
994        let overlay = if self.typed.unreadable().is_some() {
995            overlay
996                .into_iter()
997                .filter(|spec| spec.id != tilo_resource::FIELD_EDIT)
998                .collect()
999        } else {
1000            overlay
1001        };
1002        self.action_bindings
1003            .update_shared(Rc::clone(&base.resource), overlay);
1004        head
1005    }
1006
1007    /// [`Self::publication`] of the resource as `doc` holds it now, which a
1008    /// change from another window or participant may have moved past what
1009    /// this view last synced.
1010    #[inline(never)]
1011    fn current_publication(&self, doc: &AutoCommit) -> ResourceSpec {
1012        // The session keeps the record: there is no document here to read
1013        // the resource from, so what it publishes is the state the view
1014        // syncs from the reading (`one-app-model.md` §4).
1015        if !self.source.is_connection()
1016            && !self.typed.follows(doc)
1017            && let Ok(current) = self.typed.read(doc)
1018        {
1019            // Publication can run after an edit and before the view syncs.
1020            // Validate its nested views against this same state, rather than
1021            // withdrawing the new resource tree using the previous frame's
1022            // handler table. The next live revision replaces this cache.
1023            self.pin_nested_views(&*current);
1024            let reason = self.typed.unreadable();
1025            let mut tree = self.publication_of(&*current);
1026            if let Some(reason) = reason {
1027                tilo_resource::withhold_document_actions(&mut tree, &reason);
1028            }
1029            return tree;
1030        }
1031        self.publication()
1032    }
1033
1034    #[inline(never)]
1035    fn publication_of(&self, state: &dyn std::any::Any) -> ResourceSpec {
1036        let _profile = tilo_resource::profiling::Phase::start("framework.publication");
1037        let tree = self.shared_document_tree(state).to_resource();
1038        self.decorate_publication(state, tree, Decoration::Tree)
1039    }
1040
1041    /// Add to `resource`, the document-owned tree, what the view and the
1042    /// framework publish on it. [`Decoration::Actions`] decorates only the
1043    /// resource's own actions; its children stay as they are.
1044    #[inline(never)]
1045    fn decorate_publication(
1046        &self,
1047        state: &dyn std::any::Any,
1048        mut resource: ResourceSpec,
1049        decoration: Decoration,
1050    ) -> ResourceSpec {
1051        let children = decoration == Decoration::Tree;
1052        let bound = {
1053            let _profile = tilo_resource::profiling::Phase::start("framework.bound_declarations");
1054            self.typed.declarations(state)
1055        };
1056        let mut rows = Vec::new();
1057        let mut availability = Vec::new();
1058        self.surfaces(&mut |surface| {
1059            let _profile = tilo_resource::profiling::Phase::start("framework.surface_publication");
1060            availability.extend(surface.availability());
1061            if children {
1062                rows.extend(
1063                    surface
1064                        .live_rows()
1065                        .into_iter()
1066                        .map(super::LiveRow::into_resource),
1067                );
1068            }
1069        });
1070        resource = self.typed.bind(resource, bound);
1071        for status in &availability {
1072            for spec in resource
1073                .actions
1074                .iter_mut()
1075                .filter(|spec| spec.id == status.id())
1076            {
1077                status.overlay(spec);
1078            }
1079        }
1080        if children {
1081            merge(&mut resource.children, rows);
1082            let _profile = tilo_resource::profiling::Phase::start("framework.row_declarations");
1083            self.bind_row_declarations(&mut resource.children);
1084        }
1085        resource = self.with_presentation_specs(resource);
1086        // The app's own effects come before the framework's history and
1087        // render operations: an app answering `diff` its own way is asked.
1088        let effects = {
1089            let _profile = tilo_resource::profiling::Phase::start("framework.effect_specs");
1090            self.typed.effect_specs()
1091        };
1092        for spec in effects {
1093            let shadowed = resource
1094                .actions
1095                .iter()
1096                .any(|existing| existing.id == spec.id);
1097            debug_assert!(!shadowed, "effect `{}` shadows an action", spec.id);
1098            if shadowed {
1099                tracing::warn!(effect = spec.id, "effect shadows an action; not published");
1100                continue;
1101            }
1102            resource = resource.with_action(spec);
1103        }
1104        resource = self.with_framework_specs(resource);
1105        if self.typed.has_causal_drains() {
1106            mark_document_jobs(&mut resource);
1107        }
1108        if let Some(unreadable) = self.typed.unreadable() {
1109            tilo_resource::withhold_document_actions(&mut resource, &unreadable);
1110        }
1111        if children {
1112            // A record a view asked for and could not open is published
1113            // with its declared actions unavailable for why: nothing is
1114            // shown of it, so nothing but those is answered.
1115            resource.children.extend(self.storage.record_resources());
1116            resource.children.extend(self.storage.unavailable_records());
1117        }
1118        resource
1119    }
1120
1121    /// `resource` with the view and effect operations of the app's
1122    /// presentation it does not offer itself.
1123    fn with_presentation_specs(&self, mut resource: ResourceSpec) -> ResourceSpec {
1124        for spec in &self.presentation {
1125            if spec.kind == tilo_actions::OperationKind::Document {
1126                continue;
1127            }
1128            if resource
1129                .actions
1130                .iter()
1131                .any(|existing| existing.id == spec.id)
1132            {
1133                continue;
1134            }
1135            resource = resource.with_action(spec.clone());
1136        }
1137        resource
1138    }
1139
1140    /// `resource` with the framework's render, `read` and History actions it
1141    /// does not offer itself.
1142    fn with_framework_specs(&self, mut resource: ResourceSpec) -> ResourceSpec {
1143        let specs = framework_specs();
1144        let read = self.resource.readable.then_some(&specs.read);
1145        let facts = self.resource.facts.then_some(&specs.facts);
1146        let history = specs.history.iter().filter(|_| self.history);
1147        for spec in specs.native.iter().chain(read).chain(facts).chain(history) {
1148            if !resource
1149                .actions
1150                .iter()
1151                .any(|existing| existing.id == spec.id)
1152            {
1153                resource = resource.with_action(spec.clone());
1154            }
1155        }
1156        resource
1157    }
1158
1159    /// Show why the record does not read as the resource instead of the
1160    /// view while it does not, never the last state as if it were current,
1161    /// and refuse submissions with the same reason.
1162    #[inline(never)]
1163    fn follow_readability(&mut self) {
1164        let reason = self.typed.unreadable();
1165        if self.unreadable.as_ref().map(|(shown, _)| shown) == reason.as_ref() {
1166            return;
1167        }
1168        self.dispatch.set_unreadable(reason.clone());
1169        self.unreadable = reason.map(|reason| {
1170            let shown: Element = Box::new(unreadable_state(&reason));
1171            (reason, shown)
1172        });
1173        self.swapped = true;
1174    }
1175
1176    /// Follow the document when it moved — a local action, a
1177    /// collaborator's change, an undo or a restore — reading only what the
1178    /// change touched.
1179    ///
1180    /// Answers the document-owned head the bindings were refreshed from, for
1181    /// the refresh after the view synced to reuse.
1182    fn refresh(&self, doc: &AutoCommit) -> BindingHead {
1183        // The session keeps the record: the state follows the reading
1184        // (`sync_remote_state`), never a document this process lent an empty
1185        // pass (`one-app-model.md` §4).
1186        if !self.source.is_connection()
1187            && let Err(error) = self.typed.sync(doc)
1188        {
1189            tracing::warn!(%error, kind = self.resource.kind, "resource unreadable");
1190        }
1191        // Refresh the same declarations the host publishes before rebuilt
1192        // controls look up their bindings. Retained surface handles share it.
1193        let head = self.refresh_bindings(self.retained_head());
1194        self.typed.follow_change(doc, &self.followed);
1195        self.feeds.refresh(doc);
1196        self.typed.drain(self.typed.revision());
1197        head
1198    }
1199}
1200
1201/// What [`ResourceRoot::new`] hands [`ResourceRoot::assemble`] of the view
1202/// it was built in.
1203struct Parts {
1204    alive: Rc<()>,
1205    dispatch: DispatchCore,
1206    saved: LocalState<RecordSaveStatus>,
1207    saves: super::saves::Saves,
1208    stages: Stages,
1209    feeds: super::history::HistoryFeeds,
1210    run: Run,
1211    storage: Rc<super::settings::ViewStorage>,
1212    content: Element,
1213    native_presentation: crate::runner::Presentation,
1214}
1215
1216/// `widget`'s hosted surface, when it is one of the resource type
1217/// `resource`.
1218fn surface_mut(
1219    widget: &mut dyn Widget,
1220    resource: std::any::TypeId,
1221) -> Option<&mut (dyn SurfacePublication + 'static)> {
1222    capability_mut::<dyn SurfacePublication>(widget)
1223        .filter(|surface| surface.resource() == resource)
1224}
1225
1226/// What [`ResourceRoot::binding_base`] is built from.
1227#[derive(Clone)]
1228struct BaseKey {
1229    head: Rc<ResourceSpec>,
1230    locale: u64,
1231    epoch: Option<String>,
1232    causal: bool,
1233    unreadable: Option<String>,
1234}
1235
1236impl PartialEq for BaseKey {
1237    fn eq(&self, other: &Self) -> bool {
1238        Rc::ptr_eq(&self.head, &other.head)
1239            && self.locale == other.locale
1240            && self.epoch == other.epoch
1241            && self.causal == other.causal
1242            && self.unreadable == other.unreadable
1243    }
1244}
1245
1246/// The shared part of what bound controls look up, at one [`BaseKey`].
1247struct BindingBase {
1248    key: BaseKey,
1249    resource: Rc<ResourceSpec>,
1250    /// How many of its actions are the head's own.
1251    head_len: usize,
1252    /// How many are the head's and the presentation's: what an effect of
1253    /// the same id would shadow.
1254    shadow_len: usize,
1255}
1256
1257/// The framework's actions every app's resource offers unless it offers
1258/// them itself, at one locale.
1259struct FrameworkSpecs {
1260    native: Vec<tilo_actions::ActionSpec>,
1261    read: tilo_actions::ActionSpec,
1262    facts: tilo_actions::ActionSpec,
1263    history: Vec<tilo_actions::ActionSpec>,
1264}
1265
1266thread_local! {
1267    static FRAMEWORK_SPECS: std::cell::RefCell<Option<(u64, Rc<FrameworkSpecs>)>> =
1268        const { std::cell::RefCell::new(None) };
1269}
1270
1271/// [`FrameworkSpecs`] at the current locale, built once per locale on
1272/// each thread rather than on every publication.
1273fn framework_specs() -> Rc<FrameworkSpecs> {
1274    let locale = ui_core::i18n::I18nManager::revision();
1275    FRAMEWORK_SPECS.with_borrow_mut(|kept| {
1276        if let Some((at, specs)) = kept.as_ref()
1277            && *at == locale
1278        {
1279            return Rc::clone(specs);
1280        }
1281        let specs = Rc::new(FrameworkSpecs {
1282            native: with_native_presentations(ResourceSpec::new("", "", "")).actions,
1283            read: read_spec(),
1284            facts: facts::facts_spec(),
1285            history: super::history::specs(),
1286        });
1287        *kept = Some((locale, Rc::clone(&specs)));
1288        specs
1289    })
1290}
1291
1292/// How much of a publication [`ResourceRoot::decorate_publication`] adds.
1293#[derive(Clone, Copy, PartialEq, Eq)]
1294enum Decoration {
1295    /// The whole published tree: the resource's actions and its children.
1296    Tree,
1297    /// Only the resource's identity and own actions, which is all bound
1298    /// controls read.
1299    Actions,
1300}
1301
1302/// The document-owned head of the publication, at the live version and
1303/// locale it was taken at.
1304#[derive(Clone)]
1305struct BindingHead {
1306    version: ((u64, u64), u64),
1307    /// [`Resource::publication_cache_epoch`] at the time.
1308    epoch: Option<String>,
1309    resource: Rc<ResourceSpec>,
1310}
1311
1312/// `resource` without its children, cloning nothing of them.
1313#[cfg(all(test, not(target_arch = "wasm32")))]
1314fn without_children(resource: &ResourceSpec) -> ResourceSpec {
1315    let ResourceSpec {
1316        id,
1317        kind,
1318        label,
1319        presentation,
1320        semantic_identity,
1321        app_id,
1322        children: _,
1323        actions,
1324        views,
1325        readers,
1326        resource_type,
1327    } = resource;
1328    ResourceSpec {
1329        id: id.clone(),
1330        kind: kind.clone(),
1331        label: label.clone(),
1332        presentation: presentation.clone(),
1333        semantic_identity: semantic_identity.clone(),
1334        app_id: app_id.clone(),
1335        children: Vec::new(),
1336        actions: actions.clone(),
1337        views: views.clone(),
1338        readers: *readers,
1339        resource_type: resource_type.clone(),
1340    }
1341}
1342
1343/// Visit the surface of type `S` the root of a resource app of `R` shows: for
1344/// a test, or a host that mounted the app, reaching the surface between
1345/// frames (`App::with_component_root` lends the root of a mounted one).
1346/// `None` when `root` is not such a root or shows no `S`.
1347pub fn with_surface<R: Resource, S: 'static, T>(
1348    root: &mut dyn std::any::Any,
1349    visit: impl FnOnce(&mut S) -> T,
1350) -> Option<T> {
1351    fn find<S: 'static>(widget: &mut dyn ui_core::widget::Widget) -> Option<&mut S> {
1352        if widget.as_any_mut().is_some_and(|any| any.is::<S>()) {
1353            return widget.as_any_mut()?.downcast_mut::<S>();
1354        }
1355        widget.children_mut().into_iter().find_map(find::<S>)
1356    }
1357    let root = root.downcast_mut::<ResourceRoot>()?;
1358    find::<S>(root.view_mut()).map(visit)
1359}
1360
1361impl ui_core::widget::document_effects::DocumentEffects for ResourceRoot {
1362    fn start_document_effects(
1363        &mut self,
1364        operation: &ui_core::host_operation::ResourceOperation<'_>,
1365        applied: &ui_core::widget::document_effects::AppliedDocument<'_>,
1366    ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
1367        self.typed.start_document_effects(operation, applied)
1368    }
1369}
1370
1371/// How many search entries are indexed away from the UI thread rather than
1372/// in the frame that asks for them.
1373const OFF_THREAD_ENTRIES: usize = 1024;
1374
1375/// What the resource's search entries are of: a revision of the resource
1376/// and the entries its surfaces show.
1377#[derive(PartialEq)]
1378struct SearchState {
1379    revision: (u64, u64),
1380    shown: Vec<SearchEntry>,
1381}
1382
1383/// An index search answers from, and the state it is of: none for the empty
1384/// index answering before the first one arrives.
1385struct AnsweringIndex {
1386    of: Option<SearchState>,
1387    indexed: Arc<IndexedEntries>,
1388}
1389
1390/// An index being built away from the UI thread.
1391struct BuildingIndex {
1392    of: SearchState,
1393    generation: u64,
1394    arriving: std::sync::mpsc::Receiver<IndexedEntries>,
1395}
1396
1397/// The resource's search index, built only when someone searches and at
1398/// most one at a time.
1399///
1400/// Every state is given its index's generation when first seen, before
1401/// anything is built: the action directory names that generation on every
1402/// frame, and the index a later search builds carries it.
1403#[derive(Default)]
1404pub(crate) struct SearchCache {
1405    answering: Option<AnsweringIndex>,
1406    building: Option<BuildingIndex>,
1407    /// The newest state nothing is built of yet, and its generation: built
1408    /// when the next search asks once no build is in flight.
1409    wanted: Option<(SearchState, u64)>,
1410    /// Entries a showing search waits on, and their state and generation:
1411    /// indexed once the build in flight lands.
1412    queued: Option<(SearchState, u64, Vec<SearchEntry>)>,
1413    /// A search is showing that waits on its surfaces' answers.
1414    watching: bool,
1415    /// The generation that holds the surfaces' answers to the latest query:
1416    /// the waiting search shows what an index this new or newer finds.
1417    answered: Option<u64>,
1418    next_generation: u64,
1419    /// How many indexes of entries have been started.
1420    #[cfg(test)]
1421    builds: usize,
1422}
1423
1424impl SearchCache {
1425    fn reserve_generation(&mut self) -> u64 {
1426        let generation = self.next_generation;
1427        self.next_generation = generation.wrapping_add(1);
1428        generation
1429    }
1430
1431    /// Answer from an index that finished building since last asked, and
1432    /// start the one a waiting search wants once nothing is in flight.
1433    fn receive(&mut self) {
1434        self.land();
1435        if self.building.is_none()
1436            && let Some((state, generation, entries)) = self.queued.take()
1437        {
1438            self.build(state, generation, entries);
1439        }
1440    }
1441
1442    fn land(&mut self) {
1443        let Some(building) = self.building.take() else {
1444            return;
1445        };
1446        match building.arriving.try_recv() {
1447            Ok(indexed) => {
1448                self.answering = Some(AnsweringIndex {
1449                    of: Some(building.of),
1450                    indexed: Arc::new(indexed),
1451                });
1452            }
1453            Err(std::sync::mpsc::TryRecvError::Empty) => self.building = Some(building),
1454            // A lost build: its state is wanted afresh when next asked.
1455            Err(std::sync::mpsc::TryRecvError::Disconnected) => {}
1456        }
1457    }
1458
1459    /// The generation of the index that answers for `state`, given now if
1460    /// `state` is new. Builds nothing.
1461    fn generation_for(&mut self, state: SearchState) -> u64 {
1462        self.receive();
1463        if let Some(answering) = &self.answering
1464            && answering.of.as_ref() == Some(&state)
1465        {
1466            return answering.indexed.generation;
1467        }
1468        if let Some(building) = &self.building
1469            && building.of == state
1470        {
1471            return building.generation;
1472        }
1473        if let Some((wanted, generation)) = &self.wanted
1474            && *wanted == state
1475        {
1476            return *generation;
1477        }
1478        let generation = self.reserve_generation();
1479        self.wanted = Some((state, generation));
1480        generation
1481    }
1482
1483    /// Index `entries`, of `state`: in this frame when they are few, else
1484    /// away from the UI thread. The index search answers from now.
1485    fn build(
1486        &mut self,
1487        state: SearchState,
1488        generation: u64,
1489        entries: Vec<SearchEntry>,
1490    ) -> Arc<IndexedEntries> {
1491        #[cfg(test)]
1492        {
1493            self.builds = self.builds.saturating_add(1);
1494        }
1495        if entries.len() >= OFF_THREAD_ENTRIES || entries.iter().any(|entry| entry.body.is_some()) {
1496            let (send, arriving) = std::sync::mpsc::channel();
1497            let building = entries.clone();
1498            let spawned = ui_core::background::Background::current().spawn(
1499                "resource-search-index",
1500                move || {
1501                    let _ = send.send(IndexedEntries::built(building, generation));
1502                },
1503            );
1504            if spawned.is_ok() {
1505                self.building = Some(BuildingIndex {
1506                    of: state,
1507                    generation,
1508                    arriving,
1509                });
1510                return self.answering();
1511            }
1512        }
1513        let indexed = Arc::new(IndexedEntries::built(entries, generation));
1514        self.answering = Some(AnsweringIndex {
1515            of: Some(state),
1516            indexed: Arc::clone(&indexed),
1517        });
1518        indexed
1519    }
1520
1521    /// The index holding the surfaces' answers to the waiting search, once
1522    /// it is in.
1523    fn answer(&mut self) -> Option<Arc<IndexedEntries>> {
1524        self.receive();
1525        let wanted = self.answered?;
1526        self.answering
1527            .as_ref()
1528            .filter(|answering| answering.of.is_some() && answering.indexed.generation >= wanted)
1529            .map(|answering| Arc::clone(&answering.indexed))
1530    }
1531
1532    /// The index search answers from now: the empty one until a first
1533    /// arrives.
1534    fn answering(&mut self) -> Arc<IndexedEntries> {
1535        if let Some(answering) = &self.answering {
1536            return Arc::clone(&answering.indexed);
1537        }
1538        let generation = self.reserve_generation();
1539        let indexed = Arc::new(IndexedEntries::built(Vec::new(), generation));
1540        self.answering = Some(AnsweringIndex {
1541            of: None,
1542            indexed: Arc::clone(&indexed),
1543        });
1544        indexed
1545    }
1546}
1547
1548// The root is the app's view plus what the framework adds: it keeps the
1549// state current, publishes the resource, forwards edits and indexes search.
1550// Everything else — layout, input, traversal, operation admission — is the
1551// view's own, forwarded by the decorator.
1552impl RootModel for ResourceRoot {
1553    ui_core::provides!(dyn ui_core::undo::CommittedActions, dyn ui_core::history_mode::capability::DocumentHistory, dyn ui_core::widget::document_effects::DocumentEffects, dyn ui_core::widget::Collaborative, #[cfg(not(target_family = "wasm"))] dyn ui_core::component_lifecycle::WindowActivation; inner = content);
1554
1555    fn view(&self) -> &(dyn Widget + 'static) {
1556        match &self.unreadable {
1557            Some((_, shown)) => shown.as_ref(),
1558            None => self.content.as_ref(),
1559        }
1560    }
1561
1562    fn view_mut(&mut self) -> &mut (dyn Widget + 'static) {
1563        match &mut self.unreadable {
1564            Some((_, shown)) => shown.as_mut(),
1565            None => self.content.as_mut(),
1566        }
1567    }
1568
1569    fn paints_ground(&self) -> bool {
1570        self.ground
1571    }
1572
1573    fn take_structure_dirty(&mut self) -> bool {
1574        let swapped = std::mem::take(&mut self.swapped);
1575        self.view_mut().take_structure_dirty() || swapped
1576    }
1577
1578    // Input is forwarded unchanged; model, storage and policy wakes remain
1579    // owned by the runner and still request their normal synchronization.
1580    fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
1581        self.content.view_only_scroll(event)
1582    }
1583
1584    // The root adds no structure of its own beyond swapping its view for
1585    // the unreadable state: a change its view can describe stays described.
1586    fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
1587        let change = self.view_mut().take_structure_change();
1588        if std::mem::take(&mut self.swapped) {
1589            ui_core::widget::StructureChange::Rebuild
1590        } else {
1591            change
1592        }
1593    }
1594
1595    fn record_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
1596        let mut owners = self.storage.person_owners();
1597        owners.extend(self.content.record_owners());
1598        owners
1599    }
1600
1601    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
1602        if self.source.is_connection() {
1603            // The session keeps the record: the view syncs from the reading
1604            // it publishes, and the document the host lends this pass holds
1605            // nothing of it (`one-app-model.md` §4). Everything the view
1606            // binds is still synced, over the state this root now holds.
1607            self.sync_remote_state();
1608            let empty = AutoCommit::new();
1609            self.sync_over(&empty, &automerge::ROOT);
1610            return;
1611        }
1612        let (state, path) = ui_core::doc::document_ref(state, path);
1613        self.sync_over(state, path);
1614    }
1615
1616    fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ui_core::automerge::ObjId) {
1617        self.typed.apply_settlements(edits);
1618        self.content.settle(edits, path);
1619        self.storage.settle();
1620    }
1621
1622    fn draw(
1623        &mut self,
1624        scene: &mut Scene,
1625        transform: Affine,
1626        text: &mut TextRenderer,
1627        theme: &Theme,
1628        presence: &PresenceCtx,
1629    ) {
1630        self.paint_ground(scene, transform, theme, presence);
1631        self.content.draw(scene, transform, text, theme, presence);
1632        // The window drew: what its bar says of the save is seen.
1633        self.save.seen();
1634    }
1635
1636    fn needs_redraw(&self) -> bool {
1637        self.native_presentation.needs_redraw()
1638            || self.content.needs_redraw()
1639            || self.save.needs_redraw(ui_core::time::now())
1640    }
1641
1642    fn save_indicator(&self) -> &super::saves::SaveIndicator {
1643        &self.save
1644    }
1645
1646    #[cfg(test)]
1647    fn save_indicator_mut(&mut self) -> &mut super::saves::SaveIndicator {
1648        &mut self.save
1649    }
1650
1651    // A screenshot harness reaches the root to stage a scenario's view state
1652    // (`framework::stage`).
1653    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1654        self
1655    }
1656
1657    // The resource is the same for every participant (an agent task sees and
1658    // invokes the same actions); only the content's tree is per participant.
1659
1660    // The undo boundary of a surface's immediate actions this event; the
1661    // host records one step per event and author.
1662
1663    // A surface's step stays open for the actions that continue it (the
1664    // letters of a word), which report no boundary of their own.
1665
1666    /// Resource and app presentation doors publish effect/view actions; their
1667    /// operation types must be in the retained handler table or publication
1668    /// withdraws the whole component surface.
1669    fn resource_operation_handlers(&self) -> HashSet<String> {
1670        let mut handlers = ui_core::host_operation::resource_operation_handlers(self.view());
1671        handlers.extend(
1672            crate::runner::operation_types()
1673                .iter()
1674                .map(|op| (*op).to_owned()),
1675        );
1676        handlers.insert(super::history::VIEW_TYPE.to_owned());
1677        handlers.insert(super::person::HISTORY_TYPE.to_owned());
1678        handlers.insert(super::record::RECORD_ACTION_TYPE.to_owned());
1679        handlers.insert(super::chain::CHAIN_ACTION_TYPE.to_owned());
1680        handlers.insert(tilo_resource::share::SHARE_TYPE.to_owned());
1681        handlers.insert(tilo_resource::share::SHARE_INVITE_TYPE.to_owned());
1682        if self.stages.opens() {
1683            handlers.insert(OPEN_TYPE.to_owned());
1684        }
1685        // Opening another app is routed by the host to the workspace
1686        // broker, whichever resource offers it.
1687        handlers.insert(tilo_actions::remote::ApplicationLaunch::TYPE.to_owned());
1688        // A resource reads itself when it says it does, and its items from
1689        // its record's table when it does not (`offer_item_reads`): the read
1690        // it publishes is always one it answers.
1691        if self.resource.readable || self.resource.record_table.is_some() {
1692            handlers.insert(READ_TYPE.to_owned());
1693        }
1694        if self.resource.facts {
1695            handlers.insert(facts::FACTS_TYPE.to_owned());
1696        }
1697        handlers.extend(
1698            self.typed
1699                .effect_operations()
1700                .into_iter()
1701                .map(str::to_owned),
1702        );
1703        for spec in (self.resource.local_operations)()
1704            .into_iter()
1705            .chain(self.presentation.iter().cloned())
1706        {
1707            if let Some(type_id) = &spec.operation_type {
1708                handlers.insert(type_id.clone());
1709            }
1710        }
1711        handlers.extend(self.nested_views().iter().cloned());
1712        handlers
1713    }
1714
1715    fn poll_resource_operation(
1716        &mut self,
1717        invocation: tilo_actions::InvocationId,
1718        cancellation_requested: bool,
1719        doc: &AutoCommit,
1720        path: &ui_core::automerge::ObjId,
1721    ) -> Option<tilo_actions::InvocationUpdate> {
1722        if let Some((update, copied)) = self.share.poll(invocation, cancellation_requested) {
1723            if copied && let tilo_actions::InvocationUpdate::Succeeded { output, .. } = &update {
1724                self.dispatch
1725                    .note_landed(tilo_resource::share::SHARE_LINK, output);
1726            }
1727            return Some(update);
1728        }
1729        self.typed
1730            .poll_effect(
1731                invocation,
1732                cancellation_requested,
1733                effect_view(&mut self.unreadable, &mut self.content),
1734            )
1735            .or_else(|| {
1736                with_app_view_mut(&mut self.content, |view| {
1737                    ui_core::host_operation::poll_resource_operation(
1738                        view,
1739                        invocation,
1740                        cancellation_requested,
1741                        doc,
1742                        path,
1743                    )
1744                })
1745            })
1746    }
1747
1748    fn start_operation(
1749        &mut self,
1750        op: &ui_core::host_operation::ResourceOperation<'_>,
1751        doc: &mut ui_core::widget::Edits<'_>,
1752        path: &ui_core::automerge::ObjId,
1753    ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
1754        // A view of a resource the system asked for: show that item first, so
1755        // the picture is of the item and not of whatever the window happened
1756        // to show. `render` is the app's answer to "show me this resource".
1757        if let Some(request) = op.unscoped()
1758            && request.type_id == crate::runner::RENDER_TYPE
1759            && !request.payload.is_empty()
1760            && let Some(anchor) = crate::runner::rendered_anchor(request.payload)
1761        {
1762            self.stages.open(&anchor)?;
1763        }
1764        if let Some(output) = with_app_view_mut(&mut self.content, |view| {
1765            self.native_presentation.start_operation(op, doc, view)
1766        })? {
1767            return Ok(Some(
1768                ui_core::host_operation::ResourceOperationStart::Completed(output),
1769            ));
1770        }
1771        if (self.resource.readable || self.resource.record_table.is_some())
1772            && let Some(request) = op.unscoped()
1773            && request.type_id == READ_TYPE
1774        {
1775            let output = read(
1776                self.typed.as_ref(),
1777                &self.resource,
1778                &*self.typed.state(),
1779                doc,
1780                request.payload,
1781            )?;
1782            return Ok(Some(
1783                ui_core::host_operation::ResourceOperationStart::Completed(output),
1784            ));
1785        }
1786        if self.resource.facts
1787            && let Some(request) = op.unscoped()
1788            && request.type_id == facts::FACTS_TYPE
1789        {
1790            let output = facts::answer(
1791                self.typed.as_ref(),
1792                &*self.typed.state(),
1793                doc,
1794                request.payload,
1795            )?;
1796            return Ok(Some(
1797                ui_core::host_operation::ResourceOperationStart::Completed(output),
1798            ));
1799        }
1800        if let Some(request) = op.unscoped()
1801            && super::share_link::handles(request.type_id)
1802        {
1803            return self.start_share(request.invocation, request.payload);
1804        }
1805        if let Some(request) = op.unscoped()
1806            && request.type_id == OPEN_TYPE
1807        {
1808            let args: serde_json::Value =
1809                tilo_actions::decode_json(request.payload).map_err(|error| error.to_string())?;
1810            let anchor = args
1811                .get(OPEN_ANCHOR)
1812                .and_then(serde_json::Value::as_str)
1813                .ok_or_else(|| "open needs the anchor of an item".to_owned())?;
1814            self.stages.open(anchor)?;
1815            return Ok(Some(
1816                ui_core::host_operation::ResourceOperationStart::Completed(
1817                    tilo_actions::ActionOutput::default(),
1818                ),
1819            ));
1820        }
1821        if let Some(output) = self.start_record_operation(op, doc)? {
1822            return Ok(Some(
1823                ui_core::host_operation::ResourceOperationStart::Completed(output),
1824            ));
1825        }
1826        // An effect reads the resource as the document stands, though no
1827        // frame has run since a collaborator's change arrived.
1828        if let Err(error) = self.typed.sync(doc) {
1829            tracing::warn!(%error, kind = self.resource.kind, "resource unreadable");
1830        }
1831        if let Some(started) = self.typed.start_effect(
1832            op,
1833            doc.doc(),
1834            effect_view(&mut self.unreadable, &mut self.content),
1835        )? {
1836            return Ok(Some(started));
1837        }
1838        if let Some(started) =
1839            ui_core::host_operation::start_operation(self.view_mut(), op, doc, path)?
1840        {
1841            return Ok(Some(started));
1842        }
1843        Ok(self.start_catalogue_view(op))
1844    }
1845}
1846
1847impl ResourceRoot {
1848    /// The record-level requests every framework app answers the same way:
1849    /// its history at a version, an operation on one of its records or its
1850    /// chain of parts, and the person's own record history.
1851    fn start_record_operation(
1852        &self,
1853        op: &ui_core::host_operation::ResourceOperation<'_>,
1854        doc: &mut ui_core::widget::Edits<'_>,
1855    ) -> Result<Option<tilo_actions::ActionOutput>, String> {
1856        let Some(request) = op.unscoped() else {
1857            return Ok(None);
1858        };
1859        let output = if request.type_id == super::history::VIEW_TYPE {
1860            super::history::answer(doc, request.payload)?
1861        } else if request.type_id == super::record::RECORD_ACTION_TYPE {
1862            self.storage
1863                .record_action(self.dispatch.me(), request.payload)?
1864        } else if request.type_id == super::chain::CHAIN_ACTION_TYPE {
1865            self.storage
1866                .chain_action(self.dispatch.me(), request.payload, doc)?
1867        } else if request.type_id == super::person::HISTORY_TYPE {
1868            self.storage.person_history(request.payload)?
1869        } else {
1870            return Ok(None);
1871        };
1872        Ok(Some(output))
1873    }
1874
1875    /// What the resource's search entries are of now, and the surfaces'
1876    /// query sinks. What the surfaces show (a page's text, live rows)
1877    /// changes on its own, beside the resource's revision.
1878    fn search_state(&self) -> (SearchState, Vec<super::publishing::SearchQueries>) {
1879        let mut shown = Vec::new();
1880        let mut queries = Vec::new();
1881        self.surfaces(&mut |surface| {
1882            shown.extend(surface.search_entries());
1883            queries.extend(surface.search_queries());
1884        });
1885        let state = SearchState {
1886            revision: self.version(),
1887            shown,
1888        };
1889        (state, queries)
1890    }
1891
1892    /// The index search answers `state` from, building it if nothing is. A
1893    /// large set of entries (a browsing history, a store catalogue) or one
1894    /// with prose bodies (whole books) is indexed away from the UI thread,
1895    /// one build at a time: until the index of `state` arrives, search
1896    /// answers from the one before it, and states that come and go while a
1897    /// build is in flight cost only the newest one's build after it.
1898    fn current_search_index(&self, state: SearchState) -> Arc<IndexedEntries> {
1899        let mut cache = self.search.borrow_mut();
1900        let generation = cache.generation_for(state);
1901        if let Some(answering) = &cache.answering
1902            && answering.of.is_some()
1903            && answering.indexed.generation == generation
1904        {
1905            return Arc::clone(&answering.indexed);
1906        }
1907        if cache.building.is_some() {
1908            return cache.answering();
1909        }
1910        let Some((state, generation)) = cache.wanted.take() else {
1911            return cache.answering();
1912        };
1913        let entries = self.entries_of(&state);
1914        cache.build(state, generation, entries)
1915    }
1916
1917    /// Whether the surfaces' secrets are unlocked: locked when any surface
1918    /// says so, unlocked when one says so and none disagrees, else unsaid.
1919    fn secrets_unlocked(&self) -> Option<bool> {
1920        let mut said = None;
1921        self.surfaces(&mut |surface| match surface.secrets_unlocked() {
1922            Some(false) => said = Some(false),
1923            Some(true) if said.is_none() => said = Some(true),
1924            _ => {}
1925        });
1926        said
1927    }
1928
1929    /// Everything search finds in `state`: the resource's entries and what
1930    /// its surfaces show.
1931    fn entries_of(&self, state: &SearchState) -> Vec<SearchEntry> {
1932        let mut entries = self.typed.search(&*self.typed.state());
1933        entries.extend(state.shown.iter().cloned());
1934        entries
1935    }
1936
1937    /// A search that waits on its surfaces' answers (a server still
1938    /// looking) gets an index of what they show once every one of them has
1939    /// answered the latest query.
1940    fn follow_search(&self) {
1941        if !self.search.borrow().watching {
1942            return;
1943        }
1944        let mut answering = false;
1945        self.surfaces(&mut |surface| {
1946            answering |= surface
1947                .search_queries()
1948                .is_some_and(|queries| queries.answering());
1949        });
1950        if answering {
1951            return;
1952        }
1953        let (state, _) = self.search_state();
1954        let mut cache = self.search.borrow_mut();
1955        cache.watching = false;
1956        let generation = cache.generation_for(state);
1957        cache.answered = Some(generation);
1958        if cache.building.is_some() {
1959            // Indexed once the build in flight lands.
1960            if let Some((state, wanted)) = cache.wanted.take() {
1961                let entries = self.entries_of(&state);
1962                cache.queued = Some((state, wanted, entries));
1963            }
1964            return;
1965        }
1966        if let Some((state, wanted)) = cache.wanted.take() {
1967            let entries = self.entries_of(&state);
1968            cache.build(state, wanted, entries);
1969        }
1970    }
1971}
1972
1973impl ResourceRoot {
1974    fn start_catalogue_view(
1975        &self,
1976        op: &ui_core::host_operation::ResourceOperation<'_>,
1977    ) -> Option<ui_core::host_operation::ResourceOperationStart> {
1978        // View openers from the resource/presentation catalogue and from its
1979        // nested resources (e.g. a rich-text body's `tilo.rich-text.view`
1980        // editor/compact) are already mounted; accept them so agents and
1981        // Ctrl+K can switch without a missing-handler fault.
1982        let request = op.unscoped()?;
1983        if request.kind != tilo_actions::OperationKind::View {
1984            return None;
1985        }
1986        let known = (self.resource.local_operations)()
1987            .into_iter()
1988            .chain(self.presentation.iter().cloned())
1989            .any(|spec| spec.operation_type.as_deref() == Some(request.type_id))
1990            || self.nested_views().contains(request.type_id);
1991        known.then(|| {
1992            ui_core::host_operation::ResourceOperationStart::Completed(
1993                tilo_actions::ActionOutput::default(),
1994            )
1995        })
1996    }
1997
1998    /// Start minting a link to one of the resources this root publishes.
1999    fn start_share(
2000        &mut self,
2001        invocation: tilo_actions::InvocationId,
2002        payload: &[u8],
2003    ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
2004        let record = self
2005            .run
2006            .launched
2007            .record
2008            .as_ref()
2009            .map(|record| record.as_str().to_owned());
2010        let publisher = super::share_link::Publisher {
2011            app: self.dispatch.app_id(),
2012            record: record.as_deref(),
2013            root: self.dispatch.resource_id(),
2014        };
2015        let may_invite = self.dispatch.may_invite();
2016        let started = self
2017            .share
2018            .start(invocation, payload, &publisher, may_invite)?;
2019        Ok(Some(match started {
2020            Some(minted) => {
2021                if minted.copied {
2022                    self.dispatch
2023                        .note_landed(tilo_resource::share::SHARE_LINK, &minted.output);
2024                }
2025                ui_core::host_operation::ResourceOperationStart::Completed(minted.output)
2026            }
2027            None => ui_core::host_operation::ResourceOperationStart::Accepted,
2028        }))
2029    }
2030}
2031
2032// The root's own clients for the action door (minting links, opening
2033// references) share the app's collaboration connection with whatever the
2034// view keeps there.
2035impl ui_core::widget::Collaborative for ResourceRoot {
2036    fn take_collab_requests(&mut self) -> Vec<ui_core::collaboration::CollabRequest> {
2037        let mut requests =
2038            ui_core::widget::Collaborative::take_collab_requests(self.share.remote_mut());
2039        if let Some(mut door) = self.dispatch.opener.door() {
2040            requests.extend(ui_core::widget::Collaborative::take_collab_requests(
2041                &mut door,
2042            ));
2043        }
2044        with_app_view_mut(&mut self.content, |view| {
2045            if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2046                requests.extend(content.take_collab_requests());
2047            }
2048        });
2049        requests
2050    }
2051
2052    fn handle_collab_event(&mut self, event: &ui_core::collaboration::CollabEvent) {
2053        ui_core::widget::Collaborative::handle_collab_event(self.share.remote_mut(), event);
2054        if let Some(mut door) = self.dispatch.opener.door() {
2055            ui_core::widget::Collaborative::handle_collab_event(&mut door, event);
2056        }
2057        with_app_view_mut(&mut self.content, |view| {
2058            if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2059                content.handle_collab_event(event);
2060            }
2061        });
2062    }
2063}
2064
2065impl RootSearch for ResourceRoot {
2066    fn search_providers(&self) -> Vec<Box<dyn SearchProvider>> {
2067        // The index is rebuilt only when the resource or what its surfaces
2068        // show moved since the last one.
2069        let (state, queries) = self.search_state();
2070        let indexed = self.current_search_index(state);
2071        // The app's one search source: what its content widgets show beside
2072        // the resource reaches it as their surfaces' entries, never as a
2073        // provider of their own.
2074        let mut source = ResourceSearch::new(self.resource.label, self.dispatch.app_id(), indexed);
2075        source.scopes = self.typed.search_scopes(&*self.typed.state());
2076        source.queries = queries;
2077        // A person who may not read the record is found nothing of it.
2078        source.readable = self.dispatch.may_read();
2079        source.live = Some(LiveSearch {
2080            cache: Rc::clone(&self.search),
2081            answered: std::cell::RefCell::new(None),
2082        });
2083        source.opens = self
2084            .stages
2085            .opens()
2086            .then(|| tilo_actions::ActionAddress::new(self.dispatch.resource_id(), "open"));
2087        vec![Box::new(source)]
2088    }
2089
2090    /// The one source [`Self::search_providers`] makes, named with the
2091    /// generation its index has or will have once someone searches: the
2092    /// action directory reads this on every frame, and indexing whole books
2093    /// per keystroke for a search nobody runs is what it must not cost.
2094    fn search_provider_descriptors(
2095        &self,
2096    ) -> Vec<ui_core::search::capability::SearchProviderDescriptor> {
2097        let (state, _) = self.search_state();
2098        let generation = self.search.borrow_mut().generation_for(state);
2099        vec![ui_core::search::capability::SearchProviderDescriptor::new(
2100            self.resource.label,
2101            Some(generation),
2102            // `ResourceSearch` keeps the provider default.
2103            ui_core::widget::ActionDirectoryInvocationDomain::Unsupported,
2104        )]
2105    }
2106
2107    /// The resource's entries and what its surfaces show, as of the same
2108    /// generation the action directory names, with whether the surfaces'
2109    /// secrets are unlocked.
2110    fn search_index_state(&self) -> Option<ui_core::search::index_publication::IndexState> {
2111        let (state, _) = self.search_state();
2112        let generation = self.search.borrow_mut().generation_for(state);
2113        Some(ui_core::search::index_publication::IndexState {
2114            generation,
2115            secrets_unlocked: self.secrets_unlocked(),
2116        })
2117    }
2118
2119    /// Every entry a hit can open, as the session's persistent index keeps
2120    /// it: short metadata only, never a prose body. A view that keeps its
2121    /// entries out of the index
2122    /// ([`Publishes::search_entries_while_running`](super::Publishes::search_entries_while_running))
2123    /// contributes none.
2124    fn search_index_entries(&self) -> Vec<ui_core::search::index_publication::IndexEntry> {
2125        let mut entries = self.typed.search(&*self.typed.state());
2126        self.surfaces(&mut |surface| {
2127            if surface.kept_in_search_index() {
2128                entries.extend(surface.search_entries());
2129            }
2130        });
2131        entries.iter().filter_map(index_entry).collect()
2132    }
2133
2134    /// A picked hit opens the app at its anchor: the views registered with
2135    /// `on_open` show it, then any hit the content provided reaches it.
2136    fn activate_search_result(
2137        &mut self,
2138        action_id: &str,
2139        doc: &mut ui_core::widget::Edits<'_>,
2140        path: &ModelPath,
2141    ) {
2142        if self.stages.open(action_id).is_err() {
2143            with_app_view_mut(&mut self.content, |view| {
2144                ui_core::search::capability::activate_search_result(view, action_id, doc, path);
2145            });
2146        }
2147    }
2148}
2149
2150impl RootPublishes for ResourceRoot {
2151    fn resources(&self, doc: &AutoCommit) -> Vec<ResourceSpec> {
2152        RootPublishes::resources_for(self, None, doc)
2153    }
2154    fn shared_resources_for(
2155        &self,
2156        participant: Option<&tilo_actions::ParticipantId>,
2157        doc: &AutoCommit,
2158    ) -> Option<Vec<ui_core::published::SharedResource>> {
2159        self.shared_resources(participant, doc)
2160    }
2161    fn resources_for(
2162        &self,
2163        participant: Option<&tilo_actions::ParticipantId>,
2164        doc: &AutoCommit,
2165    ) -> Vec<ResourceSpec> {
2166        // What surfaces bind to where someone is, they bind to this
2167        // participant's view.
2168        let _viewer = participant
2169            .cloned()
2170            .map(ui_core::invocation_scope::ViewerScope::enter);
2171        let mut resource = self.current_publication(doc);
2172        // The content may add live external entities beside the resource,
2173        // never a document action: those are the resource's own. One naming
2174        // the resource joins it rather than publishing it twice.
2175        let mut extras = Vec::new();
2176        for extra in ui_core::resources::resources_for(self.app_view(), participant, doc) {
2177            let extra = outside_document(extra);
2178            if extra.id == resource.id {
2179                join(&mut resource, extra);
2180            } else {
2181                extras.push(extra);
2182            }
2183        }
2184        if self.stages.opens()
2185            && !resource
2186                .actions
2187                .iter()
2188                .any(|existing| existing.id == "open")
2189        {
2190            resource = resource.with_action(open_spec());
2191        }
2192        if self.stages.opens() {
2193            offer_item_open(self.resource.opens_as_item, &mut resource);
2194        }
2195        if let Some(table) = self.resource.record_table {
2196            offer_item_reads(&mut resource, table, self.resource.kind);
2197        }
2198        let mut resources = vec![resource];
2199        resources.append(&mut extras);
2200        resources
2201    }
2202    fn resource(&self, doc: &AutoCommit, id: &str) -> Option<ResourceSpec> {
2203        RootPublishes::resource_for(self, None, doc, id)
2204    }
2205    /// A live row a surface is still reading.
2206    fn resource_pending(&self, id: &str) -> bool {
2207        let mut pending = false;
2208        self.surfaces(&mut |surface| pending = pending || surface.live_row_pending(id));
2209        pending
2210    }
2211    /// Resource `id` in the published tree, or one the tree leaves out: a
2212    /// child the resource resolves by id, or a live row a surface resolves.
2213    fn resource_for(
2214        &self,
2215        participant: Option<&tilo_actions::ParticipantId>,
2216        doc: &AutoCommit,
2217        id: &str,
2218    ) -> Option<ResourceSpec> {
2219        let _viewer = participant
2220            .cloned()
2221            .map(ui_core::invocation_scope::ViewerScope::enter);
2222        // A change from another window or participant may not have reached
2223        // this view yet: an item it removed is gone, one it made is there.
2224        if let Some(published) = find(&[self.current_publication(doc)], id) {
2225            return Some(published);
2226        }
2227        let current = if self.typed.follows(doc) {
2228            None
2229        } else {
2230            self.typed.read(doc).ok()
2231        };
2232        let state = current.unwrap_or_else(|| self.typed.state());
2233        let mut child = self.typed.child(&*state, doc, id);
2234        if self.resource.readable
2235            && let Some(child) = &mut child
2236        {
2237            offer_document_reads(child, &|id| self.typed.document_child(&*state, id));
2238        }
2239        let mut row = None;
2240        self.surfaces(&mut |surface| {
2241            if row.is_none() {
2242                row = surface.live_row(id).map(super::LiveRow::into_resource);
2243            }
2244        });
2245        // An item past the published page is resolved on its own, and the row
2246        // a surface shows for it joins it as it does in the page.
2247        let resolved = match (child, row) {
2248            (Some(mut child), Some(row)) => {
2249                join(&mut child, row);
2250                Some(child)
2251            }
2252            (child, row) => child.or(row),
2253        };
2254        if let Some(mut resolved) = resolved {
2255            self.bind_row_declarations(std::slice::from_mut(&mut resolved));
2256            if self.stages.opens() {
2257                offer_item_open(self.resource.opens_as_item, &mut resolved);
2258            }
2259            return Some(resolved);
2260        }
2261        // Last, what the content resolves by id beyond what it publishes (a
2262        // place, a record past the published page), without a document
2263        // action: those are the resource's own.
2264        ui_core::resources::resource_for(self.app_view(), None, doc, id)
2265            .map(super::rows::resolved_outside_document)
2266    }
2267    fn document_action_result(
2268        &self,
2269        doc: &AutoCommit,
2270        address: &tilo_actions::ActionAddress,
2271        changes: &[ui_core::automerge::Patch],
2272    ) -> Option<serde_json::Value> {
2273        // The record's own actions, and its verbs bound on its items.
2274        if address.resource == self.dispatch.resource_id()
2275            || (self.resource.item_of)(address.resource.as_str())
2276        {
2277            if super::history::is_edit(&address.action) {
2278                return super::history::result(&address.action, doc, changes);
2279            }
2280            // What the action minted rides beside its own answer, the
2281            // resource's or else its surface's. An answer that is not an
2282            // object (an editor's summary naming the blocks it made) already
2283            // says what it made and is kept as it is.
2284            let minted = tilo_resource::take_minted();
2285            let answer = self
2286                .typed
2287                .answer(doc, &address.action, changes)
2288                .or_else(|| {
2289                    ui_core::resources::document_action_result(
2290                        self.app_view(),
2291                        doc,
2292                        address,
2293                        changes,
2294                    )
2295                });
2296            return match answer {
2297                _ if minted.is_empty() => answer,
2298                Some(serde_json::Value::Object(mut fields)) => {
2299                    fields.insert("minted".into(), minted.into());
2300                    Some(serde_json::Value::Object(fields))
2301                }
2302                Some(other) => Some(other),
2303                None => Some(serde_json::json!({ "minted": minted })),
2304            };
2305        }
2306        ui_core::resources::document_action_result(self.app_view(), doc, address, changes)
2307    }
2308}
2309
2310// The root owns the file whose history it shows beside the resource's (the
2311// runner checks the source it registered is this root's); everything else
2312// about History is its content's, as before.
2313impl ui_core::history_mode::capability::DocumentHistory for ResourceRoot {
2314    fn history_registration(
2315        &self,
2316    ) -> Result<
2317        Option<ui_core::history_mode::HistoryRegistration>,
2318        ui_core::history_mode::HistoryRegistrationError,
2319    > {
2320        match &self.run.file_history {
2321            Some((source, definition)) => {
2322                let scopes = definition.scopes.iter().map(|scope| {
2323                    ui_core::history_mode::HistorySourceRegistration::CheckedFiles {
2324                        scope: scope.key,
2325                        source: source.clone(),
2326                    }
2327                });
2328                ui_core::history_mode::HistoryRegistration::new(definition, scopes.collect())
2329                    .map(Some)
2330            }
2331            None => ui_core::history_mode::capability::history_registration(self.app_view()),
2332        }
2333    }
2334
2335    fn history_restore_readiness(&self, scope: &str) -> Result<(), String> {
2336        ui_core::widget::capability::capability::<
2337            dyn ui_core::history_mode::capability::DocumentHistory,
2338        >(self.app_view())
2339        .map_or(Ok(()), |content| content.history_restore_readiness(scope))
2340    }
2341
2342    fn document_history_definition(
2343        &self,
2344    ) -> Option<&'static ui_core::history_mode::HistoryDefinition> {
2345        ui_core::history_mode::capability::document_history_definition(self.app_view())
2346    }
2347
2348    fn physical_text_policy(
2349        &self,
2350        previous: Option<&tilo_actions::EncodedAction>,
2351        next: &tilo_actions::EncodedAction,
2352    ) -> (bool, bool) {
2353        self.typed.text_policy(previous, next)
2354    }
2355
2356    fn document_action_applied(
2357        &mut self,
2358        action: &tilo_actions::EncodedAction,
2359        before: &[ui_core::automerge::ChangeHash],
2360        doc: &ui_core::automerge::AutoCommit,
2361        path: &ui_core::automerge::ObjId,
2362    ) {
2363        // An action a participant took (an agent's verb) leaves them where
2364        // the resource says it lands, once the state follows it.
2365        if let Some(participant) = ui_core::invocation_scope::current_participant() {
2366            self.typed.note_applied(&participant, action);
2367        }
2368        with_app_view_mut(&mut self.content, |view| {
2369            if let Some(content) = ui_core::widget::capability::capability_mut::<
2370                dyn ui_core::history_mode::capability::DocumentHistory,
2371            >(view)
2372            {
2373                content.document_action_applied(action, before, doc, path);
2374            }
2375        });
2376    }
2377}
2378
2379impl ui_core::undo::CommittedActions for ResourceRoot {
2380    fn take_committed_action(&mut self) -> Option<Vec<ui_core::automerge::ChangeHash>> {
2381        self.dispatch.take_committed()
2382    }
2383    fn keeps_undo_step_open(&self) -> bool {
2384        self.dispatch.keeps_step_open()
2385    }
2386}
2387
2388impl RootApp for ResourceRoot {
2389    fn app_kind(&self) -> AppKind {
2390        AppKind::Editor
2391    }
2392
2393    fn app_title(&self) -> &str {
2394        self.title
2395    }
2396
2397    fn app_icon(&self) -> IconDescriptor {
2398        self.icon.clone()
2399    }
2400
2401    fn device_shell_operation(
2402        &mut self,
2403        operation: &ui_core::floating_surface::DeviceShellOperation,
2404    ) -> Result<serde_json::Value, String> {
2405        fn perform(
2406            widget: &mut dyn Widget,
2407            resource: std::any::TypeId,
2408            operation: &ui_core::floating_surface::DeviceShellOperation,
2409        ) -> Option<Result<serde_json::Value, String>> {
2410            if let Some(surface) = surface_mut(widget, resource)
2411                && surface.supports_device_shell()
2412            {
2413                return Some(surface.device_shell_operation(operation));
2414            }
2415            widget
2416                .children_mut()
2417                .into_iter()
2418                .find_map(|child| perform(child, resource, operation))
2419        }
2420        perform(self.content.as_mut(), self.resource.id, operation)
2421            .unwrap_or_else(|| Err("Terminal is not available".into()))
2422    }
2423
2424    fn request_device_shell(&mut self) -> bool {
2425        fn request(widget: &mut dyn Widget, resource: std::any::TypeId) -> bool {
2426            if let Some(surface) = surface_mut(widget, resource)
2427                && surface.request_device_shell()
2428            {
2429                return true;
2430            }
2431            widget
2432                .children_mut()
2433                .into_iter()
2434                .any(|child| request(child, resource))
2435        }
2436        request(self.content.as_mut(), self.resource.id)
2437    }
2438
2439    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
2440        fn take(
2441            widget: &mut dyn Widget,
2442            resource: std::any::TypeId,
2443        ) -> Option<ui_core::widget::AppHostIntent> {
2444            if let Some(surface) = surface_mut(widget, resource)
2445                && let Some(intent) = surface.take_host_intent()
2446            {
2447                return Some(intent);
2448            }
2449            // A view whose app runs elsewhere asks through its own door.
2450            if let Some(intent) = ui_core::component_lifecycle::take_host_intent(widget) {
2451                return Some(intent);
2452            }
2453            widget
2454                .children_mut()
2455                .into_iter()
2456                .find_map(|child| take(child, resource))
2457        }
2458        take(self.content.as_mut(), self.resource.id)
2459    }
2460
2461    fn take_save_retry(&mut self) -> bool {
2462        self.save.take_record_retry()
2463    }
2464
2465    fn record_save_status(&mut self, status: RecordSaveStatus) {
2466        self.save.record(&status, ui_core::time::now());
2467        self.saved.set(status);
2468    }
2469
2470    // The workspace refused what this record sent, and the record withdrew it:
2471    // the view shows the stored state again, and the action reads as
2472    // refused, as one this device refused does.
2473    fn record_refused(&mut self, refusal: &ui_core::record::RecordRefusal) {
2474        self.dispatch.refuse_by_workspace(refusal);
2475        self.typed.represent();
2476    }
2477
2478    fn take_notices(&mut self) -> Vec<String> {
2479        self.dispatch.take_notices()
2480    }
2481
2482    fn record_said(&mut self, text: &str) {
2483        self.dispatch.kept(text);
2484    }
2485
2486    // A surface that keeps presentation of its own across restarts (the
2487    // open item, the caret) saves it as the window's.
2488    fn capture_application_view(&self) -> Result<Option<ApplicationLocalView>, ThawError> {
2489        capability::<dyn ApplicationViewState>(self.app_view())
2490            .map_or(Ok(None), ApplicationViewState::capture_application_view)
2491    }
2492
2493    fn restore_application_view(
2494        &mut self,
2495        local: &ApplicationLocalView,
2496        doc: &AutoCommit,
2497    ) -> Result<ApplicationFocusRestore, ThawError> {
2498        with_app_view_mut(&mut self.content, |view| {
2499            match capability_mut::<dyn ApplicationViewState>(view) {
2500                Some(view) => view.restore_application_view(local, doc),
2501                None => Err(ThawError::State(
2502                    "application-local restoration unsupported".into(),
2503                )),
2504            }
2505        })
2506    }
2507
2508    fn finish_application_view_restore(&mut self) -> Result<(), ThawError> {
2509        with_app_view_mut(&mut self.content, |view| {
2510            capability_mut::<dyn ApplicationViewState>(view).map_or(
2511                Ok(()),
2512                ApplicationViewState::finish_application_view_restore,
2513            )
2514        })
2515    }
2516}
2517
2518#[cfg(test)]
2519mod index_entry_tests;
2520
2521#[cfg(test)]
2522#[cfg(not(target_arch = "wasm32"))]
2523mod search_tests;
2524
2525/// What a window shows instead of the app's view while its record does not
2526/// read as the resource: what happened and what to do, in the person's
2527/// words, with the reason the record was refused (a path and a missing
2528/// value) behind the state's "Details".
2529fn unreadable_state(reason: &str) -> ui_core::widget::EmptyState {
2530    ui_core::widget::EmptyState::new(ui_core::i18n::I18nManager::lookup_plain(
2531        "framework-unreadable",
2532    ))
2533    .with_icon(ui_core::icons::Icon::CircleAlert)
2534    .with_message(ui_core::i18n::I18nManager::lookup_plain(
2535        "framework-unreadable-hint",
2536    ))
2537    .with_details(reason)
2538}
2539
2540/// The window of a record that did not read as the resource when it was
2541/// opened: why, instead of the app's view, and the resource's actions
2542/// published as unavailable for that reason.
2543pub(crate) struct Unopened {
2544    title: &'static str,
2545    icon: IconDescriptor,
2546    /// What it publishes: the resource's declared actions, every document
2547    /// one unavailable for the reason.
2548    resource: ResourceSpec,
2549    shown: ui_core::widget::EmptyState,
2550}
2551
2552impl Unopened {
2553    /// The window of a record that does not read as the resource of
2554    /// `kind` and `label`, whose actions are `declarations`.
2555    pub(crate) fn new(
2556        title: &'static str,
2557        icon: IconDescriptor,
2558        (kind, label): (&'static str, &'static str),
2559        declarations: &[tilo_resource::Declaration],
2560        reason: &str,
2561    ) -> Self {
2562        let mut resource =
2563            ResourceSpec::new(kind, kind, tilo_resource::label::localized(kind, label));
2564        for decl in declarations {
2565            resource = resource.with_action(tilo_resource::declaration_spec(decl));
2566        }
2567        tilo_resource::withhold_document_actions(&mut resource, reason);
2568        Self {
2569            title,
2570            icon,
2571            shown: unreadable_state(reason),
2572            resource,
2573        }
2574    }
2575}
2576
2577impl Unopened {
2578    /// The window as a host mounts it, its widget table compiled once for
2579    /// every app.
2580    pub(crate) fn boxed(self) -> Box<dyn AppWidget> {
2581        Box::new(self)
2582    }
2583}
2584
2585impl Decorator for Unopened {
2586    ui_core::provides!(dyn ui_core::resources::PublishesResources);
2587
2588    type Inner = ui_core::widget::EmptyState;
2589
2590    fn inner(&self) -> &ui_core::widget::EmptyState {
2591        &self.shown
2592    }
2593
2594    fn inner_mut(&mut self) -> &mut ui_core::widget::EmptyState {
2595        &mut self.shown
2596    }
2597}
2598
2599impl ui_core::resources::PublishesResources for Unopened {
2600    fn resources(&self, _doc: &AutoCommit) -> Vec<ResourceSpec> {
2601        vec![self.resource.clone()]
2602    }
2603}
2604
2605impl AppWidget for Unopened {
2606    fn app_kind(&self) -> AppKind {
2607        AppKind::Editor
2608    }
2609
2610    fn app_title(&self) -> &str {
2611        self.title
2612    }
2613
2614    fn app_icon(&self) -> IconDescriptor {
2615        self.icon.clone()
2616    }
2617}
2618
2619#[cfg(test)]
2620mod tests {
2621    use super::*;
2622    use ui_core::search::{DEFAULT_SEARCH_RESULT_LIMIT, SearchQuery};
2623
2624    #[test]
2625    fn child_document_reads_are_bound_to_their_own_resource() {
2626        let mut child = ResourceSpec::new("book/page/body", "text", "Body");
2627        child.children.push(ResourceSpec::new(
2628            "private-child",
2629            "account",
2630            "Secret account",
2631        ));
2632        offer_document_reads(&mut child, &|id| id == "book/page/body");
2633        assert!(
2634            child.children[0].actions.is_empty(),
2635            "eligibility is not inherited"
2636        );
2637        let read = &child.actions[0];
2638        assert!(read.contract.reads_document());
2639        let encoded = read.build_json(&serde_json::Map::new()).unwrap();
2640        assert_eq!(encoded.type_id, READ_TYPE);
2641        let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2642        assert_eq!(payload["item"], "book/page/body");
2643        assert!(
2644            read.build_json(
2645                serde_json::json!({"item": "another/book"})
2646                    .as_object()
2647                    .unwrap()
2648            )
2649            .is_err()
2650        );
2651        let encoded = read
2652            .build_json(serde_json::json!({"version": "past"}).as_object().unwrap())
2653            .unwrap();
2654        let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2655        assert_eq!(payload["item"], "book/page/body");
2656        assert_eq!(payload["version"], "past");
2657    }
2658
2659    /// The ids the framework keeps are the ones it publishes on every
2660    /// app: the generic actions, `open`, `read`, history and presentation.
2661    #[test]
2662    fn the_reserved_ids_are_what_the_framework_publishes() {
2663        let reserved = reserved_action_ids();
2664        for id in [
2665            "apply",
2666            "group",
2667            "field-edit",
2668            "open",
2669            "read",
2670            "undo",
2671            "redo",
2672            "restore",
2673            "restore-reviewed",
2674            "review-restore",
2675            "checkpoint",
2676            "history",
2677            "diff",
2678            "render",
2679        ] {
2680            assert!(
2681                reserved.iter().any(|kept| kept == id),
2682                "{id} in {reserved:?}"
2683            );
2684        }
2685        assert!(
2686            !reserved.iter().any(|kept| kept == "rename"),
2687            "an app's own verb name is not kept"
2688        );
2689    }
2690
2691    #[test]
2692    fn a_search_entry_naming_an_action_is_a_hit_that_invokes_it() {
2693        let open = tilo_actions::ActionAddress::new("notes.page:1", "open");
2694        let search = ResourceSearch {
2695            name: "Notes",
2696            app: "so.tilo.notes",
2697            indexed: Arc::new(IndexedEntries::built(
2698                vec![
2699                    SearchEntry::new("Groceries", "milk and eggs").invoking(open.clone()),
2700                    SearchEntry::new("Garden", "roses").anchored("page:2"),
2701                ],
2702                0,
2703            )),
2704            scopes: Vec::new(),
2705            queries: Vec::new(),
2706            readable: true,
2707            live: None,
2708            opens: None,
2709        };
2710        let groceries = search.search("groceries");
2711        assert_eq!(groceries.len(), 1);
2712        assert_eq!(groceries[0].action_address(), Some(open));
2713        let garden = search.search("garden");
2714        assert_eq!(garden.len(), 1);
2715        assert_eq!(garden[0].action_address(), None);
2716        assert_eq!(garden[0].action_id, "page:2");
2717    }
2718
2719    #[test]
2720    fn a_search_entry_reads_as_its_group_subtitle_and_icon() {
2721        let reveal = tilo_actions::ActionAddress::new("video.marker:1", "reveal");
2722        let search = ResourceSearch {
2723            name: "Project",
2724            app: "so.tilo.project",
2725            indexed: Arc::new(IndexedEntries::built(
2726                vec![
2727                    SearchEntry::new("Sunset", "the last light")
2728                        .invoking(reveal)
2729                        .grouped("Timeline markers")
2730                        .subtitled("0:21 · Drone")
2731                        .with_icon("star")
2732                        .boosted(5),
2733                    SearchEntry::new("Sunset take", ""),
2734                ],
2735                0,
2736            )),
2737            scopes: Vec::new(),
2738            queries: Vec::new(),
2739            readable: true,
2740            live: None,
2741            opens: None,
2742        };
2743        let hits = search.search("sunset");
2744        let marker = hits
2745            .iter()
2746            .find(|hit| hit.title == "Sunset")
2747            .expect("the marker is found");
2748        assert_eq!(marker.kind, "Timeline markers");
2749        assert_eq!(marker.subtitle.as_deref(), Some("0:21 · Drone"));
2750        assert_eq!(marker.icon, Some(ui_core::icons::Icon::Star));
2751        let plain = hits
2752            .iter()
2753            .find(|hit| hit.title == "Sunset take")
2754            .expect("the plain entry is found");
2755        assert_eq!(plain.kind, "Project", "an ungrouped hit is the resource's");
2756        assert!(plain.preview.is_none(), "no text, no preview");
2757        // Subtitles are searched too: a marker is found by the shot it is on.
2758        assert!(
2759            search
2760                .search("drone")
2761                .iter()
2762                .any(|hit| hit.title == "Sunset")
2763        );
2764    }
2765
2766    #[test]
2767    fn every_entry_stays_findable_page_by_page() {
2768        let search = ResourceSearch::new(
2769            "Tracks",
2770            "so.tilo.test",
2771            Arc::new(IndexedEntries::built(
2772                (0..257)
2773                    .map(|n| SearchEntry::new(format!("Common track {n}"), ""))
2774                    .collect(),
2775                0,
2776            )),
2777        );
2778        let mut cursor = None;
2779        let mut seen = std::collections::HashSet::new();
2780        loop {
2781            let page = search.search_page(&SearchQuery::new("common"), cursor.as_ref(), 17, 5);
2782            assert!(page.results.len() <= 5);
2783            for hit in &page.results {
2784                assert!(seen.insert(hit.title.clone()), "{} came twice", hit.title);
2785            }
2786            let Some(next) = page.next else { break };
2787            assert!(search.action_directory_cursor_is_current(&next));
2788            cursor = Some(next);
2789        }
2790        assert_eq!(seen.len(), 257, "every match is reachable");
2791    }
2792
2793    #[test]
2794    fn no_caller_materialises_more_than_one_page_at_once() {
2795        let search = ResourceSearch::new(
2796            "Tables",
2797            "so.tilo.test",
2798            Arc::new(IndexedEntries::built(
2799                (0..300)
2800                    .map(|n| SearchEntry::new(format!("table {n}"), ""))
2801                    .collect(),
2802                0,
2803            )),
2804        );
2805        assert_eq!(
2806            search.search_limited("", usize::MAX).len(),
2807            DEFAULT_SEARCH_RESULT_LIMIT
2808        );
2809        assert_eq!(
2810            search
2811                .search_page(&SearchQuery::new(""), None, usize::MAX, usize::MAX)
2812                .results
2813                .len(),
2814            DEFAULT_SEARCH_RESULT_LIMIT
2815        );
2816    }
2817
2818    /// A chapter of a book: one paragraph per segment, each opening the
2819    /// book where it is.
2820    struct Chapter(Vec<&'static str>);
2821
2822    impl tilo_resource::SearchText for Chapter {
2823        fn segment_count(&self) -> usize {
2824            self.0.len()
2825        }
2826
2827        fn piece_count(&self, segment: usize) -> usize {
2828            usize::from(segment < self.0.len())
2829        }
2830
2831        fn piece(&self, segment: usize, piece: usize) -> Option<tilo_resource::SearchPiece<'_>> {
2832            let text = self.0.get(segment)?;
2833            (piece == 0).then_some(tilo_resource::SearchPiece {
2834                text,
2835                space_before: segment > 0,
2836            })
2837        }
2838
2839        fn locates(&self, _segment: usize) -> bool {
2840            true
2841        }
2842
2843        fn location(&self, segment: usize) -> Option<SearchEntry> {
2844            Some(
2845                SearchEntry::new(format!("Paragraph {}", segment.saturating_add(1)), "").invoking(
2846                    tilo_actions::ActionAddress::new(format!("book.location:{segment}"), "open"),
2847                ),
2848            )
2849        }
2850    }
2851
2852    #[test]
2853    fn a_match_in_a_body_is_the_place_it_is_in() {
2854        let body = tilo_resource::SearchBody::new(Arc::new(Chapter(vec![
2855            "It was a dark and stormy night.",
2856            "The lighthouse keeper counted the waves.",
2857        ])));
2858        let search = ResourceSearch::new(
2859            "Library",
2860            "so.tilo.test",
2861            Arc::new(IndexedEntries::built(
2862                vec![
2863                    SearchEntry::new("Storm Tales", "")
2864                        .invoking(tilo_actions::ActionAddress::new("book:1", "open"))
2865                        .grouped("Books")
2866                        .with_body(body.clone()),
2867                ],
2868                0,
2869            )),
2870        );
2871        let hits = search.search("lighthouse keeper");
2872        let place = hits
2873            .iter()
2874            .find(|hit| hit.title == "Paragraph 2")
2875            .expect("the paragraph the words are in is a hit");
2876        assert_eq!(
2877            place.action_address(),
2878            Some(tilo_actions::ActionAddress::new("book.location:1", "open"))
2879        );
2880        assert_eq!(place.kind, "Books", "a location is listed with its book");
2881        assert!(
2882            search
2883                .search("storm tales")
2884                .iter()
2885                .any(|hit| hit.title == "Storm Tales"),
2886            "the book itself is found by its title"
2887        );
2888        assert_eq!(body, body.clone(), "a body is the text it shares");
2889    }
2890
2891    #[test]
2892    fn a_field_ranks_by_its_own_weight() {
2893        use tilo_resource::SearchWeight;
2894        let search = ResourceSearch::new(
2895            "Mail",
2896            "so.tilo.test",
2897            Arc::new(IndexedEntries::built(
2898                vec![
2899                    SearchEntry::new("Lunch plans", "")
2900                        .field("[email protected]", SearchWeight::Metadata),
2901                    SearchEntry::new("Quarterly report", "")
2902                        .field("[email protected]", SearchWeight::Identity),
2903                ],
2904                0,
2905            )),
2906        );
2907        let hits = search.search("[email protected]");
2908        assert_eq!(hits.len(), 2, "both are found by the address");
2909        assert_eq!(
2910            hits[0].title, "Quarterly report",
2911            "a match in an identity outranks one in metadata"
2912        );
2913    }
2914}
2915
2916// Compiled only into native tests: the file's own `cfg` says so.
2917mod publication_tests;