1use 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 resource: ResourceType,
66 typed: Box<dyn TypedRoot>,
70 action_bindings: super::controls::ActionTable,
72 followed: std::cell::Cell<u64>,
74 _alive: Rc<()>,
75 pub(crate) dispatch: DispatchCore,
76 saved: LocalState<RecordSaveStatus>,
77 save: super::saves::SaveIndicator,
80 reactions: super::reactions::ReactionsIndicator,
83 permissions: Option<LivePermissions>,
87 write: Option<PermissionScope>,
88 stages: Stages,
89 feeds: super::history::HistoryFeeds,
90 pub(crate) run: Run,
92 pub(crate) storage: Rc<super::settings::ViewStorage>,
94 pub(crate) content: Element,
96 unreadable: Option<(String, Element)>,
99 swapped: bool,
101 arrived: Option<Rc<()>>,
104 presentation: Vec<tilo_actions::ActionSpec>,
107 search: Rc<std::cell::RefCell<SearchCache>>,
109 publication_tree: std::cell::RefCell<Option<SharedTreeCache>>,
114 item_children: std::cell::RefCell<Option<ItemChildren>>,
117 published_children: std::cell::RefCell<Option<PublishedChildren>>,
119 record_children: std::cell::RefCell<Option<RecordChildren>>,
122 row_children: std::cell::RefCell<Option<RowChildren>>,
125 binding_head: std::cell::RefCell<Option<BindingHead>>,
129 binding_base: std::cell::RefCell<Option<Rc<BindingBase>>>,
132 live_head: std::cell::RefCell<Option<LiveHead>>,
134 document_children: std::cell::RefCell<Option<DocumentChildren>>,
137 nested_views: std::cell::RefCell<Option<NestedViews>>,
139 history: bool,
141 ground: bool,
149 native_presentation: crate::runner::Presentation,
151 share: super::share_link::ShareLinks,
153 source: super::Source,
157}
158
159impl ResourceRoot {
160 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 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 let head = self.refresh_bindings(Some(head));
202 *self.binding_head.borrow_mut() = Some(head);
203 self.save.sync(ui_core::time::now());
205 self.sync_reactions();
206 }
207}
208
209impl ui_core::component_lifecycle::WindowActivation for ResourceRoot {
210 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
220pub(crate) const OPEN_TYPE: &str = tilo_actions::remote::APPLICATION_OPEN_TYPE;
223
224const OPEN_ANCHOR: &str = "anchor";
226
227fn 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
257const ITEM_VIEW_LABEL: &str = "Item";
260
261fn 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
284fn 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 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#[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
347type WorkspaceArrival = ui_core::widget::ArrivingView;
350
351fn 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
369fn 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
383fn 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 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 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 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 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 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 pub(crate) fn reading_from_source(mut self) -> Self {
572 self.source = self.dispatch.source().clone();
573 self
574 }
575
576 pub(crate) fn without_history(mut self) -> Self {
579 self.history = false;
580 self
581 }
582
583 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 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 pub(crate) fn settle_saves_for_capture(&mut self) {
626 self.save.settle_for_capture(ui_core::time::now());
627 }
628
629 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 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 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 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 ui_core::window_toolbar::show_view_only(read_only);
681 }
682 self.dispatch.refresh_permission_states();
683 }
684
685 pub(crate) fn into_widget(self) -> Box<dyn AppWidget> {
687 RootWidget::boxed(Box::new(self))
688 }
689
690 pub(crate) fn into_root_widget(self) -> Box<dyn Widget> {
694 Box::new(RootWidget::new(Box::new(self)))
695 }
696
697 pub(crate) fn typed<R: Resource>(&self) -> Option<&Typed<R>> {
699 self.typed.as_any().downcast_ref::<Typed<R>>()
700 }
701
702 pub(crate) fn launch(&self) -> super::launch::LaunchInput<'_> {
704 self.run.launch()
705 }
706
707 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 #[inline(never)]
732 fn bind_row_declarations(&self, rows: &mut [ResourceSpec]) {
733 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 #[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 tilo_resource::withhold_document_actions(&mut tree, &reason);
792 }
793 self.decorate_publication(&*state, tree, Decoration::Tree)
794 }
795
796 fn version(&self) -> (u64, u64) {
799 (self.typed.revision(), self.typed.loads())
800 }
801
802 #[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 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 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 #[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 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 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 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 #[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 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 #[inline(never)]
1011 fn current_publication(&self, doc: &AutoCommit) -> ResourceSpec {
1012 if !self.source.is_connection()
1016 && !self.typed.follows(doc)
1017 && let Ok(current) = self.typed.read(doc)
1018 {
1019 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 #[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 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 resource.children.extend(self.storage.record_resources());
1116 resource.children.extend(self.storage.unavailable_records());
1117 }
1118 resource
1119 }
1120
1121 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 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 #[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 fn refresh(&self, doc: &AutoCommit) -> BindingHead {
1183 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 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
1201struct 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
1216fn 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#[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
1246struct BindingBase {
1248 key: BaseKey,
1249 resource: Rc<ResourceSpec>,
1250 head_len: usize,
1252 shadow_len: usize,
1255}
1256
1257struct 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
1271fn 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#[derive(Clone, Copy, PartialEq, Eq)]
1294enum Decoration {
1295 Tree,
1297 Actions,
1300}
1301
1302#[derive(Clone)]
1305struct BindingHead {
1306 version: ((u64, u64), u64),
1307 epoch: Option<String>,
1309 resource: Rc<ResourceSpec>,
1310}
1311
1312#[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
1343pub 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
1371const OFF_THREAD_ENTRIES: usize = 1024;
1374
1375fn indexes_off_frame(entries: &[SearchEntry]) -> bool {
1384 #[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 pub(crate) static FORCE_OFF_FRAME: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
1398}
1399
1400#[derive(PartialEq)]
1403struct SearchState {
1404 revision: (u64, u64),
1405 shown: Vec<SearchEntry>,
1406}
1407
1408struct AnsweringIndex {
1411 of: Option<SearchState>,
1412 indexed: Arc<IndexedEntries>,
1413}
1414
1415struct BuildingIndex {
1417 of: SearchState,
1418 generation: u64,
1419 arriving: std::sync::mpsc::Receiver<IndexedEntries>,
1420}
1421
1422#[derive(Default)]
1429pub(crate) struct SearchCache {
1430 answering: Option<AnsweringIndex>,
1431 building: Option<BuildingIndex>,
1432 wanted: Option<(SearchState, u64)>,
1435 queued: Option<(SearchState, u64, Vec<SearchEntry>)>,
1438 watching: bool,
1440 answered: Option<u64>,
1443 next_generation: u64,
1444 #[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 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 Err(std::sync::mpsc::TryRecvError::Disconnected) => {}
1481 }
1482 }
1483
1484 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 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 pub(crate) fn building(&mut self) -> bool {
1561 self.receive();
1562 self.building.is_some()
1563 }
1564
1565 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 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
1592impl 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 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
1625 self.content.view_only_scroll(event)
1626 }
1627
1628 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 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 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 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1698 self
1699 }
1700
1701 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 handlers.insert(tilo_actions::remote::ApplicationLaunch::TYPE.to_owned());
1732 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 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 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 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 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 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 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 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 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 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 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 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
2076impl 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 let (state, queries) = self.search_state();
2114 let indexed = self.current_search_index(state);
2115 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 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 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 ui_core::widget::ActionDirectoryInvocationDomain::Unsupported,
2148 )]
2149 }
2150
2151 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 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 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 let _viewer = participant
2213 .cloned()
2214 .map(ui_core::invocation_scope::ViewerScope::enter);
2215 let mut resource = self.current_publication(doc);
2216 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 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 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 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 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 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 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 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
2354impl 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 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 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 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 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
2569fn 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
2584pub(crate) struct Unopened {
2588 title: &'static str,
2589 icon: IconDescriptor,
2590 resource: ResourceSpec,
2593 shown: ui_core::widget::EmptyState,
2594}
2595
2596impl Unopened {
2597 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 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 #[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 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 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
2960mod publication_tests;