1use std::cell::RefCell;
5use std::rc::{Rc, Weak};
6use ui_core::widget::{AppModel, ModelPath};
7
8use tilo_resource::{ActionDecl, Effect, NoEffect, Resource, SearchEntry};
9use ui_core::automerge::transaction::Transactable as _;
10use ui_core::automerge::{AutoCommit, ObjId};
11use ui_core::host_operation::{ResourceOperation, ResourceOperationStart};
12use ui_core::presence::PresenceKey;
13use ui_core::widget::{Decorator, Widget};
14use ui_core::{PresencePayload, WakeHandle};
15
16use super::controls::Actions;
17use super::dispatch::Dispatcher;
18use super::effect::{EffectAnswer, EffectCx, EffectRunner, EffectStatus, Effects, Jobs, OwnedJobs};
19use super::live::Live;
20use super::rows::{LiveRow, outside_document, resolved_outside_document};
21
22pub trait ResourceSurface<R: Resource, E: Effect = NoEffect>: Widget {
83 fn present(&mut self, state: &Rc<R::State>);
85
86 fn supports_device_shell(&self) -> bool {
87 false
88 }
89
90 fn device_shell_operation(
95 &mut self,
96 _operation: &ui_core::floating_surface::DeviceShellOperation,
97 ) -> Result<serde_json::Value, String> {
98 Err("This surface has no device shell".into())
99 }
100
101 fn request_device_shell(&mut self) -> bool {
103 false
104 }
105
106 fn search_query(&mut self, query: &ui_core::search::SearchQuery) {
112 let _ = query;
113 }
114
115 fn take_save_retry(&mut self) -> bool {
120 false
121 }
122
123 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
126 None
127 }
128
129 fn attach(&mut self, cx: SurfaceCx<R>) {
132 let _ = cx;
133 }
134
135 fn landed(&mut self, participant: &tilo_actions::ParticipantId, place: &tilo_resource::Place) {
144 let _ = (participant, place);
145 }
146
147 fn read_only_changed(&mut self, read_only: bool) {
151 let _ = read_only;
152 }
153
154 fn stage(&mut self, scenario: Option<&str>) {
163 let _ = scenario;
164 }
165
166 fn effect(&mut self, effect: E, cx: &mut EffectCx<R, Self>) -> EffectAnswer
177 where
178 Self: Sized,
179 {
180 let _ = cx;
181 Err(format!("`{}` has no handler", effect.id()))
182 }
183
184 fn effects(&self) -> Vec<EffectStatus> {
189 Vec::new()
190 }
191
192 fn availability(&self) -> Vec<EffectStatus> {
198 Vec::new()
199 }
200
201 fn bound_declarations(&self) -> Vec<ActionDecl<R>> {
211 Vec::new()
212 }
213
214 fn live_rows(&self) -> Vec<LiveRow> {
219 Vec::new()
220 }
221
222 fn live_row(&self, id: &str) -> Option<LiveRow> {
226 let _ = id;
227 None
228 }
229
230 fn row_declarations(&self, row: &str) -> Vec<ActionDecl<R>> {
236 let _ = row;
237 Vec::new()
238 }
239
240 fn search_entries(&self) -> Vec<SearchEntry> {
246 Vec::new()
247 }
248}
249
250fn only_through_actions(
254 state: &mut ui_core::widget::Edits<'_>,
255 path: &ModelPath,
256 pass: &str,
257 forward: impl FnOnce(&mut ui_core::widget::Edits<'_>, &ModelPath),
258) {
259 let before = ui_core::doc::document(state, path).0.pending_ops();
260 forward(state, path);
261 refuse_raw_writes(ui_core::doc::document(state, path).0, before, pass);
262}
263
264fn refuse_raw_writes(doc: &AutoCommit, before: usize, pass: &str) {
269 if before != 0 || doc.pending_ops() == 0 {
270 return;
271 }
272 let dropped = doc.pending_ops();
273 tracing::error!(
274 pass,
275 dropped,
276 "a surface wrote the document outside an action; the write was refused"
277 );
278 debug_assert_eq!(
279 dropped, 0,
280 "a surface wrote the document outside an action during `{pass}`"
281 );
282}
283
284pub(crate) trait SurfacePublication {
287 fn resource(&self) -> std::any::TypeId;
290 fn bound_declarations(&self, into: &mut dyn std::any::Any);
293 fn availability(&self) -> Vec<EffectStatus>;
294 fn live_rows(&self) -> Vec<LiveRow>;
295 fn live_row(&self, id: &str) -> Option<LiveRow>;
296 fn offers_on_row(&self, row: &str) -> bool;
298 fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool;
301 fn search_entries(&self) -> Vec<SearchEntry>;
302 fn supports_device_shell(&self) -> bool;
303 fn device_shell_operation(
304 &mut self,
305 operation: &ui_core::floating_surface::DeviceShellOperation,
306 ) -> Result<serde_json::Value, String>;
307 fn request_device_shell(&mut self) -> bool;
308 fn search_queries(&self) -> SearchQueries;
309 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent>;
310 fn take_save_retry(&mut self) -> bool;
311}
312
313#[derive(Clone, Default)]
315pub(crate) struct SearchQueries(Rc<RefCell<QueryDelivery>>);
316
317#[derive(Default)]
318struct QueryDelivery {
319 last: Option<ui_core::search::SearchQuery>,
320 pending: Option<ui_core::search::SearchQuery>,
321}
322
323impl SearchQueries {
324 pub(crate) fn submit(&self, query: &ui_core::search::SearchQuery) {
325 if query
326 .text()
327 .chars()
328 .nth(ui_core::search::MAX_SEARCH_QUERY_SCALARS)
329 .is_some()
330 {
331 return;
332 }
333 let mut delivery = self.0.borrow_mut();
334 if delivery.last.as_ref() != Some(query) {
335 delivery.last = Some(query.clone());
336 delivery.pending = Some(query.clone());
337 }
338 }
339
340 fn take(&self) -> Option<ui_core::search::SearchQuery> {
341 self.0.borrow_mut().pending.take()
342 }
343}
344
345pub struct SurfaceCx<R: Resource> {
347 pub(crate) env: super::env::RecordEnv<R>,
348 pub(crate) jobs: Jobs<R>,
349 pub(crate) actions: Actions<R>,
350}
351
352impl<R: Resource> std::ops::Deref for SurfaceCx<R> {
353 type Target = super::env::RecordEnv<R>;
354 fn deref(&self) -> &super::env::RecordEnv<R> {
355 &self.env
356 }
357}
358
359impl<R: Resource> SurfaceCx<R> {
360 pub fn record(&self) -> Option<&ui_core::record::RecordId> {
364 self.run.launched.record.as_ref()
365 }
366
367 pub fn opener(&self) -> Option<super::OpenHere> {
371 self.run.opener.clone()
372 }
373
374 pub fn jobs(&self) -> Jobs<R> {
378 self.jobs.clone()
379 }
380
381 pub fn actions(&self) -> Actions<R> {
386 self.actions.clone()
387 }
388
389 pub fn live(&self) -> Live<R> {
394 self.live.clone()
395 }
396
397 pub fn file_history(&self) -> Option<ui_core::history_mode::checked_files::FileHistorySource> {
403 self.run
404 .file_history
405 .as_ref()
406 .map(|(source, _)| source.clone())
407 }
408
409 pub fn app_id(&self) -> &'static str {
412 self.dispatch.app_id()
413 }
414
415 pub fn secrets(&self) -> ui_core::secrets::SecretStore {
418 self.storage.storage().secrets()
419 }
420
421 pub fn files(&self) -> ui_core::storage::AppFiles {
423 self.storage.storage().files()
424 }
425
426 pub fn waker(&self) -> WakeHandle {
430 self.effects
431 .env()
432 .map_or_else(WakeHandle::new, |env| env.wake.clone())
433 }
434}
435
436#[derive(Clone, Debug, PartialEq)]
439pub enum ResourceSelection {
440 Item(String),
442 Caret { item: String, at: usize },
444 Range {
446 item: String,
447 start: usize,
448 end: usize,
449 },
450 Pointer { item: String, x: f32, y: f32 },
454}
455
456impl ResourceSelection {
457 pub fn presence<R: Resource>(&self, app_id: &str) -> (PresenceKey, PresencePayload) {
460 self.presence_of(R::KIND, app_id)
461 }
462
463 pub fn presence_of(&self, kind: &str, app_id: &str) -> (PresenceKey, PresencePayload) {
465 let key = |item: &str| PresenceKey::Resource {
466 app: app_id.to_owned(),
467 id: item.to_owned(),
468 kind: kind.to_owned(),
469 };
470 match self {
471 Self::Item(item) => (key(item), PresencePayload::Selection { start: 0, end: 0 }),
472 Self::Caret { item, at } => (key(item), PresencePayload::Caret { pos: *at }),
473 Self::Range { item, start, end } => (
474 key(item),
475 PresencePayload::Selection {
476 start: *start,
477 end: *end,
478 },
479 ),
480 Self::Pointer { item, x, y } => (key(item), PresencePayload::Pointer { x: *x, y: *y }),
481 }
482 }
483}
484
485impl From<tilo_resource::Place> for ResourceSelection {
486 fn from(place: tilo_resource::Place) -> Self {
487 match place {
488 tilo_resource::Place::Item(item) => Self::Item(item),
489 tilo_resource::Place::Caret { item, at } => Self::Caret { item, at },
490 tilo_resource::Place::Range { item, start, end } => Self::Range { item, start, end },
491 }
492 }
493}
494
495#[repr(C)]
504pub(crate) struct SurfaceHost<R: Resource, S, E> {
505 surface: S,
506 search_queries: SearchQueries,
507 live: Live<R>,
508 presented: u64,
509 landings: u64,
511 dispatch: Dispatcher<R>,
512 read_only: bool,
513 staging: Staging,
515 effects: Option<EffectRunner<R, S, E>>,
518 statuses: Rc<RefCell<Vec<EffectStatus>>>,
520 jobs: OwnedJobs<R, S>,
522}
523
524pub(crate) type Staging = Rc<RefCell<Option<Option<String>>>>;
527
528impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceHost<R, S, E> {
529 pub(crate) fn new(
530 live: Live<R>,
531 dispatch: Dispatcher<R>,
532 effects: &Effects<R>,
533 jobs: Jobs<R>,
534 alive: Weak<()>,
535 surface: S,
536 ) -> Self {
537 let statuses = Rc::new(RefCell::new(surface.effects()));
538 let runner = effects.env().map(|env| EffectRunner::new(env.clone()));
539 if !E::declarations().is_empty() {
542 let published = Rc::clone(&statuses);
543 effects.publish::<E>(alive, move || {
544 super::effect::published_specs::<E>(&published.borrow())
545 });
546 }
547 Self {
548 presented: live.revision(),
549 landings: live.landings_since(u64::MAX).1,
550 live,
551 read_only: dispatch.read_only(),
552 dispatch,
553 staging: Staging::default(),
554 effects: runner,
555 statuses,
556 jobs: OwnedJobs::new(jobs),
557 surface,
558 search_queries: SearchQueries::default(),
559 }
560 }
561
562 pub(crate) fn staging(&self) -> Staging {
563 Rc::clone(&self.staging)
564 }
565}
566
567pub(crate) trait SurfaceDriver: SurfacePublication {
570 fn surface(&self) -> &(dyn Widget + 'static);
572 fn surface_mut(&mut self) -> &mut (dyn Widget + 'static);
573 fn surface_any(&self) -> &dyn std::any::Any;
576 fn surface_any_mut(&mut self) -> &mut dyn std::any::Any;
577 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool;
580 fn sync(&mut self, state: &AppModel, path: &ModelPath);
582 fn start_effect(
587 &mut self,
588 op: &ResourceOperation<'_>,
589 doc: &AutoCommit,
590 ) -> Result<Option<ResourceOperationStart>, String>;
591 fn poll_effect(
593 &mut self,
594 invocation: tilo_actions::InvocationId,
595 cancellation_requested: bool,
596 ) -> Option<tilo_actions::InvocationUpdate>;
597}
598
599impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceDriver
600 for SurfaceHost<R, S, E>
601{
602 fn surface(&self) -> &(dyn Widget + 'static) {
603 &self.surface
604 }
605
606 fn surface_mut(&mut self) -> &mut (dyn Widget + 'static) {
607 &mut self.surface
608 }
609
610 fn surface_any(&self) -> &dyn std::any::Any {
611 &self.surface
612 }
613
614 fn surface_any_mut(&mut self) -> &mut dyn std::any::Any {
615 &mut self.surface
616 }
617
618 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
619 self.live.revision() == self.presented
620 && self.staging.borrow().is_none()
621 && self.search_queries.0.borrow().pending.is_none()
622 && self.dispatch.read_only() == self.read_only
623 && self.surface.view_only_scroll(event)
624 }
625
626 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
627 let read_only = self.dispatch.read_only();
628 if read_only != self.read_only {
629 self.read_only = read_only;
630 self.surface.read_only_changed(read_only);
631 }
632 if self.live.revision() != self.presented {
633 self.presented = self.live.revision();
634 self.surface.present(&self.live.get());
635 }
636 let (landed, next) = self.live.landings_since(self.landings);
637 self.landings = next;
638 for (participant, place) in &landed {
639 self.surface.landed(participant, place);
640 }
641 let staged = self.staging.borrow_mut().take();
642 if let Some(scenario) = staged {
643 self.surface.stage(scenario.as_deref());
644 }
645 if let Some(query) = self.search_queries.take()
646 && self.effects.is_some()
647 {
648 self.surface.search_query(&query);
649 }
650 self.jobs.poll(&mut self.surface);
651 self.surface.sync(state, path);
652 if self.effects.is_some() {
653 let now = self.surface.effects();
654 if *self.statuses.borrow() != now {
655 *self.statuses.borrow_mut() = now;
656 }
657 }
658 }
659 fn start_effect(
660 &mut self,
661 op: &ResourceOperation<'_>,
662 doc: &AutoCommit,
663 ) -> Result<Option<ResourceOperationStart>, String> {
664 match self.effects.as_mut() {
665 Some(runner) => runner.start(op, &mut self.surface, Some(doc), S::effect),
666 None => Ok(None),
667 }
668 }
669
670 fn poll_effect(
671 &mut self,
672 invocation: tilo_actions::InvocationId,
673 cancellation_requested: bool,
674 ) -> Option<tilo_actions::InvocationUpdate> {
675 self.effects
676 .as_mut()?
677 .poll(invocation, cancellation_requested, &mut self.surface)
678 }
679}
680
681pub(crate) struct HostedSurface {
684 host: Box<dyn SurfaceDriver>,
685}
686
687impl HostedSurface {
688 #[inline(never)]
692 pub(crate) fn element(host: Box<dyn SurfaceDriver>) -> ui_core::widget::Element {
693 Box::new(Self { host })
694 }
695}
696
697impl Decorator for HostedSurface {
701 fn provide<'a>(&'a self, request: &mut ui_core::widget::capability::Request<'a, '_>) {
702 if request.wants::<dyn SurfacePublication>() {
703 request.provide::<dyn SurfacePublication>(self);
704 return;
705 }
706 if request.wants::<dyn ui_core::resources::PublishesResources>() {
707 request.provide::<dyn ui_core::resources::PublishesResources>(self);
708 return;
709 }
710 self.host.surface().provide(request);
711 }
712
713 fn provide_mut<'a>(
714 &'a mut self,
715 request: &mut ui_core::widget::capability::RequestMut<'a, '_>,
716 ) {
717 if request.wants::<dyn SurfacePublication>() {
718 request.provide::<dyn SurfacePublication>(self);
719 return;
720 }
721 if request.wants::<dyn ui_core::resources::PublishesResources>() {
722 request.provide::<dyn ui_core::resources::PublishesResources>(self);
723 return;
724 }
725 self.host.surface_mut().provide_mut(request);
726 }
727
728 type Inner = dyn Widget;
729
730 fn inner(&self) -> &(dyn Widget + 'static) {
731 self.host.surface()
732 }
733
734 fn inner_mut(&mut self) -> &mut (dyn Widget + 'static) {
735 self.host.surface_mut()
736 }
737
738 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
739 self.host.view_only_scroll(event)
740 }
741
742 fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
745 self.host.surface_mut().take_structure_change()
746 }
747
748 fn as_any(&self) -> Option<&dyn std::any::Any> {
751 Some(self.host.surface_any())
752 }
753
754 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
755 Some(self.host.surface_any_mut())
756 }
757
758 fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ObjId) {
759 self.host.surface_mut().settle(edits, path);
760 }
761
762 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
763 self.host.sync(state, path);
764 }
765
766 fn start_operation(
767 &mut self,
768 op: &ResourceOperation<'_>,
769 doc: &mut ui_core::widget::Edits<'_>,
770 path: &ObjId,
771 ) -> Result<Option<ResourceOperationStart>, String> {
772 if let Some(started) = self.host.start_effect(op, doc.doc())? {
773 return Ok(Some(started));
774 }
775 let before = doc.pending_ops();
779 let started =
780 ui_core::host_operation::start_in_children(self.host.surface_mut(), op, doc, path);
781 refuse_raw_writes(doc, before, "start_operation");
782 started
783 }
784
785 fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
786 self.host.surface_mut().update(state, path)
787 }
788
789 fn handle_event(
790 &mut self,
791 event: &ui_core::winit::event::WindowEvent,
792 ctx: &mut ui_core::widget::EventCtx<'_>,
793 ) {
794 let before = ctx.state.pending_ops();
795 self.host.surface_mut().handle_event(event, ctx);
796 refuse_raw_writes(ctx.state.doc(), before, "handle_event");
797 }
798
799 fn perform_semantic_action(
800 &mut self,
801 target: &str,
802 action: &ui_core::a11y::Action,
803 state: &mut ui_core::widget::Edits<'_>,
804 path: &ModelPath,
805 ) -> bool {
806 let surface = self.host.surface_mut();
807 let mut performed = false;
808 only_through_actions(state, path, "perform_semantic_action", |state, path| {
809 performed = surface.perform_semantic_action(target, action, state, path);
810 });
811 performed
812 }
813
814 fn poll_resource_operation(
815 &mut self,
816 invocation: tilo_actions::InvocationId,
817 cancellation_requested: bool,
818 doc: &AutoCommit,
819 path: &ObjId,
820 ) -> Option<tilo_actions::InvocationUpdate> {
821 if let Some(update) = self.host.poll_effect(invocation, cancellation_requested) {
822 return Some(update);
823 }
824 self.host
825 .surface_mut()
826 .children_mut()
827 .into_iter()
828 .find_map(|child| {
829 child.poll_resource_operation(invocation, cancellation_requested, doc, path)
830 })
831 }
832}
833
834impl SurfacePublication for HostedSurface {
835 fn resource(&self) -> std::any::TypeId {
836 self.host.resource()
837 }
838 fn bound_declarations(&self, into: &mut dyn std::any::Any) {
839 self.host.bound_declarations(into);
840 }
841 fn availability(&self) -> Vec<EffectStatus> {
842 self.host.availability()
843 }
844 fn live_rows(&self) -> Vec<LiveRow> {
845 self.host.live_rows()
846 }
847 fn live_row(&self, id: &str) -> Option<LiveRow> {
848 self.host.live_row(id)
849 }
850 fn offers_on_row(&self, row: &str) -> bool {
851 self.host.offers_on_row(row)
852 }
853 fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool {
854 self.host.row_declarations(row, into)
855 }
856 fn search_entries(&self) -> Vec<SearchEntry> {
857 self.host.search_entries()
858 }
859 fn supports_device_shell(&self) -> bool {
860 self.host.supports_device_shell()
861 }
862 fn device_shell_operation(
863 &mut self,
864 operation: &ui_core::floating_surface::DeviceShellOperation,
865 ) -> Result<serde_json::Value, String> {
866 self.host.device_shell_operation(operation)
867 }
868 fn request_device_shell(&mut self) -> bool {
869 self.host.request_device_shell()
870 }
871 fn search_queries(&self) -> SearchQueries {
872 self.host.search_queries()
873 }
874 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
875 self.host.take_host_intent()
876 }
877 fn take_save_retry(&mut self) -> bool {
878 self.host.take_save_retry()
879 }
880}
881
882impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfacePublication
883 for SurfaceHost<R, S, E>
884{
885 fn resource(&self) -> std::any::TypeId {
886 std::any::TypeId::of::<R>()
887 }
888
889 fn supports_device_shell(&self) -> bool {
890 self.surface.supports_device_shell()
891 }
892 fn device_shell_operation(
893 &mut self,
894 operation: &ui_core::floating_surface::DeviceShellOperation,
895 ) -> Result<serde_json::Value, String> {
896 self.surface.device_shell_operation(operation)
897 }
898 fn request_device_shell(&mut self) -> bool {
899 self.surface.request_device_shell()
900 }
901
902 fn take_save_retry(&mut self) -> bool {
903 self.effects.is_some() && self.surface.take_save_retry()
904 }
905
906 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
907 self.surface.take_host_intent()
908 }
909
910 fn bound_declarations(&self, into: &mut dyn std::any::Any) {
911 if let Some(into) = into.downcast_mut::<Vec<ActionDecl<R>>>() {
912 into.extend(self.surface.bound_declarations());
913 }
914 }
915
916 fn availability(&self) -> Vec<EffectStatus> {
917 self.surface.availability()
918 }
919
920 fn live_rows(&self) -> Vec<LiveRow> {
921 self.surface.live_rows()
922 }
923
924 fn live_row(&self, id: &str) -> Option<LiveRow> {
925 self.surface.live_row(id)
926 }
927
928 fn offers_on_row(&self, row: &str) -> bool {
929 !self.surface.row_declarations(row).is_empty()
930 }
931
932 fn row_declarations(&self, row: &str, into: &mut dyn std::any::Any) -> bool {
933 let offered = self.surface.row_declarations(row);
934 match into.downcast_mut::<Vec<ActionDecl<R>>>() {
935 Some(into) if !offered.is_empty() => {
936 into.extend(offered);
937 true
938 }
939 _ => false,
940 }
941 }
942
943 fn search_entries(&self) -> Vec<SearchEntry> {
944 self.surface.search_entries()
945 }
946
947 fn search_queries(&self) -> SearchQueries {
948 self.search_queries.clone()
949 }
950}
951
952impl ui_core::resources::PublishesResources for HostedSurface {
955 fn resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::Resource> {
956 ui_core::resources::resources(self.host.surface(), doc)
957 .into_iter()
958 .map(outside_document)
959 .collect()
960 }
961
962 fn caller_view_resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::Resource> {
963 ui_core::resources::caller_view_resources(self.host.surface(), doc)
964 .into_iter()
965 .map(outside_document)
966 .collect()
967 }
968
969 fn action_directory_resource_page(
970 &self,
971 doc: &AutoCommit,
972 cursor: &[u8],
973 max_results: usize,
974 ) -> Option<ui_core::widget::ActionDirectoryResourcePage> {
975 let mut page = ui_core::resources::action_directory_resource_page(
976 self.host.surface(),
977 doc,
978 cursor,
979 max_results,
980 )?;
981 page.resources = page.resources.into_iter().map(outside_document).collect();
982 Some(page)
983 }
984
985 fn action_directory_revision(&self, doc: &AutoCommit) -> Option<String> {
986 ui_core::resources::action_directory_revision(self.host.surface(), doc)
987 }
988
989 fn action_directory_invocation_domain(
990 &self,
991 doc: &AutoCommit,
992 ) -> ui_core::widget::ActionDirectoryInvocationDomain {
993 ui_core::resources::action_directory_invocation_domain(self.host.surface(), doc)
994 }
995
996 fn resource(&self, doc: &AutoCommit, id: &str) -> Option<tilo_actions::Resource> {
997 ui_core::resources::resource(self.host.surface(), doc, id).map(resolved_outside_document)
998 }
999
1000 fn resources_for(
1001 &self,
1002 participant: Option<&tilo_actions::ParticipantId>,
1003 doc: &AutoCommit,
1004 ) -> Vec<tilo_actions::Resource> {
1005 ui_core::resources::resources_for(self.host.surface(), participant, doc)
1006 .into_iter()
1007 .map(outside_document)
1008 .collect()
1009 }
1010
1011 fn resource_for(
1012 &self,
1013 participant: Option<&tilo_actions::ParticipantId>,
1014 doc: &AutoCommit,
1015 id: &str,
1016 ) -> Option<tilo_actions::Resource> {
1017 ui_core::resources::resource_for(self.host.surface(), participant, doc, id)
1018 .map(resolved_outside_document)
1019 }
1020
1021 fn action_directory_visibility(
1022 &self,
1023 doc: &AutoCommit,
1024 resource_id: &str,
1025 ) -> Option<tilo_actions::ActionAddress> {
1026 ui_core::resources::action_directory_visibility(self.host.surface(), doc, resource_id)
1027 }
1028
1029 fn document_action_result(
1030 &self,
1031 doc: &AutoCommit,
1032 address: &tilo_actions::ActionAddress,
1033 changes: &[ui_core::automerge::Patch],
1034 ) -> Option<serde_json::Value> {
1035 ui_core::resources::document_action_result(self.host.surface(), doc, address, changes)
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042 use ui_core::automerge::ROOT;
1043 use ui_core::automerge::transaction::Transactable;
1044
1045 #[test]
1046 fn what_a_surface_applied_through_actions_stays() {
1047 let mut doc = AutoCommit::new();
1048 doc.put(ROOT, "title", "applied").unwrap();
1049 doc.commit();
1051 let heads = doc.get_heads();
1052 refuse_raw_writes(&doc, 0, "sync");
1053 assert_eq!(doc.get_heads(), heads);
1054 }
1055
1056 #[test]
1057 #[should_panic(expected = "outside an action")]
1058 fn a_surface_writing_around_its_dispatcher_is_stopped() {
1059 let mut doc = AutoCommit::new();
1060 doc.put(ROOT, "title", "raw").unwrap();
1061 refuse_raw_writes(&doc, 0, "sync");
1062 }
1063}