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
1375#[derive(PartialEq)]
1378struct SearchState {
1379 revision: (u64, u64),
1380 shown: Vec<SearchEntry>,
1381}
1382
1383struct AnsweringIndex {
1386 of: Option<SearchState>,
1387 indexed: Arc<IndexedEntries>,
1388}
1389
1390struct BuildingIndex {
1392 of: SearchState,
1393 generation: u64,
1394 arriving: std::sync::mpsc::Receiver<IndexedEntries>,
1395}
1396
1397#[derive(Default)]
1404pub(crate) struct SearchCache {
1405 answering: Option<AnsweringIndex>,
1406 building: Option<BuildingIndex>,
1407 wanted: Option<(SearchState, u64)>,
1410 queued: Option<(SearchState, u64, Vec<SearchEntry>)>,
1413 watching: bool,
1415 answered: Option<u64>,
1418 next_generation: u64,
1419 #[cfg(test)]
1421 builds: usize,
1422}
1423
1424impl SearchCache {
1425 fn reserve_generation(&mut self) -> u64 {
1426 let generation = self.next_generation;
1427 self.next_generation = generation.wrapping_add(1);
1428 generation
1429 }
1430
1431 fn receive(&mut self) {
1434 self.land();
1435 if self.building.is_none()
1436 && let Some((state, generation, entries)) = self.queued.take()
1437 {
1438 self.build(state, generation, entries);
1439 }
1440 }
1441
1442 fn land(&mut self) {
1443 let Some(building) = self.building.take() else {
1444 return;
1445 };
1446 match building.arriving.try_recv() {
1447 Ok(indexed) => {
1448 self.answering = Some(AnsweringIndex {
1449 of: Some(building.of),
1450 indexed: Arc::new(indexed),
1451 });
1452 }
1453 Err(std::sync::mpsc::TryRecvError::Empty) => self.building = Some(building),
1454 Err(std::sync::mpsc::TryRecvError::Disconnected) => {}
1456 }
1457 }
1458
1459 fn generation_for(&mut self, state: SearchState) -> u64 {
1462 self.receive();
1463 if let Some(answering) = &self.answering
1464 && answering.of.as_ref() == Some(&state)
1465 {
1466 return answering.indexed.generation;
1467 }
1468 if let Some(building) = &self.building
1469 && building.of == state
1470 {
1471 return building.generation;
1472 }
1473 if let Some((wanted, generation)) = &self.wanted
1474 && *wanted == state
1475 {
1476 return *generation;
1477 }
1478 let generation = self.reserve_generation();
1479 self.wanted = Some((state, generation));
1480 generation
1481 }
1482
1483 fn build(
1486 &mut self,
1487 state: SearchState,
1488 generation: u64,
1489 entries: Vec<SearchEntry>,
1490 ) -> Arc<IndexedEntries> {
1491 #[cfg(test)]
1492 {
1493 self.builds = self.builds.saturating_add(1);
1494 }
1495 if entries.len() >= OFF_THREAD_ENTRIES || entries.iter().any(|entry| entry.body.is_some()) {
1496 let (send, arriving) = std::sync::mpsc::channel();
1497 let building = entries.clone();
1498 let spawned = ui_core::background::Background::current().spawn(
1499 "resource-search-index",
1500 move || {
1501 let _ = send.send(IndexedEntries::built(building, generation));
1502 },
1503 );
1504 if spawned.is_ok() {
1505 self.building = Some(BuildingIndex {
1506 of: state,
1507 generation,
1508 arriving,
1509 });
1510 return self.answering();
1511 }
1512 }
1513 let indexed = Arc::new(IndexedEntries::built(entries, generation));
1514 self.answering = Some(AnsweringIndex {
1515 of: Some(state),
1516 indexed: Arc::clone(&indexed),
1517 });
1518 indexed
1519 }
1520
1521 fn answer(&mut self) -> Option<Arc<IndexedEntries>> {
1524 self.receive();
1525 let wanted = self.answered?;
1526 self.answering
1527 .as_ref()
1528 .filter(|answering| answering.of.is_some() && answering.indexed.generation >= wanted)
1529 .map(|answering| Arc::clone(&answering.indexed))
1530 }
1531
1532 fn answering(&mut self) -> Arc<IndexedEntries> {
1535 if let Some(answering) = &self.answering {
1536 return Arc::clone(&answering.indexed);
1537 }
1538 let generation = self.reserve_generation();
1539 let indexed = Arc::new(IndexedEntries::built(Vec::new(), generation));
1540 self.answering = Some(AnsweringIndex {
1541 of: None,
1542 indexed: Arc::clone(&indexed),
1543 });
1544 indexed
1545 }
1546}
1547
1548impl RootModel for ResourceRoot {
1553 ui_core::provides!(dyn ui_core::undo::CommittedActions, dyn ui_core::history_mode::capability::DocumentHistory, dyn ui_core::widget::document_effects::DocumentEffects, dyn ui_core::widget::Collaborative, #[cfg(not(target_family = "wasm"))] dyn ui_core::component_lifecycle::WindowActivation; inner = content);
1554
1555 fn view(&self) -> &(dyn Widget + 'static) {
1556 match &self.unreadable {
1557 Some((_, shown)) => shown.as_ref(),
1558 None => self.content.as_ref(),
1559 }
1560 }
1561
1562 fn view_mut(&mut self) -> &mut (dyn Widget + 'static) {
1563 match &mut self.unreadable {
1564 Some((_, shown)) => shown.as_mut(),
1565 None => self.content.as_mut(),
1566 }
1567 }
1568
1569 fn paints_ground(&self) -> bool {
1570 self.ground
1571 }
1572
1573 fn take_structure_dirty(&mut self) -> bool {
1574 let swapped = std::mem::take(&mut self.swapped);
1575 self.view_mut().take_structure_dirty() || swapped
1576 }
1577
1578 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
1581 self.content.view_only_scroll(event)
1582 }
1583
1584 fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
1587 let change = self.view_mut().take_structure_change();
1588 if std::mem::take(&mut self.swapped) {
1589 ui_core::widget::StructureChange::Rebuild
1590 } else {
1591 change
1592 }
1593 }
1594
1595 fn record_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
1596 let mut owners = self.storage.person_owners();
1597 owners.extend(self.content.record_owners());
1598 owners
1599 }
1600
1601 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
1602 if self.source.is_connection() {
1603 self.sync_remote_state();
1608 let empty = AutoCommit::new();
1609 self.sync_over(&empty, &automerge::ROOT);
1610 return;
1611 }
1612 let (state, path) = ui_core::doc::document_ref(state, path);
1613 self.sync_over(state, path);
1614 }
1615
1616 fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ui_core::automerge::ObjId) {
1617 self.typed.apply_settlements(edits);
1618 self.content.settle(edits, path);
1619 self.storage.settle();
1620 }
1621
1622 fn draw(
1623 &mut self,
1624 scene: &mut Scene,
1625 transform: Affine,
1626 text: &mut TextRenderer,
1627 theme: &Theme,
1628 presence: &PresenceCtx,
1629 ) {
1630 self.paint_ground(scene, transform, theme, presence);
1631 self.content.draw(scene, transform, text, theme, presence);
1632 self.save.seen();
1634 }
1635
1636 fn needs_redraw(&self) -> bool {
1637 self.native_presentation.needs_redraw()
1638 || self.content.needs_redraw()
1639 || self.save.needs_redraw(ui_core::time::now())
1640 }
1641
1642 fn save_indicator(&self) -> &super::saves::SaveIndicator {
1643 &self.save
1644 }
1645
1646 #[cfg(test)]
1647 fn save_indicator_mut(&mut self) -> &mut super::saves::SaveIndicator {
1648 &mut self.save
1649 }
1650
1651 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1654 self
1655 }
1656
1657 fn resource_operation_handlers(&self) -> HashSet<String> {
1670 let mut handlers = ui_core::host_operation::resource_operation_handlers(self.view());
1671 handlers.extend(
1672 crate::runner::operation_types()
1673 .iter()
1674 .map(|op| (*op).to_owned()),
1675 );
1676 handlers.insert(super::history::VIEW_TYPE.to_owned());
1677 handlers.insert(super::person::HISTORY_TYPE.to_owned());
1678 handlers.insert(super::record::RECORD_ACTION_TYPE.to_owned());
1679 handlers.insert(super::chain::CHAIN_ACTION_TYPE.to_owned());
1680 handlers.insert(tilo_resource::share::SHARE_TYPE.to_owned());
1681 handlers.insert(tilo_resource::share::SHARE_INVITE_TYPE.to_owned());
1682 if self.stages.opens() {
1683 handlers.insert(OPEN_TYPE.to_owned());
1684 }
1685 handlers.insert(tilo_actions::remote::ApplicationLaunch::TYPE.to_owned());
1688 if self.resource.readable || self.resource.record_table.is_some() {
1692 handlers.insert(READ_TYPE.to_owned());
1693 }
1694 if self.resource.facts {
1695 handlers.insert(facts::FACTS_TYPE.to_owned());
1696 }
1697 handlers.extend(
1698 self.typed
1699 .effect_operations()
1700 .into_iter()
1701 .map(str::to_owned),
1702 );
1703 for spec in (self.resource.local_operations)()
1704 .into_iter()
1705 .chain(self.presentation.iter().cloned())
1706 {
1707 if let Some(type_id) = &spec.operation_type {
1708 handlers.insert(type_id.clone());
1709 }
1710 }
1711 handlers.extend(self.nested_views().iter().cloned());
1712 handlers
1713 }
1714
1715 fn poll_resource_operation(
1716 &mut self,
1717 invocation: tilo_actions::InvocationId,
1718 cancellation_requested: bool,
1719 doc: &AutoCommit,
1720 path: &ui_core::automerge::ObjId,
1721 ) -> Option<tilo_actions::InvocationUpdate> {
1722 if let Some((update, copied)) = self.share.poll(invocation, cancellation_requested) {
1723 if copied && let tilo_actions::InvocationUpdate::Succeeded { output, .. } = &update {
1724 self.dispatch
1725 .note_landed(tilo_resource::share::SHARE_LINK, output);
1726 }
1727 return Some(update);
1728 }
1729 self.typed
1730 .poll_effect(
1731 invocation,
1732 cancellation_requested,
1733 effect_view(&mut self.unreadable, &mut self.content),
1734 )
1735 .or_else(|| {
1736 with_app_view_mut(&mut self.content, |view| {
1737 ui_core::host_operation::poll_resource_operation(
1738 view,
1739 invocation,
1740 cancellation_requested,
1741 doc,
1742 path,
1743 )
1744 })
1745 })
1746 }
1747
1748 fn start_operation(
1749 &mut self,
1750 op: &ui_core::host_operation::ResourceOperation<'_>,
1751 doc: &mut ui_core::widget::Edits<'_>,
1752 path: &ui_core::automerge::ObjId,
1753 ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
1754 if let Some(request) = op.unscoped()
1758 && request.type_id == crate::runner::RENDER_TYPE
1759 && !request.payload.is_empty()
1760 && let Some(anchor) = crate::runner::rendered_anchor(request.payload)
1761 {
1762 self.stages.open(&anchor)?;
1763 }
1764 if let Some(output) = with_app_view_mut(&mut self.content, |view| {
1765 self.native_presentation.start_operation(op, doc, view)
1766 })? {
1767 return Ok(Some(
1768 ui_core::host_operation::ResourceOperationStart::Completed(output),
1769 ));
1770 }
1771 if (self.resource.readable || self.resource.record_table.is_some())
1772 && let Some(request) = op.unscoped()
1773 && request.type_id == READ_TYPE
1774 {
1775 let output = read(
1776 self.typed.as_ref(),
1777 &self.resource,
1778 &*self.typed.state(),
1779 doc,
1780 request.payload,
1781 )?;
1782 return Ok(Some(
1783 ui_core::host_operation::ResourceOperationStart::Completed(output),
1784 ));
1785 }
1786 if self.resource.facts
1787 && let Some(request) = op.unscoped()
1788 && request.type_id == facts::FACTS_TYPE
1789 {
1790 let output = facts::answer(
1791 self.typed.as_ref(),
1792 &*self.typed.state(),
1793 doc,
1794 request.payload,
1795 )?;
1796 return Ok(Some(
1797 ui_core::host_operation::ResourceOperationStart::Completed(output),
1798 ));
1799 }
1800 if let Some(request) = op.unscoped()
1801 && super::share_link::handles(request.type_id)
1802 {
1803 return self.start_share(request.invocation, request.payload);
1804 }
1805 if let Some(request) = op.unscoped()
1806 && request.type_id == OPEN_TYPE
1807 {
1808 let args: serde_json::Value =
1809 tilo_actions::decode_json(request.payload).map_err(|error| error.to_string())?;
1810 let anchor = args
1811 .get(OPEN_ANCHOR)
1812 .and_then(serde_json::Value::as_str)
1813 .ok_or_else(|| "open needs the anchor of an item".to_owned())?;
1814 self.stages.open(anchor)?;
1815 return Ok(Some(
1816 ui_core::host_operation::ResourceOperationStart::Completed(
1817 tilo_actions::ActionOutput::default(),
1818 ),
1819 ));
1820 }
1821 if let Some(output) = self.start_record_operation(op, doc)? {
1822 return Ok(Some(
1823 ui_core::host_operation::ResourceOperationStart::Completed(output),
1824 ));
1825 }
1826 if let Err(error) = self.typed.sync(doc) {
1829 tracing::warn!(%error, kind = self.resource.kind, "resource unreadable");
1830 }
1831 if let Some(started) = self.typed.start_effect(
1832 op,
1833 doc.doc(),
1834 effect_view(&mut self.unreadable, &mut self.content),
1835 )? {
1836 return Ok(Some(started));
1837 }
1838 if let Some(started) =
1839 ui_core::host_operation::start_operation(self.view_mut(), op, doc, path)?
1840 {
1841 return Ok(Some(started));
1842 }
1843 Ok(self.start_catalogue_view(op))
1844 }
1845}
1846
1847impl ResourceRoot {
1848 fn start_record_operation(
1852 &self,
1853 op: &ui_core::host_operation::ResourceOperation<'_>,
1854 doc: &mut ui_core::widget::Edits<'_>,
1855 ) -> Result<Option<tilo_actions::ActionOutput>, String> {
1856 let Some(request) = op.unscoped() else {
1857 return Ok(None);
1858 };
1859 let output = if request.type_id == super::history::VIEW_TYPE {
1860 super::history::answer(doc, request.payload)?
1861 } else if request.type_id == super::record::RECORD_ACTION_TYPE {
1862 self.storage
1863 .record_action(self.dispatch.me(), request.payload)?
1864 } else if request.type_id == super::chain::CHAIN_ACTION_TYPE {
1865 self.storage
1866 .chain_action(self.dispatch.me(), request.payload, doc)?
1867 } else if request.type_id == super::person::HISTORY_TYPE {
1868 self.storage.person_history(request.payload)?
1869 } else {
1870 return Ok(None);
1871 };
1872 Ok(Some(output))
1873 }
1874
1875 fn search_state(&self) -> (SearchState, Vec<super::publishing::SearchQueries>) {
1879 let mut shown = Vec::new();
1880 let mut queries = Vec::new();
1881 self.surfaces(&mut |surface| {
1882 shown.extend(surface.search_entries());
1883 queries.extend(surface.search_queries());
1884 });
1885 let state = SearchState {
1886 revision: self.version(),
1887 shown,
1888 };
1889 (state, queries)
1890 }
1891
1892 fn current_search_index(&self, state: SearchState) -> Arc<IndexedEntries> {
1899 let mut cache = self.search.borrow_mut();
1900 let generation = cache.generation_for(state);
1901 if let Some(answering) = &cache.answering
1902 && answering.of.is_some()
1903 && answering.indexed.generation == generation
1904 {
1905 return Arc::clone(&answering.indexed);
1906 }
1907 if cache.building.is_some() {
1908 return cache.answering();
1909 }
1910 let Some((state, generation)) = cache.wanted.take() else {
1911 return cache.answering();
1912 };
1913 let entries = self.entries_of(&state);
1914 cache.build(state, generation, entries)
1915 }
1916
1917 fn secrets_unlocked(&self) -> Option<bool> {
1920 let mut said = None;
1921 self.surfaces(&mut |surface| match surface.secrets_unlocked() {
1922 Some(false) => said = Some(false),
1923 Some(true) if said.is_none() => said = Some(true),
1924 _ => {}
1925 });
1926 said
1927 }
1928
1929 fn entries_of(&self, state: &SearchState) -> Vec<SearchEntry> {
1932 let mut entries = self.typed.search(&*self.typed.state());
1933 entries.extend(state.shown.iter().cloned());
1934 entries
1935 }
1936
1937 fn follow_search(&self) {
1941 if !self.search.borrow().watching {
1942 return;
1943 }
1944 let mut answering = false;
1945 self.surfaces(&mut |surface| {
1946 answering |= surface
1947 .search_queries()
1948 .is_some_and(|queries| queries.answering());
1949 });
1950 if answering {
1951 return;
1952 }
1953 let (state, _) = self.search_state();
1954 let mut cache = self.search.borrow_mut();
1955 cache.watching = false;
1956 let generation = cache.generation_for(state);
1957 cache.answered = Some(generation);
1958 if cache.building.is_some() {
1959 if let Some((state, wanted)) = cache.wanted.take() {
1961 let entries = self.entries_of(&state);
1962 cache.queued = Some((state, wanted, entries));
1963 }
1964 return;
1965 }
1966 if let Some((state, wanted)) = cache.wanted.take() {
1967 let entries = self.entries_of(&state);
1968 cache.build(state, wanted, entries);
1969 }
1970 }
1971}
1972
1973impl ResourceRoot {
1974 fn start_catalogue_view(
1975 &self,
1976 op: &ui_core::host_operation::ResourceOperation<'_>,
1977 ) -> Option<ui_core::host_operation::ResourceOperationStart> {
1978 let request = op.unscoped()?;
1983 if request.kind != tilo_actions::OperationKind::View {
1984 return None;
1985 }
1986 let known = (self.resource.local_operations)()
1987 .into_iter()
1988 .chain(self.presentation.iter().cloned())
1989 .any(|spec| spec.operation_type.as_deref() == Some(request.type_id))
1990 || self.nested_views().contains(request.type_id);
1991 known.then(|| {
1992 ui_core::host_operation::ResourceOperationStart::Completed(
1993 tilo_actions::ActionOutput::default(),
1994 )
1995 })
1996 }
1997
1998 fn start_share(
2000 &mut self,
2001 invocation: tilo_actions::InvocationId,
2002 payload: &[u8],
2003 ) -> Result<Option<ui_core::host_operation::ResourceOperationStart>, String> {
2004 let record = self
2005 .run
2006 .launched
2007 .record
2008 .as_ref()
2009 .map(|record| record.as_str().to_owned());
2010 let publisher = super::share_link::Publisher {
2011 app: self.dispatch.app_id(),
2012 record: record.as_deref(),
2013 root: self.dispatch.resource_id(),
2014 };
2015 let may_invite = self.dispatch.may_invite();
2016 let started = self
2017 .share
2018 .start(invocation, payload, &publisher, may_invite)?;
2019 Ok(Some(match started {
2020 Some(minted) => {
2021 if minted.copied {
2022 self.dispatch
2023 .note_landed(tilo_resource::share::SHARE_LINK, &minted.output);
2024 }
2025 ui_core::host_operation::ResourceOperationStart::Completed(minted.output)
2026 }
2027 None => ui_core::host_operation::ResourceOperationStart::Accepted,
2028 }))
2029 }
2030}
2031
2032impl ui_core::widget::Collaborative for ResourceRoot {
2036 fn take_collab_requests(&mut self) -> Vec<ui_core::collaboration::CollabRequest> {
2037 let mut requests =
2038 ui_core::widget::Collaborative::take_collab_requests(self.share.remote_mut());
2039 if let Some(mut door) = self.dispatch.opener.door() {
2040 requests.extend(ui_core::widget::Collaborative::take_collab_requests(
2041 &mut door,
2042 ));
2043 }
2044 with_app_view_mut(&mut self.content, |view| {
2045 if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2046 requests.extend(content.take_collab_requests());
2047 }
2048 });
2049 requests
2050 }
2051
2052 fn handle_collab_event(&mut self, event: &ui_core::collaboration::CollabEvent) {
2053 ui_core::widget::Collaborative::handle_collab_event(self.share.remote_mut(), event);
2054 if let Some(mut door) = self.dispatch.opener.door() {
2055 ui_core::widget::Collaborative::handle_collab_event(&mut door, event);
2056 }
2057 with_app_view_mut(&mut self.content, |view| {
2058 if let Some(content) = capability_mut::<dyn ui_core::widget::Collaborative>(view) {
2059 content.handle_collab_event(event);
2060 }
2061 });
2062 }
2063}
2064
2065impl RootSearch for ResourceRoot {
2066 fn search_providers(&self) -> Vec<Box<dyn SearchProvider>> {
2067 let (state, queries) = self.search_state();
2070 let indexed = self.current_search_index(state);
2071 let mut source = ResourceSearch::new(self.resource.label, self.dispatch.app_id(), indexed);
2075 source.scopes = self.typed.search_scopes(&*self.typed.state());
2076 source.queries = queries;
2077 source.readable = self.dispatch.may_read();
2079 source.live = Some(LiveSearch {
2080 cache: Rc::clone(&self.search),
2081 answered: std::cell::RefCell::new(None),
2082 });
2083 source.opens = self
2084 .stages
2085 .opens()
2086 .then(|| tilo_actions::ActionAddress::new(self.dispatch.resource_id(), "open"));
2087 vec![Box::new(source)]
2088 }
2089
2090 fn search_provider_descriptors(
2095 &self,
2096 ) -> Vec<ui_core::search::capability::SearchProviderDescriptor> {
2097 let (state, _) = self.search_state();
2098 let generation = self.search.borrow_mut().generation_for(state);
2099 vec![ui_core::search::capability::SearchProviderDescriptor::new(
2100 self.resource.label,
2101 Some(generation),
2102 ui_core::widget::ActionDirectoryInvocationDomain::Unsupported,
2104 )]
2105 }
2106
2107 fn search_index_state(&self) -> Option<ui_core::search::index_publication::IndexState> {
2111 let (state, _) = self.search_state();
2112 let generation = self.search.borrow_mut().generation_for(state);
2113 Some(ui_core::search::index_publication::IndexState {
2114 generation,
2115 secrets_unlocked: self.secrets_unlocked(),
2116 })
2117 }
2118
2119 fn search_index_entries(&self) -> Vec<ui_core::search::index_publication::IndexEntry> {
2125 let mut entries = self.typed.search(&*self.typed.state());
2126 self.surfaces(&mut |surface| {
2127 if surface.kept_in_search_index() {
2128 entries.extend(surface.search_entries());
2129 }
2130 });
2131 entries.iter().filter_map(index_entry).collect()
2132 }
2133
2134 fn activate_search_result(
2137 &mut self,
2138 action_id: &str,
2139 doc: &mut ui_core::widget::Edits<'_>,
2140 path: &ModelPath,
2141 ) {
2142 if self.stages.open(action_id).is_err() {
2143 with_app_view_mut(&mut self.content, |view| {
2144 ui_core::search::capability::activate_search_result(view, action_id, doc, path);
2145 });
2146 }
2147 }
2148}
2149
2150impl RootPublishes for ResourceRoot {
2151 fn resources(&self, doc: &AutoCommit) -> Vec<ResourceSpec> {
2152 RootPublishes::resources_for(self, None, doc)
2153 }
2154 fn shared_resources_for(
2155 &self,
2156 participant: Option<&tilo_actions::ParticipantId>,
2157 doc: &AutoCommit,
2158 ) -> Option<Vec<ui_core::published::SharedResource>> {
2159 self.shared_resources(participant, doc)
2160 }
2161 fn resources_for(
2162 &self,
2163 participant: Option<&tilo_actions::ParticipantId>,
2164 doc: &AutoCommit,
2165 ) -> Vec<ResourceSpec> {
2166 let _viewer = participant
2169 .cloned()
2170 .map(ui_core::invocation_scope::ViewerScope::enter);
2171 let mut resource = self.current_publication(doc);
2172 let mut extras = Vec::new();
2176 for extra in ui_core::resources::resources_for(self.app_view(), participant, doc) {
2177 let extra = outside_document(extra);
2178 if extra.id == resource.id {
2179 join(&mut resource, extra);
2180 } else {
2181 extras.push(extra);
2182 }
2183 }
2184 if self.stages.opens()
2185 && !resource
2186 .actions
2187 .iter()
2188 .any(|existing| existing.id == "open")
2189 {
2190 resource = resource.with_action(open_spec());
2191 }
2192 if self.stages.opens() {
2193 offer_item_open(self.resource.opens_as_item, &mut resource);
2194 }
2195 if let Some(table) = self.resource.record_table {
2196 offer_item_reads(&mut resource, table, self.resource.kind);
2197 }
2198 let mut resources = vec![resource];
2199 resources.append(&mut extras);
2200 resources
2201 }
2202 fn resource(&self, doc: &AutoCommit, id: &str) -> Option<ResourceSpec> {
2203 RootPublishes::resource_for(self, None, doc, id)
2204 }
2205 fn resource_pending(&self, id: &str) -> bool {
2207 let mut pending = false;
2208 self.surfaces(&mut |surface| pending = pending || surface.live_row_pending(id));
2209 pending
2210 }
2211 fn resource_for(
2214 &self,
2215 participant: Option<&tilo_actions::ParticipantId>,
2216 doc: &AutoCommit,
2217 id: &str,
2218 ) -> Option<ResourceSpec> {
2219 let _viewer = participant
2220 .cloned()
2221 .map(ui_core::invocation_scope::ViewerScope::enter);
2222 if let Some(published) = find(&[self.current_publication(doc)], id) {
2225 return Some(published);
2226 }
2227 let current = if self.typed.follows(doc) {
2228 None
2229 } else {
2230 self.typed.read(doc).ok()
2231 };
2232 let state = current.unwrap_or_else(|| self.typed.state());
2233 let mut child = self.typed.child(&*state, doc, id);
2234 if self.resource.readable
2235 && let Some(child) = &mut child
2236 {
2237 offer_document_reads(child, &|id| self.typed.document_child(&*state, id));
2238 }
2239 let mut row = None;
2240 self.surfaces(&mut |surface| {
2241 if row.is_none() {
2242 row = surface.live_row(id).map(super::LiveRow::into_resource);
2243 }
2244 });
2245 let resolved = match (child, row) {
2248 (Some(mut child), Some(row)) => {
2249 join(&mut child, row);
2250 Some(child)
2251 }
2252 (child, row) => child.or(row),
2253 };
2254 if let Some(mut resolved) = resolved {
2255 self.bind_row_declarations(std::slice::from_mut(&mut resolved));
2256 if self.stages.opens() {
2257 offer_item_open(self.resource.opens_as_item, &mut resolved);
2258 }
2259 return Some(resolved);
2260 }
2261 ui_core::resources::resource_for(self.app_view(), None, doc, id)
2265 .map(super::rows::resolved_outside_document)
2266 }
2267 fn document_action_result(
2268 &self,
2269 doc: &AutoCommit,
2270 address: &tilo_actions::ActionAddress,
2271 changes: &[ui_core::automerge::Patch],
2272 ) -> Option<serde_json::Value> {
2273 if address.resource == self.dispatch.resource_id()
2275 || (self.resource.item_of)(address.resource.as_str())
2276 {
2277 if super::history::is_edit(&address.action) {
2278 return super::history::result(&address.action, doc, changes);
2279 }
2280 let minted = tilo_resource::take_minted();
2285 let answer = self
2286 .typed
2287 .answer(doc, &address.action, changes)
2288 .or_else(|| {
2289 ui_core::resources::document_action_result(
2290 self.app_view(),
2291 doc,
2292 address,
2293 changes,
2294 )
2295 });
2296 return match answer {
2297 _ if minted.is_empty() => answer,
2298 Some(serde_json::Value::Object(mut fields)) => {
2299 fields.insert("minted".into(), minted.into());
2300 Some(serde_json::Value::Object(fields))
2301 }
2302 Some(other) => Some(other),
2303 None => Some(serde_json::json!({ "minted": minted })),
2304 };
2305 }
2306 ui_core::resources::document_action_result(self.app_view(), doc, address, changes)
2307 }
2308}
2309
2310impl ui_core::history_mode::capability::DocumentHistory for ResourceRoot {
2314 fn history_registration(
2315 &self,
2316 ) -> Result<
2317 Option<ui_core::history_mode::HistoryRegistration>,
2318 ui_core::history_mode::HistoryRegistrationError,
2319 > {
2320 match &self.run.file_history {
2321 Some((source, definition)) => {
2322 let scopes = definition.scopes.iter().map(|scope| {
2323 ui_core::history_mode::HistorySourceRegistration::CheckedFiles {
2324 scope: scope.key,
2325 source: source.clone(),
2326 }
2327 });
2328 ui_core::history_mode::HistoryRegistration::new(definition, scopes.collect())
2329 .map(Some)
2330 }
2331 None => ui_core::history_mode::capability::history_registration(self.app_view()),
2332 }
2333 }
2334
2335 fn history_restore_readiness(&self, scope: &str) -> Result<(), String> {
2336 ui_core::widget::capability::capability::<
2337 dyn ui_core::history_mode::capability::DocumentHistory,
2338 >(self.app_view())
2339 .map_or(Ok(()), |content| content.history_restore_readiness(scope))
2340 }
2341
2342 fn document_history_definition(
2343 &self,
2344 ) -> Option<&'static ui_core::history_mode::HistoryDefinition> {
2345 ui_core::history_mode::capability::document_history_definition(self.app_view())
2346 }
2347
2348 fn physical_text_policy(
2349 &self,
2350 previous: Option<&tilo_actions::EncodedAction>,
2351 next: &tilo_actions::EncodedAction,
2352 ) -> (bool, bool) {
2353 self.typed.text_policy(previous, next)
2354 }
2355
2356 fn document_action_applied(
2357 &mut self,
2358 action: &tilo_actions::EncodedAction,
2359 before: &[ui_core::automerge::ChangeHash],
2360 doc: &ui_core::automerge::AutoCommit,
2361 path: &ui_core::automerge::ObjId,
2362 ) {
2363 if let Some(participant) = ui_core::invocation_scope::current_participant() {
2366 self.typed.note_applied(&participant, action);
2367 }
2368 with_app_view_mut(&mut self.content, |view| {
2369 if let Some(content) = ui_core::widget::capability::capability_mut::<
2370 dyn ui_core::history_mode::capability::DocumentHistory,
2371 >(view)
2372 {
2373 content.document_action_applied(action, before, doc, path);
2374 }
2375 });
2376 }
2377}
2378
2379impl ui_core::undo::CommittedActions for ResourceRoot {
2380 fn take_committed_action(&mut self) -> Option<Vec<ui_core::automerge::ChangeHash>> {
2381 self.dispatch.take_committed()
2382 }
2383 fn keeps_undo_step_open(&self) -> bool {
2384 self.dispatch.keeps_step_open()
2385 }
2386}
2387
2388impl RootApp for ResourceRoot {
2389 fn app_kind(&self) -> AppKind {
2390 AppKind::Editor
2391 }
2392
2393 fn app_title(&self) -> &str {
2394 self.title
2395 }
2396
2397 fn app_icon(&self) -> IconDescriptor {
2398 self.icon.clone()
2399 }
2400
2401 fn device_shell_operation(
2402 &mut self,
2403 operation: &ui_core::floating_surface::DeviceShellOperation,
2404 ) -> Result<serde_json::Value, String> {
2405 fn perform(
2406 widget: &mut dyn Widget,
2407 resource: std::any::TypeId,
2408 operation: &ui_core::floating_surface::DeviceShellOperation,
2409 ) -> Option<Result<serde_json::Value, String>> {
2410 if let Some(surface) = surface_mut(widget, resource)
2411 && surface.supports_device_shell()
2412 {
2413 return Some(surface.device_shell_operation(operation));
2414 }
2415 widget
2416 .children_mut()
2417 .into_iter()
2418 .find_map(|child| perform(child, resource, operation))
2419 }
2420 perform(self.content.as_mut(), self.resource.id, operation)
2421 .unwrap_or_else(|| Err("Terminal is not available".into()))
2422 }
2423
2424 fn request_device_shell(&mut self) -> bool {
2425 fn request(widget: &mut dyn Widget, resource: std::any::TypeId) -> bool {
2426 if let Some(surface) = surface_mut(widget, resource)
2427 && surface.request_device_shell()
2428 {
2429 return true;
2430 }
2431 widget
2432 .children_mut()
2433 .into_iter()
2434 .any(|child| request(child, resource))
2435 }
2436 request(self.content.as_mut(), self.resource.id)
2437 }
2438
2439 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
2440 fn take(
2441 widget: &mut dyn Widget,
2442 resource: std::any::TypeId,
2443 ) -> Option<ui_core::widget::AppHostIntent> {
2444 if let Some(surface) = surface_mut(widget, resource)
2445 && let Some(intent) = surface.take_host_intent()
2446 {
2447 return Some(intent);
2448 }
2449 if let Some(intent) = ui_core::component_lifecycle::take_host_intent(widget) {
2451 return Some(intent);
2452 }
2453 widget
2454 .children_mut()
2455 .into_iter()
2456 .find_map(|child| take(child, resource))
2457 }
2458 take(self.content.as_mut(), self.resource.id)
2459 }
2460
2461 fn take_save_retry(&mut self) -> bool {
2462 self.save.take_record_retry()
2463 }
2464
2465 fn record_save_status(&mut self, status: RecordSaveStatus) {
2466 self.save.record(&status, ui_core::time::now());
2467 self.saved.set(status);
2468 }
2469
2470 fn record_refused(&mut self, refusal: &ui_core::record::RecordRefusal) {
2474 self.dispatch.refuse_by_workspace(refusal);
2475 self.typed.represent();
2476 }
2477
2478 fn take_notices(&mut self) -> Vec<String> {
2479 self.dispatch.take_notices()
2480 }
2481
2482 fn record_said(&mut self, text: &str) {
2483 self.dispatch.kept(text);
2484 }
2485
2486 fn capture_application_view(&self) -> Result<Option<ApplicationLocalView>, ThawError> {
2489 capability::<dyn ApplicationViewState>(self.app_view())
2490 .map_or(Ok(None), ApplicationViewState::capture_application_view)
2491 }
2492
2493 fn restore_application_view(
2494 &mut self,
2495 local: &ApplicationLocalView,
2496 doc: &AutoCommit,
2497 ) -> Result<ApplicationFocusRestore, ThawError> {
2498 with_app_view_mut(&mut self.content, |view| {
2499 match capability_mut::<dyn ApplicationViewState>(view) {
2500 Some(view) => view.restore_application_view(local, doc),
2501 None => Err(ThawError::State(
2502 "application-local restoration unsupported".into(),
2503 )),
2504 }
2505 })
2506 }
2507
2508 fn finish_application_view_restore(&mut self) -> Result<(), ThawError> {
2509 with_app_view_mut(&mut self.content, |view| {
2510 capability_mut::<dyn ApplicationViewState>(view).map_or(
2511 Ok(()),
2512 ApplicationViewState::finish_application_view_restore,
2513 )
2514 })
2515 }
2516}
2517
2518#[cfg(test)]
2519mod index_entry_tests;
2520
2521#[cfg(test)]
2522#[cfg(not(target_arch = "wasm32"))]
2523mod search_tests;
2524
2525fn unreadable_state(reason: &str) -> ui_core::widget::EmptyState {
2530 ui_core::widget::EmptyState::new(ui_core::i18n::I18nManager::lookup_plain(
2531 "framework-unreadable",
2532 ))
2533 .with_icon(ui_core::icons::Icon::CircleAlert)
2534 .with_message(ui_core::i18n::I18nManager::lookup_plain(
2535 "framework-unreadable-hint",
2536 ))
2537 .with_details(reason)
2538}
2539
2540pub(crate) struct Unopened {
2544 title: &'static str,
2545 icon: IconDescriptor,
2546 resource: ResourceSpec,
2549 shown: ui_core::widget::EmptyState,
2550}
2551
2552impl Unopened {
2553 pub(crate) fn new(
2556 title: &'static str,
2557 icon: IconDescriptor,
2558 (kind, label): (&'static str, &'static str),
2559 declarations: &[tilo_resource::Declaration],
2560 reason: &str,
2561 ) -> Self {
2562 let mut resource =
2563 ResourceSpec::new(kind, kind, tilo_resource::label::localized(kind, label));
2564 for decl in declarations {
2565 resource = resource.with_action(tilo_resource::declaration_spec(decl));
2566 }
2567 tilo_resource::withhold_document_actions(&mut resource, reason);
2568 Self {
2569 title,
2570 icon,
2571 shown: unreadable_state(reason),
2572 resource,
2573 }
2574 }
2575}
2576
2577impl Unopened {
2578 pub(crate) fn boxed(self) -> Box<dyn AppWidget> {
2581 Box::new(self)
2582 }
2583}
2584
2585impl Decorator for Unopened {
2586 ui_core::provides!(dyn ui_core::resources::PublishesResources);
2587
2588 type Inner = ui_core::widget::EmptyState;
2589
2590 fn inner(&self) -> &ui_core::widget::EmptyState {
2591 &self.shown
2592 }
2593
2594 fn inner_mut(&mut self) -> &mut ui_core::widget::EmptyState {
2595 &mut self.shown
2596 }
2597}
2598
2599impl ui_core::resources::PublishesResources for Unopened {
2600 fn resources(&self, _doc: &AutoCommit) -> Vec<ResourceSpec> {
2601 vec![self.resource.clone()]
2602 }
2603}
2604
2605impl AppWidget for Unopened {
2606 fn app_kind(&self) -> AppKind {
2607 AppKind::Editor
2608 }
2609
2610 fn app_title(&self) -> &str {
2611 self.title
2612 }
2613
2614 fn app_icon(&self) -> IconDescriptor {
2615 self.icon.clone()
2616 }
2617}
2618
2619#[cfg(test)]
2620mod tests {
2621 use super::*;
2622 use ui_core::search::{DEFAULT_SEARCH_RESULT_LIMIT, SearchQuery};
2623
2624 #[test]
2625 fn child_document_reads_are_bound_to_their_own_resource() {
2626 let mut child = ResourceSpec::new("book/page/body", "text", "Body");
2627 child.children.push(ResourceSpec::new(
2628 "private-child",
2629 "account",
2630 "Secret account",
2631 ));
2632 offer_document_reads(&mut child, &|id| id == "book/page/body");
2633 assert!(
2634 child.children[0].actions.is_empty(),
2635 "eligibility is not inherited"
2636 );
2637 let read = &child.actions[0];
2638 assert!(read.contract.reads_document());
2639 let encoded = read.build_json(&serde_json::Map::new()).unwrap();
2640 assert_eq!(encoded.type_id, READ_TYPE);
2641 let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2642 assert_eq!(payload["item"], "book/page/body");
2643 assert!(
2644 read.build_json(
2645 serde_json::json!({"item": "another/book"})
2646 .as_object()
2647 .unwrap()
2648 )
2649 .is_err()
2650 );
2651 let encoded = read
2652 .build_json(serde_json::json!({"version": "past"}).as_object().unwrap())
2653 .unwrap();
2654 let payload: serde_json::Value = tilo_actions::decode_json(&encoded.payload).unwrap();
2655 assert_eq!(payload["item"], "book/page/body");
2656 assert_eq!(payload["version"], "past");
2657 }
2658
2659 #[test]
2662 fn the_reserved_ids_are_what_the_framework_publishes() {
2663 let reserved = reserved_action_ids();
2664 for id in [
2665 "apply",
2666 "group",
2667 "field-edit",
2668 "open",
2669 "read",
2670 "undo",
2671 "redo",
2672 "restore",
2673 "restore-reviewed",
2674 "review-restore",
2675 "checkpoint",
2676 "history",
2677 "diff",
2678 "render",
2679 ] {
2680 assert!(
2681 reserved.iter().any(|kept| kept == id),
2682 "{id} in {reserved:?}"
2683 );
2684 }
2685 assert!(
2686 !reserved.iter().any(|kept| kept == "rename"),
2687 "an app's own verb name is not kept"
2688 );
2689 }
2690
2691 #[test]
2692 fn a_search_entry_naming_an_action_is_a_hit_that_invokes_it() {
2693 let open = tilo_actions::ActionAddress::new("notes.page:1", "open");
2694 let search = ResourceSearch {
2695 name: "Notes",
2696 app: "so.tilo.notes",
2697 indexed: Arc::new(IndexedEntries::built(
2698 vec![
2699 SearchEntry::new("Groceries", "milk and eggs").invoking(open.clone()),
2700 SearchEntry::new("Garden", "roses").anchored("page:2"),
2701 ],
2702 0,
2703 )),
2704 scopes: Vec::new(),
2705 queries: Vec::new(),
2706 readable: true,
2707 live: None,
2708 opens: None,
2709 };
2710 let groceries = search.search("groceries");
2711 assert_eq!(groceries.len(), 1);
2712 assert_eq!(groceries[0].action_address(), Some(open));
2713 let garden = search.search("garden");
2714 assert_eq!(garden.len(), 1);
2715 assert_eq!(garden[0].action_address(), None);
2716 assert_eq!(garden[0].action_id, "page:2");
2717 }
2718
2719 #[test]
2720 fn a_search_entry_reads_as_its_group_subtitle_and_icon() {
2721 let reveal = tilo_actions::ActionAddress::new("video.marker:1", "reveal");
2722 let search = ResourceSearch {
2723 name: "Project",
2724 app: "so.tilo.project",
2725 indexed: Arc::new(IndexedEntries::built(
2726 vec![
2727 SearchEntry::new("Sunset", "the last light")
2728 .invoking(reveal)
2729 .grouped("Timeline markers")
2730 .subtitled("0:21 · Drone")
2731 .with_icon("star")
2732 .boosted(5),
2733 SearchEntry::new("Sunset take", ""),
2734 ],
2735 0,
2736 )),
2737 scopes: Vec::new(),
2738 queries: Vec::new(),
2739 readable: true,
2740 live: None,
2741 opens: None,
2742 };
2743 let hits = search.search("sunset");
2744 let marker = hits
2745 .iter()
2746 .find(|hit| hit.title == "Sunset")
2747 .expect("the marker is found");
2748 assert_eq!(marker.kind, "Timeline markers");
2749 assert_eq!(marker.subtitle.as_deref(), Some("0:21 · Drone"));
2750 assert_eq!(marker.icon, Some(ui_core::icons::Icon::Star));
2751 let plain = hits
2752 .iter()
2753 .find(|hit| hit.title == "Sunset take")
2754 .expect("the plain entry is found");
2755 assert_eq!(plain.kind, "Project", "an ungrouped hit is the resource's");
2756 assert!(plain.preview.is_none(), "no text, no preview");
2757 assert!(
2759 search
2760 .search("drone")
2761 .iter()
2762 .any(|hit| hit.title == "Sunset")
2763 );
2764 }
2765
2766 #[test]
2767 fn every_entry_stays_findable_page_by_page() {
2768 let search = ResourceSearch::new(
2769 "Tracks",
2770 "so.tilo.test",
2771 Arc::new(IndexedEntries::built(
2772 (0..257)
2773 .map(|n| SearchEntry::new(format!("Common track {n}"), ""))
2774 .collect(),
2775 0,
2776 )),
2777 );
2778 let mut cursor = None;
2779 let mut seen = std::collections::HashSet::new();
2780 loop {
2781 let page = search.search_page(&SearchQuery::new("common"), cursor.as_ref(), 17, 5);
2782 assert!(page.results.len() <= 5);
2783 for hit in &page.results {
2784 assert!(seen.insert(hit.title.clone()), "{} came twice", hit.title);
2785 }
2786 let Some(next) = page.next else { break };
2787 assert!(search.action_directory_cursor_is_current(&next));
2788 cursor = Some(next);
2789 }
2790 assert_eq!(seen.len(), 257, "every match is reachable");
2791 }
2792
2793 #[test]
2794 fn no_caller_materialises_more_than_one_page_at_once() {
2795 let search = ResourceSearch::new(
2796 "Tables",
2797 "so.tilo.test",
2798 Arc::new(IndexedEntries::built(
2799 (0..300)
2800 .map(|n| SearchEntry::new(format!("table {n}"), ""))
2801 .collect(),
2802 0,
2803 )),
2804 );
2805 assert_eq!(
2806 search.search_limited("", usize::MAX).len(),
2807 DEFAULT_SEARCH_RESULT_LIMIT
2808 );
2809 assert_eq!(
2810 search
2811 .search_page(&SearchQuery::new(""), None, usize::MAX, usize::MAX)
2812 .results
2813 .len(),
2814 DEFAULT_SEARCH_RESULT_LIMIT
2815 );
2816 }
2817
2818 struct Chapter(Vec<&'static str>);
2821
2822 impl tilo_resource::SearchText for Chapter {
2823 fn segment_count(&self) -> usize {
2824 self.0.len()
2825 }
2826
2827 fn piece_count(&self, segment: usize) -> usize {
2828 usize::from(segment < self.0.len())
2829 }
2830
2831 fn piece(&self, segment: usize, piece: usize) -> Option<tilo_resource::SearchPiece<'_>> {
2832 let text = self.0.get(segment)?;
2833 (piece == 0).then_some(tilo_resource::SearchPiece {
2834 text,
2835 space_before: segment > 0,
2836 })
2837 }
2838
2839 fn locates(&self, _segment: usize) -> bool {
2840 true
2841 }
2842
2843 fn location(&self, segment: usize) -> Option<SearchEntry> {
2844 Some(
2845 SearchEntry::new(format!("Paragraph {}", segment.saturating_add(1)), "").invoking(
2846 tilo_actions::ActionAddress::new(format!("book.location:{segment}"), "open"),
2847 ),
2848 )
2849 }
2850 }
2851
2852 #[test]
2853 fn a_match_in_a_body_is_the_place_it_is_in() {
2854 let body = tilo_resource::SearchBody::new(Arc::new(Chapter(vec![
2855 "It was a dark and stormy night.",
2856 "The lighthouse keeper counted the waves.",
2857 ])));
2858 let search = ResourceSearch::new(
2859 "Library",
2860 "so.tilo.test",
2861 Arc::new(IndexedEntries::built(
2862 vec![
2863 SearchEntry::new("Storm Tales", "")
2864 .invoking(tilo_actions::ActionAddress::new("book:1", "open"))
2865 .grouped("Books")
2866 .with_body(body.clone()),
2867 ],
2868 0,
2869 )),
2870 );
2871 let hits = search.search("lighthouse keeper");
2872 let place = hits
2873 .iter()
2874 .find(|hit| hit.title == "Paragraph 2")
2875 .expect("the paragraph the words are in is a hit");
2876 assert_eq!(
2877 place.action_address(),
2878 Some(tilo_actions::ActionAddress::new("book.location:1", "open"))
2879 );
2880 assert_eq!(place.kind, "Books", "a location is listed with its book");
2881 assert!(
2882 search
2883 .search("storm tales")
2884 .iter()
2885 .any(|hit| hit.title == "Storm Tales"),
2886 "the book itself is found by its title"
2887 );
2888 assert_eq!(body, body.clone(), "a body is the text it shares");
2889 }
2890
2891 #[test]
2892 fn a_field_ranks_by_its_own_weight() {
2893 use tilo_resource::SearchWeight;
2894 let search = ResourceSearch::new(
2895 "Mail",
2896 "so.tilo.test",
2897 Arc::new(IndexedEntries::built(
2898 vec![
2899 SearchEntry::new("Lunch plans", "")
2900 .field("[email protected]", SearchWeight::Metadata),
2901 SearchEntry::new("Quarterly report", "")
2902 .field("[email protected]", SearchWeight::Identity),
2903 ],
2904 0,
2905 )),
2906 );
2907 let hits = search.search("[email protected]");
2908 assert_eq!(hits.len(), 2, "both are found by the address");
2909 assert_eq!(
2910 hits[0].title, "Quarterly report",
2911 "a match in an identity outranks one in metadata"
2912 );
2913 }
2914}
2915
2916mod publication_tests;