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/// Whether `entries` are indexed away from the frame that asks for them.
1376///
1377/// Natively, a few short entries index in that frame: tantivy costs tens of
1378/// microseconds, and a build that landed at once saves the search a look
1379/// again. A page moves every build off the frame — its one thread runs the
1380/// toolkit's background queue between frames, so a build in the frame holds
1381/// the palette's first paint until it finishes. A set large enough to be
1382/// slow, or one carrying prose bodies, moves off the frame on either host.
1383fn indexes_off_frame(entries: &[SearchEntry]) -> bool {
1384    // Tests drive both rules on the host they run on.
1385    #[cfg(test)]
1386    if let Some(forced) = FORCE_OFF_FRAME.with(std::cell::Cell::get) {
1387        return forced;
1388    }
1389    crate::runner::IN_A_PAGE
1390        || entries.len() >= OFF_THREAD_ENTRIES
1391        || entries.iter().any(|entry| entry.body.is_some())
1392}
1393
1394#[cfg(test)]
1395thread_local! {
1396    /// Tests set whether every build moves off the frame, as a page's does.
1397    pub(crate) static FORCE_OFF_FRAME: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
1398}
1399
1400/// What the resource's search entries are of: a revision of the resource
1401/// and the entries its surfaces show.
1402#[derive(PartialEq)]
1403struct SearchState {
1404    revision: (u64, u64),
1405    shown: Vec<SearchEntry>,
1406}
1407
1408/// An index search answers from, and the state it is of: none for the empty
1409/// index answering before the first one arrives.
1410struct AnsweringIndex {
1411    of: Option<SearchState>,
1412    indexed: Arc<IndexedEntries>,
1413}
1414
1415/// An index being built away from the UI thread.
1416struct BuildingIndex {
1417    of: SearchState,
1418    generation: u64,
1419    arriving: std::sync::mpsc::Receiver<IndexedEntries>,
1420}
1421
1422/// The resource's search index, built only when someone searches and at
1423/// most one at a time.
1424///
1425/// Every state is given its index's generation when first seen, before
1426/// anything is built: the action directory names that generation on every
1427/// frame, and the index a later search builds carries it.
1428#[derive(Default)]
1429pub(crate) struct SearchCache {
1430    answering: Option<AnsweringIndex>,
1431    building: Option<BuildingIndex>,
1432    /// The newest state nothing is built of yet, and its generation: built
1433    /// when the next search asks once no build is in flight.
1434    wanted: Option<(SearchState, u64)>,
1435    /// Entries a showing search waits on, and their state and generation:
1436    /// indexed once the build in flight lands.
1437    queued: Option<(SearchState, u64, Vec<SearchEntry>)>,
1438    /// A search is showing that waits on its surfaces' answers.
1439    watching: bool,
1440    /// The generation that holds the surfaces' answers to the latest query:
1441    /// the waiting search shows what an index this new or newer finds.
1442    answered: Option<u64>,
1443    next_generation: u64,
1444    /// How many indexes of entries have been started.
1445    #[cfg(test)]
1446    builds: usize,
1447}
1448
1449impl SearchCache {
1450    fn reserve_generation(&mut self) -> u64 {
1451        let generation = self.next_generation;
1452        self.next_generation = generation.wrapping_add(1);
1453        generation
1454    }
1455
1456    /// Answer from an index that finished building since last asked, and
1457    /// start the one a waiting search wants once nothing is in flight.
1458    fn receive(&mut self) {
1459        self.land();
1460        if self.building.is_none()
1461            && let Some((state, generation, entries)) = self.queued.take()
1462        {
1463            self.build(state, generation, entries);
1464        }
1465    }
1466
1467    fn land(&mut self) {
1468        let Some(building) = self.building.take() else {
1469            return;
1470        };
1471        match building.arriving.try_recv() {
1472            Ok(indexed) => {
1473                self.answering = Some(AnsweringIndex {
1474                    of: Some(building.of),
1475                    indexed: Arc::new(indexed),
1476                });
1477            }
1478            Err(std::sync::mpsc::TryRecvError::Empty) => self.building = Some(building),
1479            // A lost build: its state is wanted afresh when next asked.
1480            Err(std::sync::mpsc::TryRecvError::Disconnected) => {}
1481        }
1482    }
1483
1484    /// The generation of the index that answers for `state`, given now if
1485    /// `state` is new. Builds nothing.
1486    fn generation_for(&mut self, state: SearchState) -> u64 {
1487        self.receive();
1488        if let Some(answering) = &self.answering
1489            && answering.of.as_ref() == Some(&state)
1490        {
1491            return answering.indexed.generation;
1492        }
1493        if let Some(building) = &self.building
1494            && building.of == state
1495        {
1496            return building.generation;
1497        }
1498        if let Some((wanted, generation)) = &self.wanted
1499            && *wanted == state
1500        {
1501            return *generation;
1502        }
1503        let generation = self.reserve_generation();
1504        self.wanted = Some((state, generation));
1505        generation
1506    }
1507
1508    /// Index `entries`, of `state`: in this frame when they are few, else
1509    /// away from the UI thread. The index search answers from now.
1510    ///
1511    /// A page never indexes in the frame that asks for it, however few the
1512    /// entries. Its one thread runs the toolkit's
1513    /// [`background`](ui_core::background) queue between frames, so a build
1514    /// in the frame holds the palette's first paint until it finishes, and
1515    /// indexing a window's content into tantivy there is what the page
1516    /// desk measured as the 14–57 s between typing and the first row, per
1517    /// app, moving with whichever window was opened last. A background job
1518    /// runs after the frame returns, so the ready rows paint at once and the
1519    /// content's rows join them when the index is built
1520    /// ([`SearchCache::building`], which the search showing polls for).
1521    fn build(
1522        &mut self,
1523        state: SearchState,
1524        generation: u64,
1525        entries: Vec<SearchEntry>,
1526    ) -> Arc<IndexedEntries> {
1527        #[cfg(test)]
1528        {
1529            self.builds = self.builds.saturating_add(1);
1530        }
1531        if indexes_off_frame(&entries) {
1532            let (send, arriving) = std::sync::mpsc::channel();
1533            let building = entries.clone();
1534            let spawned = ui_core::background::Background::current().spawn(
1535                "resource-search-index",
1536                move || {
1537                    let _ = send.send(IndexedEntries::built(building, generation));
1538                },
1539            );
1540            if spawned.is_ok() {
1541                self.building = Some(BuildingIndex {
1542                    of: state,
1543                    generation,
1544                    arriving,
1545                });
1546                return self.answering();
1547            }
1548        }
1549        let indexed = Arc::new(IndexedEntries::built(entries, generation));
1550        self.answering = Some(AnsweringIndex {
1551            of: Some(state),
1552            indexed: Arc::clone(&indexed),
1553        });
1554        indexed
1555    }
1556
1557    /// Whether an index is being built off the frame for a state this cache
1558    /// was asked for: the search showing looks again shortly, so the rows the
1559    /// build finds are drawn when it lands.
1560    pub(crate) fn building(&mut self) -> bool {
1561        self.receive();
1562        self.building.is_some()
1563    }
1564
1565    /// The index holding the surfaces' answers to the waiting search, once
1566    /// it is in.
1567    fn answer(&mut self) -> Option<Arc<IndexedEntries>> {
1568        self.receive();
1569        let wanted = self.answered?;
1570        self.answering
1571            .as_ref()
1572            .filter(|answering| answering.of.is_some() && answering.indexed.generation >= wanted)
1573            .map(|answering| Arc::clone(&answering.indexed))
1574    }
1575
1576    /// The index search answers from now: the empty one until a first
1577    /// arrives.
1578    fn answering(&mut self) -> Arc<IndexedEntries> {
1579        if let Some(answering) = &self.answering {
1580            return Arc::clone(&answering.indexed);
1581        }
1582        let generation = self.reserve_generation();
1583        let indexed = Arc::new(IndexedEntries::built(Vec::new(), generation));
1584        self.answering = Some(AnsweringIndex {
1585            of: None,
1586            indexed: Arc::clone(&indexed),
1587        });
1588        indexed
1589    }
1590}
1591
1592// The root is the app's view plus what the framework adds: it keeps the
1593// state current, publishes the resource, forwards edits and indexes search.
1594// Everything else — layout, input, traversal, operation admission — is the
1595// view's own, forwarded by the decorator.
1596impl RootModel for ResourceRoot {
1597    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);
1598
1599    fn view(&self) -> &(dyn Widget + 'static) {
1600        match &self.unreadable {
1601            Some((_, shown)) => shown.as_ref(),
1602            None => self.content.as_ref(),
1603        }
1604    }
1605
1606    fn view_mut(&mut self) -> &mut (dyn Widget + 'static) {
1607        match &mut self.unreadable {
1608            Some((_, shown)) => shown.as_mut(),
1609            None => self.content.as_mut(),
1610        }
1611    }
1612
1613    fn paints_ground(&self) -> bool {
1614        self.ground
1615    }
1616
1617    fn take_structure_dirty(&mut self) -> bool {
1618        let swapped = std::mem::take(&mut self.swapped);
1619        self.view_mut().take_structure_dirty() || swapped
1620    }
1621
1622    // Input is forwarded unchanged; model, storage and policy wakes remain
1623    // owned by the runner and still request their normal synchronization.
1624    fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
1625        self.content.view_only_scroll(event)
1626    }
1627
1628    // The root adds no structure of its own beyond swapping its view for
1629    // the unreadable state: a change its view can describe stays described.
1630    fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
1631        let change = self.view_mut().take_structure_change();
1632        if std::mem::take(&mut self.swapped) {
1633            ui_core::widget::StructureChange::Rebuild
1634        } else {
1635            change
1636        }
1637    }
1638
1639    fn record_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
1640        let mut owners = self.storage.person_owners();
1641        owners.extend(self.content.record_owners());
1642        owners
1643    }
1644
1645    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
1646        if self.source.is_connection() {
1647            // The session keeps the record: the view syncs from the reading
1648            // it publishes, and the document the host lends this pass holds
1649            // nothing of it (`one-app-model.md` §4). Everything the view
1650            // binds is still synced, over the state this root now holds.
1651            self.sync_remote_state();
1652            let empty = AutoCommit::new();
1653            self.sync_over(&empty, &automerge::ROOT);
1654            return;
1655        }
1656        let (state, path) = ui_core::doc::document_ref(state, path);
1657        self.sync_over(state, path);
1658    }
1659
1660    fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ui_core::automerge::ObjId) {
1661        self.typed.apply_settlements(edits);
1662        self.content.settle(edits, path);
1663        self.storage.settle();
1664    }
1665
1666    fn draw(
1667        &mut self,
1668        scene: &mut Scene,
1669        transform: Affine,
1670        text: &mut TextRenderer,
1671        theme: &Theme,
1672        presence: &PresenceCtx,
1673    ) {
1674        self.paint_ground(scene, transform, theme, presence);
1675        self.content.draw(scene, transform, text, theme, presence);
1676        // The window drew: what its bar says of the save is seen.
1677        self.save.seen();
1678    }
1679
1680    fn needs_redraw(&self) -> bool {
1681        self.native_presentation.needs_redraw()
1682            || self.content.needs_redraw()
1683            || self.save.needs_redraw(ui_core::time::now())
1684    }
1685
1686    fn save_indicator(&self) -> &super::saves::SaveIndicator {
1687        &self.save
1688    }
1689
1690    #[cfg(test)]
1691    fn save_indicator_mut(&mut self) -> &mut super::saves::SaveIndicator {
1692        &mut self.save
1693    }
1694
1695    // A screenshot harness reaches the root to stage a scenario's view state
1696    // (`framework::stage`).
1697    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1698        self
1699    }
1700
1701    // The resource is the same for every participant (an agent task sees and
1702    // invokes the same actions); only the content's tree is per participant.
1703
1704    // The undo boundary of a surface's immediate actions this event; the
1705    // host records one step per event and author.
1706
1707    // A surface's step stays open for the actions that continue it (the
1708    // letters of a word), which report no boundary of their own.
1709
1710    /// Resource and app presentation doors publish effect/view actions; their
1711    /// operation types must be in the retained handler table or publication
1712    /// withdraws the whole component surface.
1713    fn resource_operation_handlers(&self) -> HashSet<String> {
1714        let mut handlers = ui_core::host_operation::resource_operation_handlers(self.view());
1715        handlers.extend(
1716            crate::runner::operation_types()
1717                .iter()
1718                .map(|op| (*op).to_owned()),
1719        );
1720        handlers.insert(super::history::VIEW_TYPE.to_owned());
1721        handlers.insert(super::person::HISTORY_TYPE.to_owned());
1722        handlers.insert(super::record::RECORD_ACTION_TYPE.to_owned());
1723        handlers.insert(super::chain::CHAIN_ACTION_TYPE.to_owned());
1724        handlers.insert(tilo_resource::share::SHARE_TYPE.to_owned());
1725        handlers.insert(tilo_resource::share::SHARE_INVITE_TYPE.to_owned());
1726        if self.stages.opens() {
1727            handlers.insert(OPEN_TYPE.to_owned());
1728        }
1729        // Opening another app is routed by the host to the workspace
1730        // broker, whichever resource offers it.
1731        handlers.insert(tilo_actions::remote::ApplicationLaunch::TYPE.to_owned());
1732        // A resource reads itself when it says it does, and its items from
1733        // its record's table when it does not (`offer_item_reads`): the read
1734        // it publishes is always one it answers.
1735        if self.resource.readable || self.resource.record_table.is_some() {
1736            handlers.insert(READ_TYPE.to_owned());
1737        }
1738        if self.resource.facts {
1739            handlers.insert(facts::FACTS_TYPE.to_owned());
1740        }
1741        handlers.extend(
1742            self.typed
1743                .effect_operations()
1744                .into_iter()
1745                .map(str::to_owned),
1746        );
1747        for spec in (self.resource.local_operations)()
1748            .into_iter()
1749            .chain(self.presentation.iter().cloned())
1750        {
1751            if let Some(type_id) = &spec.operation_type {
1752                handlers.insert(type_id.clone());
1753            }
1754        }
1755        handlers.extend(self.nested_views().iter().cloned());
1756        handlers
1757    }
1758
1759    fn poll_resource_operation(
1760        &mut self,
1761        invocation: tilo_actions::InvocationId,
1762        cancellation_requested: bool,
1763        doc: &AutoCommit,
1764        path: &ui_core::automerge::ObjId,
1765    ) -> Option<tilo_actions::InvocationUpdate> {
1766        if let Some((update, copied)) = self.share.poll(invocation, cancellation_requested) {
1767            if copied && let tilo_actions::InvocationUpdate::Succeeded { output, .. } = &update {
1768                self.dispatch
1769                    .note_landed(tilo_resource::share::SHARE_LINK, output);
1770            }
1771            return Some(update);
1772        }
1773        self.typed
1774            .poll_effect(
1775                invocation,
1776                cancellation_requested,
1777                effect_view(&mut self.unreadable, &mut self.content),
1778            )
1779            .or_else(|| {
1780                with_app_view_mut(&mut self.content, |view| {
1781                    ui_core::host_operation::poll_resource_operation(
1782                        view,
1783                        invocation,
1784                        cancellation_requested,
1785                        doc,
1786                        path,
1787                    )
1788                })
1789            })
1790    }
1791
1792    fn start_operation(
1793        &mut self,
1794        op: &ui_core::host_operation::ResourceOperation<'_>,
1795        doc: &mut ui_core::widget::Edits<'_>,
1796        path: &ui_core::automerge::ObjId,
1797    ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
1798        // A view of a resource the system asked for: show that item first, so
1799        // the picture is of the item and not of whatever the window happened
1800        // to show. `render` is the app's answer to "show me this resource".
1801        if let Some(request) = op.unscoped()
1802            && request.type_id == crate::runner::RENDER_TYPE
1803            && !request.payload.is_empty()
1804            && let Some(anchor) = crate::runner::rendered_anchor(request.payload)
1805        {
1806            self.stages.open(&anchor)?;
1807        }
1808        if let Some(output) = with_app_view_mut(&mut self.content, |view| {
1809            self.native_presentation.start_operation(op, doc, view)
1810        })? {
1811            return Ok(Some(
1812                ui_core::host_operation::ResourceOperationStart::Completed(output),
1813            ));
1814        }
1815        if (self.resource.readable || self.resource.record_table.is_some())
1816            && let Some(request) = op.unscoped()
1817            && request.type_id == READ_TYPE
1818        {
1819            let output = read(
1820                self.typed.as_ref(),
1821                &self.resource,
1822                &*self.typed.state(),
1823                doc,
1824                request.payload,
1825            )?;
1826            return Ok(Some(
1827                ui_core::host_operation::ResourceOperationStart::Completed(output),
1828            ));
1829        }
1830        if self.resource.facts
1831            && let Some(request) = op.unscoped()
1832            && request.type_id == facts::FACTS_TYPE
1833        {
1834            let output = facts::answer(
1835                self.typed.as_ref(),
1836                &*self.typed.state(),
1837                doc,
1838                request.payload,
1839            )?;
1840            return Ok(Some(
1841                ui_core::host_operation::ResourceOperationStart::Completed(output),
1842            ));
1843        }
1844        if let Some(request) = op.unscoped()
1845            && super::share_link::handles(request.type_id)
1846        {
1847            return self.start_share(request.invocation, request.payload);
1848        }
1849        if let Some(request) = op.unscoped()
1850            && request.type_id == OPEN_TYPE
1851        {
1852            let args: serde_json::Value =
1853                tilo_actions::decode_json(request.payload).map_err(|error| error.to_string())?;
1854            let anchor = args
1855                .get(OPEN_ANCHOR)
1856                .and_then(serde_json::Value::as_str)
1857                .ok_or_else(|| "open needs the anchor of an item".to_owned())?;
1858            self.stages.open(anchor)?;
1859            return Ok(Some(
1860                ui_core::host_operation::ResourceOperationStart::Completed(
1861                    tilo_actions::ActionOutput::default(),
1862                ),
1863            ));
1864        }
1865        if let Some(output) = self.start_record_operation(op, doc)? {
1866            return Ok(Some(
1867                ui_core::host_operation::ResourceOperationStart::Completed(output),
1868            ));
1869        }
1870        // An effect reads the resource as the document stands, though no
1871        // frame has run since a collaborator's change arrived.
1872        if let Err(error) = self.typed.sync(doc) {
1873            tracing::warn!(%error, kind = self.resource.kind, "resource unreadable");
1874        }
1875        if let Some(started) = self.typed.start_effect(
1876            op,
1877            doc.doc(),
1878            effect_view(&mut self.unreadable, &mut self.content),
1879        )? {
1880            return Ok(Some(started));
1881        }
1882        if let Some(started) =
1883            ui_core::host_operation::start_operation(self.view_mut(), op, doc, path)?
1884        {
1885            return Ok(Some(started));
1886        }
1887        Ok(self.start_catalogue_view(op))
1888    }
1889}
1890
1891impl ResourceRoot {
1892    /// The record-level requests every framework app answers the same way:
1893    /// its history at a version, an operation on one of its records or its
1894    /// chain of parts, and the person's own record history.
1895    fn start_record_operation(
1896        &self,
1897        op: &ui_core::host_operation::ResourceOperation<'_>,
1898        doc: &mut ui_core::widget::Edits<'_>,
1899    ) -> Result<Option<tilo_actions::ActionOutput>, String> {
1900        let Some(request) = op.unscoped() else {
1901            return Ok(None);
1902        };
1903        let output = if request.type_id == super::history::VIEW_TYPE {
1904            super::history::answer(doc, request.payload)?
1905        } else if request.type_id == super::record::RECORD_ACTION_TYPE {
1906            self.storage
1907                .record_action(self.dispatch.me(), request.payload)?
1908        } else if request.type_id == super::chain::CHAIN_ACTION_TYPE {
1909            self.storage
1910                .chain_action(self.dispatch.me(), request.payload, doc)?
1911        } else if request.type_id == super::person::HISTORY_TYPE {
1912            self.storage.person_history(request.payload)?
1913        } else {
1914            return Ok(None);
1915        };
1916        Ok(Some(output))
1917    }
1918
1919    /// What the resource's search entries are of now, and the surfaces'
1920    /// query sinks. What the surfaces show (a page's text, live rows)
1921    /// changes on its own, beside the resource's revision.
1922    fn search_state(&self) -> (SearchState, Vec<super::publishing::SearchQueries>) {
1923        let mut shown = Vec::new();
1924        let mut queries = Vec::new();
1925        self.surfaces(&mut |surface| {
1926            shown.extend(surface.search_entries());
1927            queries.extend(surface.search_queries());
1928        });
1929        let state = SearchState {
1930            revision: self.version(),
1931            shown,
1932        };
1933        (state, queries)
1934    }
1935
1936    /// The index search answers `state` from, building it if nothing is. A
1937    /// large set of entries (a browsing history, a store catalogue) or one
1938    /// with prose bodies (whole books) is indexed away from the UI thread,
1939    /// one build at a time: until the index of `state` arrives, search
1940    /// answers from the one before it, and states that come and go while a
1941    /// build is in flight cost only the newest one's build after it.
1942    fn current_search_index(&self, state: SearchState) -> Arc<IndexedEntries> {
1943        let mut cache = self.search.borrow_mut();
1944        let generation = cache.generation_for(state);
1945        if let Some(answering) = &cache.answering
1946            && answering.of.is_some()
1947            && answering.indexed.generation == generation
1948        {
1949            return Arc::clone(&answering.indexed);
1950        }
1951        if cache.building.is_some() {
1952            return cache.answering();
1953        }
1954        let Some((state, generation)) = cache.wanted.take() else {
1955            return cache.answering();
1956        };
1957        let entries = self.entries_of(&state);
1958        cache.build(state, generation, entries)
1959    }
1960
1961    /// Whether the surfaces' secrets are unlocked: locked when any surface
1962    /// says so, unlocked when one says so and none disagrees, else unsaid.
1963    fn secrets_unlocked(&self) -> Option<bool> {
1964        let mut said = None;
1965        self.surfaces(&mut |surface| match surface.secrets_unlocked() {
1966            Some(false) => said = Some(false),
1967            Some(true) if said.is_none() => said = Some(true),
1968            _ => {}
1969        });
1970        said
1971    }
1972
1973    /// Everything search finds in `state`: the resource's entries and what
1974    /// its surfaces show.
1975    fn entries_of(&self, state: &SearchState) -> Vec<SearchEntry> {
1976        let mut entries = self.typed.search(&*self.typed.state());
1977        entries.extend(state.shown.iter().cloned());
1978        entries
1979    }
1980
1981    /// A search that waits on its surfaces' answers (a server still
1982    /// looking) gets an index of what they show once every one of them has
1983    /// answered the latest query.
1984    fn follow_search(&self) {
1985        if !self.search.borrow().watching {
1986            return;
1987        }
1988        let mut answering = false;
1989        self.surfaces(&mut |surface| {
1990            answering |= surface
1991                .search_queries()
1992                .is_some_and(|queries| queries.answering());
1993        });
1994        if answering {
1995            return;
1996        }
1997        let (state, _) = self.search_state();
1998        let mut cache = self.search.borrow_mut();
1999        cache.watching = false;
2000        let generation = cache.generation_for(state);
2001        cache.answered = Some(generation);
2002        if cache.building.is_some() {
2003            // Indexed once the build in flight lands.
2004            if let Some((state, wanted)) = cache.wanted.take() {
2005                let entries = self.entries_of(&state);
2006                cache.queued = Some((state, wanted, entries));
2007            }
2008            return;
2009        }
2010        if let Some((state, wanted)) = cache.wanted.take() {
2011            let entries = self.entries_of(&state);
2012            cache.build(state, wanted, entries);
2013        }
2014    }
2015}
2016
2017impl ResourceRoot {
2018    fn start_catalogue_view(
2019        &self,
2020        op: &ui_core::host_operation::ResourceOperation<'_>,
2021    ) -> Option<ui_core::host_operation::ResourceOperationStart> {
2022        // View openers from the resource/presentation catalogue and from its
2023        // nested resources (e.g. a rich-text body's `tilo.rich-text.view`
2024        // editor/compact) are already mounted; accept them so agents and
2025        // Ctrl+K can switch without a missing-handler fault.
2026        let request = op.unscoped()?;
2027        if request.kind != tilo_actions::OperationKind::View {
2028            return None;
2029        }
2030        let known = (self.resource.local_operations)()
2031            .into_iter()
2032            .chain(self.presentation.iter().cloned())
2033            .any(|spec| spec.operation_type.as_deref() == Some(request.type_id))
2034            || self.nested_views().contains(request.type_id);
2035        known.then(|| {
2036            ui_core::host_operation::ResourceOperationStart::Completed(
2037                tilo_actions::ActionOutput::default(),
2038            )
2039        })
2040    }
2041
2042    /// Start minting a link to one of the resources this root publishes.
2043    fn start_share(
2044        &mut self,
2045        invocation: tilo_actions::InvocationId,
2046        payload: &[u8],
2047    ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
2048        let record = self
2049            .run
2050            .launched
2051            .record
2052            .as_ref()
2053            .map(|record| record.as_str().to_owned());
2054        let publisher = super::share_link::Publisher {
2055            app: self.dispatch.app_id(),
2056            record: record.as_deref(),
2057            root: self.dispatch.resource_id(),
2058        };
2059        let may_invite = self.dispatch.may_invite();
2060        let started = self
2061            .share
2062            .start(invocation, payload, &publisher, may_invite)?;
2063        Ok(Some(match started {
2064            Some(minted) => {
2065                if minted.copied {
2066                    self.dispatch
2067                        .note_landed(tilo_resource::share::SHARE_LINK, &minted.output);
2068                }
2069                ui_core::host_operation::ResourceOperationStart::Completed(minted.output)
2070            }
2071            None => ui_core::host_operation::ResourceOperationStart::Accepted,
2072        }))
2073    }
2074}
2075
2076// The root's own clients for the action door (minting links, opening
2077// references) share the app's collaboration connection with whatever the
2078// view keeps there.
2079impl ui_core::widget::Collaborative for ResourceRoot {
2080    fn take_collab_requests(&mut self) -> Vec<ui_core::collaboration::CollabRequest> {
2081        let mut requests =
2082            ui_core::widget::Collaborative::take_collab_requests(self.share.remote_mut());
2083        if let Some(mut door) = self.dispatch.opener.door() {
2084            requests.extend(ui_core::widget::Collaborative::take_collab_requests(
2085                &mut door,
2086            ));
2087        }
2088        with_app_view_mut(&mut self.content, |view| {
2089            if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2090                requests.extend(content.take_collab_requests());
2091            }
2092        });
2093        requests
2094    }
2095
2096    fn handle_collab_event(&mut self, event: &ui_core::collaboration::CollabEvent) {
2097        ui_core::widget::Collaborative::handle_collab_event(self.share.remote_mut(), event);
2098        if let Some(mut door) = self.dispatch.opener.door() {
2099            ui_core::widget::Collaborative::handle_collab_event(&mut door, event);
2100        }
2101        with_app_view_mut(&mut self.content, |view| {
2102            if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2103                content.handle_collab_event(event);
2104            }
2105        });
2106    }
2107}
2108
2109impl RootSearch for ResourceRoot {
2110    fn search_providers(&self) -> Vec<Box<dyn SearchProvider>> {
2111        // The index is rebuilt only when the resource or what its surfaces
2112        // show moved since the last one.
2113        let (state, queries) = self.search_state();
2114        let indexed = self.current_search_index(state);
2115        // The app's one search source: what its content widgets show beside
2116        // the resource reaches it as their surfaces' entries, never as a
2117        // provider of their own.
2118        let mut source = ResourceSearch::new(self.resource.label, self.dispatch.app_id(), indexed);
2119        source.scopes = self.typed.search_scopes(&*self.typed.state());
2120        source.queries = queries;
2121        // A person who may not read the record is found nothing of it.
2122        source.readable = self.dispatch.may_read();
2123        source.live = Some(LiveSearch {
2124            cache: Rc::clone(&self.search),
2125            answered: std::cell::RefCell::new(None),
2126        });
2127        source.opens = self
2128            .stages
2129            .opens()
2130            .then(|| tilo_actions::ActionAddress::new(self.dispatch.resource_id(), "open"));
2131        vec![Box::new(source)]
2132    }
2133
2134    /// The one source [`Self::search_providers`] makes, named with the
2135    /// generation its index has or will have once someone searches: the
2136    /// action directory reads this on every frame, and indexing whole books
2137    /// per keystroke for a search nobody runs is what it must not cost.
2138    fn search_provider_descriptors(
2139        &self,
2140    ) -> Vec<ui_core::search::capability::SearchProviderDescriptor> {
2141        let (state, _) = self.search_state();
2142        let generation = self.search.borrow_mut().generation_for(state);
2143        vec![ui_core::search::capability::SearchProviderDescriptor::new(
2144            self.resource.label,
2145            Some(generation),
2146            // `ResourceSearch` keeps the provider default.
2147            ui_core::widget::ActionDirectoryInvocationDomain::Unsupported,
2148        )]
2149    }
2150
2151    /// The resource's entries and what its surfaces show, as of the same
2152    /// generation the action directory names, with whether the surfaces'
2153    /// secrets are unlocked.
2154    fn search_index_state(&self) -> Option<ui_core::search::index_publication::IndexState> {
2155        let (state, _) = self.search_state();
2156        let generation = self.search.borrow_mut().generation_for(state);
2157        Some(ui_core::search::index_publication::IndexState {
2158            generation,
2159            secrets_unlocked: self.secrets_unlocked(),
2160        })
2161    }
2162
2163    /// Every entry a hit can open, as the session's persistent index keeps
2164    /// it: short metadata only, never a prose body. A view that keeps its
2165    /// entries out of the index
2166    /// ([`Publishes::search_entries_while_running`](super::Publishes::search_entries_while_running))
2167    /// contributes none.
2168    fn search_index_entries(&self) -> Vec<ui_core::search::index_publication::IndexEntry> {
2169        let mut entries = self.typed.search(&*self.typed.state());
2170        self.surfaces(&mut |surface| {
2171            if surface.kept_in_search_index() {
2172                entries.extend(surface.search_entries());
2173            }
2174        });
2175        entries.iter().filter_map(index_entry).collect()
2176    }
2177
2178    /// A picked hit opens the app at its anchor: the views registered with
2179    /// `on_open` show it, then any hit the content provided reaches it.
2180    fn activate_search_result(
2181        &mut self,
2182        action_id: &str,
2183        doc: &mut ui_core::widget::Edits<'_>,
2184        path: &ModelPath,
2185    ) {
2186        if self.stages.open(action_id).is_err() {
2187            with_app_view_mut(&mut self.content, |view| {
2188                ui_core::search::capability::activate_search_result(view, action_id, doc, path);
2189            });
2190        }
2191    }
2192}
2193
2194impl RootPublishes for ResourceRoot {
2195    fn resources(&self, doc: &AutoCommit) -> Vec<ResourceSpec> {
2196        RootPublishes::resources_for(self, None, doc)
2197    }
2198    fn shared_resources_for(
2199        &self,
2200        participant: Option<&tilo_actions::ParticipantId>,
2201        doc: &AutoCommit,
2202    ) -> Option<Vec<ui_core::published::SharedResource>> {
2203        self.shared_resources(participant, doc)
2204    }
2205    fn resources_for(
2206        &self,
2207        participant: Option<&tilo_actions::ParticipantId>,
2208        doc: &AutoCommit,
2209    ) -> Vec<ResourceSpec> {
2210        // What surfaces bind to where someone is, they bind to this
2211        // participant's view.
2212        let _viewer = participant
2213            .cloned()
2214            .map(ui_core::invocation_scope::ViewerScope::enter);
2215        let mut resource = self.current_publication(doc);
2216        // The content may add live external entities beside the resource,
2217        // never a document action: those are the resource's own. One naming
2218        // the resource joins it rather than publishing it twice.
2219        let mut extras = Vec::new();
2220        for extra in ui_core::resources::resources_for(self.app_view(), participant, doc) {
2221            let extra = outside_document(extra);
2222            if extra.id == resource.id {
2223                join(&mut resource, extra);
2224            } else {
2225                extras.push(extra);
2226            }
2227        }
2228        if self.stages.opens()
2229            && !resource
2230                .actions
2231                .iter()
2232                .any(|existing| existing.id == "open")
2233        {
2234            resource = resource.with_action(open_spec());
2235        }
2236        if self.stages.opens() {
2237            offer_item_open(self.resource.opens_as_item, &mut resource);
2238        }
2239        if let Some(table) = self.resource.record_table {
2240            offer_item_reads(&mut resource, table, self.resource.kind);
2241        }
2242        let mut resources = vec![resource];
2243        resources.append(&mut extras);
2244        resources
2245    }
2246    fn resource(&self, doc: &AutoCommit, id: &str) -> Option<ResourceSpec> {
2247        RootPublishes::resource_for(self, None, doc, id)
2248    }
2249    /// A live row a surface is still reading.
2250    fn resource_pending(&self, id: &str) -> bool {
2251        let mut pending = false;
2252        self.surfaces(&mut |surface| pending = pending || surface.live_row_pending(id));
2253        pending
2254    }
2255    /// Resource `id` in the published tree, or one the tree leaves out: a
2256    /// child the resource resolves by id, or a live row a surface resolves.
2257    fn resource_for(
2258        &self,
2259        participant: Option<&tilo_actions::ParticipantId>,
2260        doc: &AutoCommit,
2261        id: &str,
2262    ) -> Option<ResourceSpec> {
2263        let _viewer = participant
2264            .cloned()
2265            .map(ui_core::invocation_scope::ViewerScope::enter);
2266        // A change from another window or participant may not have reached
2267        // this view yet: an item it removed is gone, one it made is there.
2268        if let Some(published) = find(&[self.current_publication(doc)], id) {
2269            return Some(published);
2270        }
2271        let current = if self.typed.follows(doc) {
2272            None
2273        } else {
2274            self.typed.read(doc).ok()
2275        };
2276        let state = current.unwrap_or_else(|| self.typed.state());
2277        let mut child = self.typed.child(&*state, doc, id);
2278        if self.resource.readable
2279            && let Some(child) = &mut child
2280        {
2281            offer_document_reads(child, &|id| self.typed.document_child(&*state, id));
2282        }
2283        let mut row = None;
2284        self.surfaces(&mut |surface| {
2285            if row.is_none() {
2286                row = surface.live_row(id).map(super::LiveRow::into_resource);
2287            }
2288        });
2289        // An item past the published page is resolved on its own, and the row
2290        // a surface shows for it joins it as it does in the page.
2291        let resolved = match (child, row) {
2292            (Some(mut child), Some(row)) => {
2293                join(&mut child, row);
2294                Some(child)
2295            }
2296            (child, row) => child.or(row),
2297        };
2298        if let Some(mut resolved) = resolved {
2299            self.bind_row_declarations(std::slice::from_mut(&mut resolved));
2300            if self.stages.opens() {
2301                offer_item_open(self.resource.opens_as_item, &mut resolved);
2302            }
2303            return Some(resolved);
2304        }
2305        // Last, what the content resolves by id beyond what it publishes (a
2306        // place, a record past the published page), without a document
2307        // action: those are the resource's own.
2308        ui_core::resources::resource_for(self.app_view(), None, doc, id)
2309            .map(super::rows::resolved_outside_document)
2310    }
2311    fn document_action_result(
2312        &self,
2313        doc: &AutoCommit,
2314        address: &tilo_actions::ActionAddress,
2315        changes: &[ui_core::automerge::Patch],
2316    ) -> Option<serde_json::Value> {
2317        // The record's own actions, and its verbs bound on its items.
2318        if address.resource == self.dispatch.resource_id()
2319            || (self.resource.item_of)(address.resource.as_str())
2320        {
2321            if super::history::is_edit(&address.action) {
2322                return super::history::result(&address.action, doc, changes);
2323            }
2324            // What the action minted rides beside its own answer, the
2325            // resource's or else its surface's. An answer that is not an
2326            // object (an editor's summary naming the blocks it made) already
2327            // says what it made and is kept as it is.
2328            let minted = tilo_resource::take_minted();
2329            let answer = self
2330                .typed
2331                .answer(doc, &address.action, changes)
2332                .or_else(|| {
2333                    ui_core::resources::document_action_result(
2334                        self.app_view(),
2335                        doc,
2336                        address,
2337                        changes,
2338                    )
2339                });
2340            return match answer {
2341                _ if minted.is_empty() => answer,
2342                Some(serde_json::Value::Object(mut fields)) => {
2343                    fields.insert("minted".into(), minted.into());
2344                    Some(serde_json::Value::Object(fields))
2345                }
2346                Some(other) => Some(other),
2347                None => Some(serde_json::json!({ "minted": minted })),
2348            };
2349        }
2350        ui_core::resources::document_action_result(self.app_view(), doc, address, changes)
2351    }
2352}
2353
2354// The root owns the file whose history it shows beside the resource's (the
2355// runner checks the source it registered is this root's); everything else
2356// about History is its content's, as before.
2357impl ui_core::history_mode::capability::DocumentHistory for ResourceRoot {
2358    fn history_registration(
2359        &self,
2360    ) -> Result<
2361        Option<ui_core::history_mode::HistoryRegistration>,
2362        ui_core::history_mode::HistoryRegistrationError,
2363    > {
2364        match &self.run.file_history {
2365            Some((source, definition)) => {
2366                let scopes = definition.scopes.iter().map(|scope| {
2367                    ui_core::history_mode::HistorySourceRegistration::CheckedFiles {
2368                        scope: scope.key,
2369                        source: source.clone(),
2370                    }
2371                });
2372                ui_core::history_mode::HistoryRegistration::new(definition, scopes.collect())
2373                    .map(Some)
2374            }
2375            None => ui_core::history_mode::capability::history_registration(self.app_view()),
2376        }
2377    }
2378
2379    fn history_restore_readiness(&self, scope: &str) -> Result<(), String> {
2380        ui_core::widget::capability::capability::<
2381            dyn ui_core::history_mode::capability::DocumentHistory,
2382        >(self.app_view())
2383        .map_or(Ok(()), |content| content.history_restore_readiness(scope))
2384    }
2385
2386    fn document_history_definition(
2387        &self,
2388    ) -> Option<&'static ui_core::history_mode::HistoryDefinition> {
2389        ui_core::history_mode::capability::document_history_definition(self.app_view())
2390    }
2391
2392    fn physical_text_policy(
2393        &self,
2394        previous: Option<&tilo_actions::EncodedAction>,
2395        next: &tilo_actions::EncodedAction,
2396    ) -> (bool, bool) {
2397        self.typed.text_policy(previous, next)
2398    }
2399
2400    fn document_action_applied(
2401        &mut self,
2402        action: &tilo_actions::EncodedAction,
2403        before: &[ui_core::automerge::ChangeHash],
2404        doc: &ui_core::automerge::AutoCommit,
2405        path: &ui_core::automerge::ObjId,
2406    ) {
2407        // An action a participant took (an agent's verb) leaves them where
2408        // the resource says it lands, once the state follows it.
2409        if let Some(participant) = ui_core::invocation_scope::current_participant() {
2410            self.typed.note_applied(&participant, action);
2411        }
2412        with_app_view_mut(&mut self.content, |view| {
2413            if let Some(content) = ui_core::widget::capability::capability_mut::<
2414                dyn ui_core::history_mode::capability::DocumentHistory,
2415            >(view)
2416            {
2417                content.document_action_applied(action, before, doc, path);
2418            }
2419        });
2420    }
2421}
2422
2423impl ui_core::undo::CommittedActions for ResourceRoot {
2424    fn take_committed_action(&mut self) -> Option<Vec<ui_core::automerge::ChangeHash>> {
2425        self.dispatch.take_committed()
2426    }
2427    fn keeps_undo_step_open(&self) -> bool {
2428        self.dispatch.keeps_step_open()
2429    }
2430}
2431
2432impl RootApp for ResourceRoot {
2433    fn app_kind(&self) -> AppKind {
2434        AppKind::Editor
2435    }
2436
2437    fn app_title(&self) -> &str {
2438        self.title
2439    }
2440
2441    fn app_icon(&self) -> IconDescriptor {
2442        self.icon.clone()
2443    }
2444
2445    fn device_shell_operation(
2446        &mut self,
2447        operation: &ui_core::floating_surface::DeviceShellOperation,
2448    ) -> Result<serde_json::Value, String> {
2449        fn perform(
2450            widget: &mut dyn Widget,
2451            resource: std::any::TypeId,
2452            operation: &ui_core::floating_surface::DeviceShellOperation,
2453        ) -> Option<Result<serde_json::Value, String>> {
2454            if let Some(surface) = surface_mut(widget, resource)
2455                && surface.supports_device_shell()
2456            {
2457                return Some(surface.device_shell_operation(operation));
2458            }
2459            widget
2460                .children_mut()
2461                .into_iter()
2462                .find_map(|child| perform(child, resource, operation))
2463        }
2464        perform(self.content.as_mut(), self.resource.id, operation)
2465            .unwrap_or_else(|| Err("Terminal is not available".into()))
2466    }
2467
2468    fn request_device_shell(&mut self) -> bool {
2469        fn request(widget: &mut dyn Widget, resource: std::any::TypeId) -> bool {
2470            if let Some(surface) = surface_mut(widget, resource)
2471                && surface.request_device_shell()
2472            {
2473                return true;
2474            }
2475            widget
2476                .children_mut()
2477                .into_iter()
2478                .any(|child| request(child, resource))
2479        }
2480        request(self.content.as_mut(), self.resource.id)
2481    }
2482
2483    fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
2484        fn take(
2485            widget: &mut dyn Widget,
2486            resource: std::any::TypeId,
2487        ) -> Option<ui_core::widget::AppHostIntent> {
2488            if let Some(surface) = surface_mut(widget, resource)
2489                && let Some(intent) = surface.take_host_intent()
2490            {
2491                return Some(intent);
2492            }
2493            // A view whose app runs elsewhere asks through its own door.
2494            if let Some(intent) = ui_core::component_lifecycle::take_host_intent(widget) {
2495                return Some(intent);
2496            }
2497            widget
2498                .children_mut()
2499                .into_iter()
2500                .find_map(|child| take(child, resource))
2501        }
2502        take(self.content.as_mut(), self.resource.id)
2503    }
2504
2505    fn take_save_retry(&mut self) -> bool {
2506        self.save.take_record_retry()
2507    }
2508
2509    fn record_save_status(&mut self, status: RecordSaveStatus) {
2510        self.save.record(&status, ui_core::time::now());
2511        self.saved.set(status);
2512    }
2513
2514    // The workspace refused what this record sent, and the record withdrew it:
2515    // the view shows the stored state again, and the action reads as
2516    // refused, as one this device refused does.
2517    fn record_refused(&mut self, refusal: &ui_core::record::RecordRefusal) {
2518        self.dispatch.refuse_by_workspace(refusal);
2519        self.typed.represent();
2520    }
2521
2522    fn take_notices(&mut self) -> Vec<String> {
2523        self.dispatch.take_notices()
2524    }
2525
2526    fn record_said(&mut self, text: &str) {
2527        self.dispatch.kept(text);
2528    }
2529
2530    // A surface that keeps presentation of its own across restarts (the
2531    // open item, the caret) saves it as the window's.
2532    fn capture_application_view(&self) -> Result<Option<ApplicationLocalView>, ThawError> {
2533        capability::<dyn ApplicationViewState>(self.app_view())
2534            .map_or(Ok(None), ApplicationViewState::capture_application_view)
2535    }
2536
2537    fn restore_application_view(
2538        &mut self,
2539        local: &ApplicationLocalView,
2540        doc: &AutoCommit,
2541    ) -> Result<ApplicationFocusRestore, ThawError> {
2542        with_app_view_mut(&mut self.content, |view| {
2543            match capability_mut::<dyn ApplicationViewState>(view) {
2544                Some(view) => view.restore_application_view(local, doc),
2545                None => Err(ThawError::State(
2546                    "application-local restoration unsupported".into(),
2547                )),
2548            }
2549        })
2550    }
2551
2552    fn finish_application_view_restore(&mut self) -> Result<(), ThawError> {
2553        with_app_view_mut(&mut self.content, |view| {
2554            capability_mut::<dyn ApplicationViewState>(view).map_or(
2555                Ok(()),
2556                ApplicationViewState::finish_application_view_restore,
2557            )
2558        })
2559    }
2560}
2561
2562#[cfg(test)]
2563mod index_entry_tests;
2564
2565#[cfg(test)]
2566#[cfg(not(target_arch = "wasm32"))]
2567mod search_tests;
2568
2569/// What a window shows instead of the app's view while its record does not
2570/// read as the resource: what happened and what to do, in the person's
2571/// words, with the reason the record was refused (a path and a missing
2572/// value) behind the state's "Details".
2573fn unreadable_state(reason: &str) -> ui_core::widget::EmptyState {
2574    ui_core::widget::EmptyState::new(ui_core::i18n::I18nManager::lookup_plain(
2575        "framework-unreadable",
2576    ))
2577    .with_icon(ui_core::icons::Icon::CircleAlert)
2578    .with_message(ui_core::i18n::I18nManager::lookup_plain(
2579        "framework-unreadable-hint",
2580    ))
2581    .with_details(reason)
2582}
2583
2584/// The window of a record that did not read as the resource when it was
2585/// opened: why, instead of the app's view, and the resource's actions
2586/// published as unavailable for that reason.
2587pub(crate) struct Unopened {
2588    title: &'static str,
2589    icon: IconDescriptor,
2590    /// What it publishes: the resource's declared actions, every document
2591    /// one unavailable for the reason.
2592    resource: ResourceSpec,
2593    shown: ui_core::widget::EmptyState,
2594}
2595
2596impl Unopened {
2597    /// The window of a record that does not read as the resource of
2598    /// `kind` and `label`, whose actions are `declarations`.
2599    pub(crate) fn new(
2600        title: &'static str,
2601        icon: IconDescriptor,
2602        (kind, label): (&'static str, &'static str),
2603        declarations: &[tilo_resource::Declaration],
2604        reason: &str,
2605    ) -> Self {
2606        let mut resource =
2607            ResourceSpec::new(kind, kind, tilo_resource::label::localized(kind, label));
2608        for decl in declarations {
2609            resource = resource.with_action(tilo_resource::declaration_spec(decl));
2610        }
2611        tilo_resource::withhold_document_actions(&mut resource, reason);
2612        Self {
2613            title,
2614            icon,
2615            shown: unreadable_state(reason),
2616            resource,
2617        }
2618    }
2619}
2620
2621impl Unopened {
2622    /// The window as a host mounts it, its widget table compiled once for
2623    /// every app.
2624    pub(crate) fn boxed(self) -> Box<dyn AppWidget> {
2625        Box::new(self)
2626    }
2627}
2628
2629impl Decorator for Unopened {
2630    ui_core::provides!(dyn ui_core::resources::PublishesResources);
2631
2632    type Inner = ui_core::widget::EmptyState;
2633
2634    fn inner(&self) -> &ui_core::widget::EmptyState {
2635        &self.shown
2636    }
2637
2638    fn inner_mut(&mut self) -> &mut ui_core::widget::EmptyState {
2639        &mut self.shown
2640    }
2641}
2642
2643impl ui_core::resources::PublishesResources for Unopened {
2644    fn resources(&self, _doc: &AutoCommit) -> Vec<ResourceSpec> {
2645        vec![self.resource.clone()]
2646    }
2647}
2648
2649impl AppWidget for Unopened {
2650    fn app_kind(&self) -> AppKind {
2651        AppKind::Editor
2652    }
2653
2654    fn app_title(&self) -> &str {
2655        self.title
2656    }
2657
2658    fn app_icon(&self) -> IconDescriptor {
2659        self.icon.clone()
2660    }
2661}
2662
2663#[cfg(test)]
2664mod tests {
2665    use super::*;
2666    use ui_core::search::{DEFAULT_SEARCH_RESULT_LIMIT, SearchQuery};
2667
2668    #[test]
2669    fn child_document_reads_are_bound_to_their_own_resource() {
2670        let mut child = ResourceSpec::new("book/page/body", "text", "Body");
2671        child.children.push(ResourceSpec::new(
2672            "private-child",
2673            "account",
2674            "Secret account",
2675        ));
2676        offer_document_reads(&mut child, &|id| id == "book/page/body");
2677        assert!(
2678            child.children[0].actions.is_empty(),
2679            "eligibility is not inherited"
2680        );
2681        let read = &child.actions[0];
2682        assert!(read.contract.reads_document());
2683        let encoded = read.build_json(&serde_json::Map::new()).unwrap();
2684        assert_eq!(encoded.type_id, READ_TYPE);
2685        let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2686        assert_eq!(payload["item"], "book/page/body");
2687        assert!(
2688            read.build_json(
2689                serde_json::json!({"item": "another/book"})
2690                    .as_object()
2691                    .unwrap()
2692            )
2693            .is_err()
2694        );
2695        let encoded = read
2696            .build_json(serde_json::json!({"version": "past"}).as_object().unwrap())
2697            .unwrap();
2698        let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2699        assert_eq!(payload["item"], "book/page/body");
2700        assert_eq!(payload["version"], "past");
2701    }
2702
2703    /// The ids the framework keeps are the ones it publishes on every
2704    /// app: the generic actions, `open`, `read`, history and presentation.
2705    #[test]
2706    fn the_reserved_ids_are_what_the_framework_publishes() {
2707        let reserved = reserved_action_ids();
2708        for id in [
2709            "apply",
2710            "group",
2711            "field-edit",
2712            "open",
2713            "read",
2714            "undo",
2715            "redo",
2716            "restore",
2717            "restore-reviewed",
2718            "review-restore",
2719            "checkpoint",
2720            "history",
2721            "diff",
2722            "render",
2723        ] {
2724            assert!(
2725                reserved.iter().any(|kept| kept == id),
2726                "{id} in {reserved:?}"
2727            );
2728        }
2729        assert!(
2730            !reserved.iter().any(|kept| kept == "rename"),
2731            "an app's own verb name is not kept"
2732        );
2733    }
2734
2735    #[test]
2736    fn a_search_entry_naming_an_action_is_a_hit_that_invokes_it() {
2737        let open = tilo_actions::ActionAddress::new("notes.page:1", "open");
2738        let search = ResourceSearch {
2739            name: "Notes",
2740            app: "so.tilo.notes",
2741            indexed: Arc::new(IndexedEntries::built(
2742                vec![
2743                    SearchEntry::new("Groceries", "milk and eggs").invoking(open.clone()),
2744                    SearchEntry::new("Garden", "roses").anchored("page:2"),
2745                ],
2746                0,
2747            )),
2748            scopes: Vec::new(),
2749            queries: Vec::new(),
2750            readable: true,
2751            live: None,
2752            opens: None,
2753        };
2754        let groceries = search.search("groceries");
2755        assert_eq!(groceries.len(), 1);
2756        assert_eq!(groceries[0].action_address(), Some(open));
2757        let garden = search.search("garden");
2758        assert_eq!(garden.len(), 1);
2759        assert_eq!(garden[0].action_address(), None);
2760        assert_eq!(garden[0].action_id, "page:2");
2761    }
2762
2763    #[test]
2764    fn a_search_entry_reads_as_its_group_subtitle_and_icon() {
2765        let reveal = tilo_actions::ActionAddress::new("video.marker:1", "reveal");
2766        let search = ResourceSearch {
2767            name: "Project",
2768            app: "so.tilo.project",
2769            indexed: Arc::new(IndexedEntries::built(
2770                vec![
2771                    SearchEntry::new("Sunset", "the last light")
2772                        .invoking(reveal)
2773                        .grouped("Timeline markers")
2774                        .subtitled("0:21 · Drone")
2775                        .with_icon("star")
2776                        .boosted(5),
2777                    SearchEntry::new("Sunset take", ""),
2778                ],
2779                0,
2780            )),
2781            scopes: Vec::new(),
2782            queries: Vec::new(),
2783            readable: true,
2784            live: None,
2785            opens: None,
2786        };
2787        let hits = search.search("sunset");
2788        let marker = hits
2789            .iter()
2790            .find(|hit| hit.title == "Sunset")
2791            .expect("the marker is found");
2792        assert_eq!(marker.kind, "Timeline markers");
2793        assert_eq!(marker.subtitle.as_deref(), Some("0:21 · Drone"));
2794        assert_eq!(marker.icon, Some(ui_core::icons::Icon::Star));
2795        let plain = hits
2796            .iter()
2797            .find(|hit| hit.title == "Sunset take")
2798            .expect("the plain entry is found");
2799        assert_eq!(plain.kind, "Project", "an ungrouped hit is the resource's");
2800        assert!(plain.preview.is_none(), "no text, no preview");
2801        // Subtitles are searched too: a marker is found by the shot it is on.
2802        assert!(
2803            search
2804                .search("drone")
2805                .iter()
2806                .any(|hit| hit.title == "Sunset")
2807        );
2808    }
2809
2810    #[test]
2811    fn every_entry_stays_findable_page_by_page() {
2812        let search = ResourceSearch::new(
2813            "Tracks",
2814            "so.tilo.test",
2815            Arc::new(IndexedEntries::built(
2816                (0..257)
2817                    .map(|n| SearchEntry::new(format!("Common track {n}"), ""))
2818                    .collect(),
2819                0,
2820            )),
2821        );
2822        let mut cursor = None;
2823        let mut seen = std::collections::HashSet::new();
2824        loop {
2825            let page = search.search_page(&SearchQuery::new("common"), cursor.as_ref(), 17, 5);
2826            assert!(page.results.len() <= 5);
2827            for hit in &page.results {
2828                assert!(seen.insert(hit.title.clone()), "{} came twice", hit.title);
2829            }
2830            let Some(next) = page.next else { break };
2831            assert!(search.action_directory_cursor_is_current(&next));
2832            cursor = Some(next);
2833        }
2834        assert_eq!(seen.len(), 257, "every match is reachable");
2835    }
2836
2837    #[test]
2838    fn no_caller_materialises_more_than_one_page_at_once() {
2839        let search = ResourceSearch::new(
2840            "Tables",
2841            "so.tilo.test",
2842            Arc::new(IndexedEntries::built(
2843                (0..300)
2844                    .map(|n| SearchEntry::new(format!("table {n}"), ""))
2845                    .collect(),
2846                0,
2847            )),
2848        );
2849        assert_eq!(
2850            search.search_limited("", usize::MAX).len(),
2851            DEFAULT_SEARCH_RESULT_LIMIT
2852        );
2853        assert_eq!(
2854            search
2855                .search_page(&SearchQuery::new(""), None, usize::MAX, usize::MAX)
2856                .results
2857                .len(),
2858            DEFAULT_SEARCH_RESULT_LIMIT
2859        );
2860    }
2861
2862    /// A chapter of a book: one paragraph per segment, each opening the
2863    /// book where it is.
2864    struct Chapter(Vec<&'static str>);
2865
2866    impl tilo_resource::SearchText for Chapter {
2867        fn segment_count(&self) -> usize {
2868            self.0.len()
2869        }
2870
2871        fn piece_count(&self, segment: usize) -> usize {
2872            usize::from(segment < self.0.len())
2873        }
2874
2875        fn piece(&self, segment: usize, piece: usize) -> Option<tilo_resource::SearchPiece<'_>> {
2876            let text = self.0.get(segment)?;
2877            (piece == 0).then_some(tilo_resource::SearchPiece {
2878                text,
2879                space_before: segment > 0,
2880            })
2881        }
2882
2883        fn locates(&self, _segment: usize) -> bool {
2884            true
2885        }
2886
2887        fn location(&self, segment: usize) -> Option<SearchEntry> {
2888            Some(
2889                SearchEntry::new(format!("Paragraph {}", segment.saturating_add(1)), "").invoking(
2890                    tilo_actions::ActionAddress::new(format!("book.location:{segment}"), "open"),
2891                ),
2892            )
2893        }
2894    }
2895
2896    #[test]
2897    fn a_match_in_a_body_is_the_place_it_is_in() {
2898        let body = tilo_resource::SearchBody::new(Arc::new(Chapter(vec![
2899            "It was a dark and stormy night.",
2900            "The lighthouse keeper counted the waves.",
2901        ])));
2902        let search = ResourceSearch::new(
2903            "Library",
2904            "so.tilo.test",
2905            Arc::new(IndexedEntries::built(
2906                vec![
2907                    SearchEntry::new("Storm Tales", "")
2908                        .invoking(tilo_actions::ActionAddress::new("book:1", "open"))
2909                        .grouped("Books")
2910                        .with_body(body.clone()),
2911                ],
2912                0,
2913            )),
2914        );
2915        let hits = search.search("lighthouse keeper");
2916        let place = hits
2917            .iter()
2918            .find(|hit| hit.title == "Paragraph 2")
2919            .expect("the paragraph the words are in is a hit");
2920        assert_eq!(
2921            place.action_address(),
2922            Some(tilo_actions::ActionAddress::new("book.location:1", "open"))
2923        );
2924        assert_eq!(place.kind, "Books", "a location is listed with its book");
2925        assert!(
2926            search
2927                .search("storm tales")
2928                .iter()
2929                .any(|hit| hit.title == "Storm Tales"),
2930            "the book itself is found by its title"
2931        );
2932        assert_eq!(body, body.clone(), "a body is the text it shares");
2933    }
2934
2935    #[test]
2936    fn a_field_ranks_by_its_own_weight() {
2937        use tilo_resource::SearchWeight;
2938        let search = ResourceSearch::new(
2939            "Mail",
2940            "so.tilo.test",
2941            Arc::new(IndexedEntries::built(
2942                vec![
2943                    SearchEntry::new("Lunch plans", "")
2944                        .field("[email protected]", SearchWeight::Metadata),
2945                    SearchEntry::new("Quarterly report", "")
2946                        .field("[email protected]", SearchWeight::Identity),
2947                ],
2948                0,
2949            )),
2950        );
2951        let hits = search.search("[email protected]");
2952        assert_eq!(hits.len(), 2, "both are found by the address");
2953        assert_eq!(
2954            hits[0].title, "Quarterly report",
2955            "a match in an identity outranks one in metadata"
2956        );
2957    }
2958}
2959
2960// Compiled only into native tests: the file's own `cfg` says so.
2961mod publication_tests;