1use std::cell::RefCell;
5use std::rc::{Rc, Weak};
6use std::sync::{
7 Arc, Weak as SyncWeak,
8 atomic::{AtomicBool, Ordering},
9};
10use ui_core::widget::{AppModel, ModelPath};
11
12use tilo_resource::{Effect, NoEffect, Resource, SearchEntry};
13use ui_core::PresencePayload;
14use ui_core::automerge::transaction::Transactable as _;
15use ui_core::automerge::{AutoCommit, ObjId};
16use ui_core::host_operation::{ResourceOperation, ResourceOperationStart, ResourceOperations};
17use ui_core::presence::PresenceKey;
18use ui_core::widget::{Decorator, Widget};
19
20use super::controls::Actions;
21use super::dispatch::Dispatcher;
22use super::effect::{EffectAnswer, EffectCx, EffectRunner, EffectStatus, Effects, Jobs, OwnedJobs};
23use super::live::Live;
24use super::rows::{LiveRow, outside_document, resolved_outside_document};
25
26pub(crate) struct SurfaceScope {
29 token: RefCell<Option<Rc<()>>>,
30 parent: Weak<()>,
31 children: RefCell<Vec<Weak<Self>>>,
32 jobs: RefCell<Vec<SyncWeak<AtomicBool>>>,
33}
34#[inline(never)]
37fn note_statuses(statuses: &RefCell<Vec<EffectStatus>>, now: Vec<EffectStatus>) {
38 if *statuses.borrow() != now {
39 *statuses.borrow_mut() = now;
40 }
41}
42
43#[inline(never)]
47fn stage_hosted(stages: &super::stage::Stages, scope: Rc<SurfaceScope>, pending: Staging) {
48 stages.push(
49 scope.alive(),
50 Box::new(move |scenario| {
51 if scope.active() {
52 *pending.borrow_mut() = Some(scenario.map(str::to_owned));
53 }
54 }),
55 );
56}
57
58impl SurfaceScope {
59 #[inline(never)]
63 fn under(parent: Weak<()>, within: Option<&Rc<Self>>) -> Rc<Self> {
64 let scope = Self::new(parent);
65 if let Some(within) = within {
66 let mut children = within.children.borrow_mut();
67 children.retain(|child| child.strong_count() > 0);
68 children.push(Rc::downgrade(&scope));
69 }
70 scope
71 }
72
73 pub(crate) fn new(parent: Weak<()>) -> Rc<Self> {
74 Rc::new(Self {
75 token: RefCell::new(Some(Rc::new(()))),
76 parent,
77 children: RefCell::default(),
78 jobs: RefCell::default(),
79 })
80 }
81 fn alive(&self) -> Weak<()> {
82 self.token
83 .borrow()
84 .as_ref()
85 .map_or_else(Weak::new, Rc::downgrade)
86 }
87 pub(crate) fn active(&self) -> bool {
88 if self.parent.strong_count() == 0 {
89 self.retire();
90 }
91 self.token.borrow().is_some()
92 }
93 pub(crate) fn track(&self, cancelled: &Arc<AtomicBool>) {
94 if self.active() {
95 let mut jobs = self.jobs.borrow_mut();
96 jobs.retain(|job| job.strong_count() > 0);
97 jobs.push(Arc::downgrade(cancelled));
98 } else {
99 cancelled.store(true, Ordering::Release);
100 }
101 }
102 pub(crate) fn retire(&self) {
103 self.token.borrow_mut().take();
104 for child in self
105 .children
106 .borrow_mut()
107 .drain(..)
108 .filter_map(|child| child.upgrade())
109 {
110 child.retire();
111 }
112 for job in self
113 .jobs
114 .borrow_mut()
115 .drain(..)
116 .filter_map(|job| job.upgrade())
117 {
118 job.store(true, Ordering::Release);
119 }
120 }
121}
122
123pub trait ResourceSurface<R: Resource, E: Effect = NoEffect>: Widget {
185 fn present(&mut self, state: &Rc<R::State>);
196
197 fn supports_device_shell(&self) -> bool {
198 false
199 }
200
201 fn device_shell_operation(
203 &mut self,
204 _operation: &ui_core::floating_surface::DeviceShellOperation,
205 ) -> Result<serde_json::Value, String> {
206 Err("This surface has no device shell".into())
207 }
208
209 fn request_device_shell(&mut self) -> bool {
211 false
212 }
213
214 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
217 None
218 }
219
220 fn attach(&mut self, cx: SurfaceCx<R>) {
223 let _ = cx;
224 }
225
226 fn landed(&mut self, participant: &tilo_actions::ParticipantId, place: &tilo_resource::Place) {
235 let _ = (participant, place);
236 }
237
238 fn read_only_changed(&mut self, read_only: bool) {
242 let _ = read_only;
243 }
244
245 fn stage(&mut self, scenario: Option<&str>) {
254 let _ = scenario;
255 }
256
257 fn effect(&mut self, effect: E, cx: &mut EffectCx<R, Self>) -> EffectAnswer
268 where
269 Self: Sized,
270 {
271 let _ = cx;
272 Err(format!("`{}` has no handler", effect.id()))
273 }
274
275 fn effects(&self) -> Vec<EffectStatus> {
280 Vec::new()
281 }
282
283 fn live_rows(&self) -> Vec<LiveRow> {
288 Vec::new()
289 }
290
291 fn live_row(&self, id: &str) -> Option<LiveRow> {
295 let _ = id;
296 None
297 }
298}
299
300fn only_through_actions(
304 state: &mut ui_core::widget::Edits<'_>,
305 path: &ModelPath,
306 pass: &str,
307 forward: impl FnOnce(&mut ui_core::widget::Edits<'_>, &ModelPath),
308) {
309 let before = ui_core::doc::document(state, path).0.pending_ops();
310 forward(state, path);
311 refuse_raw_writes(ui_core::doc::document(state, path).0, before, pass);
312}
313
314fn refuse_raw_writes(doc: &AutoCommit, before: usize, pass: &str) {
319 if before != 0 || doc.pending_ops() == 0 {
320 return;
321 }
322 let dropped = doc.pending_ops();
323 tracing::error!(
324 pass,
325 dropped,
326 "a surface wrote the document outside an action; the write was refused"
327 );
328 debug_assert_eq!(
329 dropped, 0,
330 "a surface wrote the document outside an action during `{pass}`"
331 );
332}
333
334pub(crate) trait SurfacePublication {
338 fn resource(&self) -> std::any::TypeId;
341 fn availability(&self) -> Vec<EffectStatus> {
342 Vec::new()
343 }
344 fn live_rows(&self) -> Vec<LiveRow>;
345 fn live_row(&self, id: &str) -> Option<LiveRow>;
346 fn live_row_pending(&self, _id: &str) -> bool {
347 false
348 }
349 fn binds_rows(&self) -> bool {
351 false
352 }
353 fn offers_on_row(&self, _row: &str) -> bool {
355 false
356 }
357 fn row_declarations(&self, _row: &str, _into: &mut dyn std::any::Any) -> bool {
360 false
361 }
362 fn search_entries(&self) -> Vec<SearchEntry> {
363 Vec::new()
364 }
365 fn secrets_unlocked(&self) -> Option<bool> {
366 None
367 }
368 fn kept_in_search_index(&self) -> bool {
369 true
370 }
371 fn supports_device_shell(&self) -> bool {
372 false
373 }
374 fn device_shell_operation(
375 &mut self,
376 _operation: &ui_core::floating_surface::DeviceShellOperation,
377 ) -> Result<serde_json::Value, String> {
378 Err("this view has no device shell".into())
379 }
380 fn request_device_shell(&mut self) -> bool {
381 false
382 }
383 fn search_queries(&self) -> Option<super::publishing::SearchQueries> {
385 None
386 }
387 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent>;
388}
389
390pub struct SurfaceCx<R: Resource> {
392 pub(crate) env: super::env::RecordEnv<R>,
393 pub(crate) jobs: Jobs<R>,
394 pub(crate) actions: Actions<R>,
395 pub(crate) stages: super::stage::Stages,
396 pub(crate) scope: Option<Rc<SurfaceScope>>,
397}
398
399impl<R: Resource> Clone for SurfaceCx<R> {
400 fn clone(&self) -> Self {
401 Self {
402 env: self.env.clone(),
403 jobs: self.jobs.clone(),
404 actions: self.actions.clone(),
405 stages: self.stages.clone(),
406 scope: self.scope.clone(),
407 }
408 }
409}
410
411impl<R: Resource> std::ops::Deref for SurfaceCx<R> {
412 type Target = super::env::RecordEnv<R>;
413 fn deref(&self) -> &super::env::RecordEnv<R> {
414 &self.env
415 }
416}
417
418impl<R: Resource> SurfaceCx<R> {
419 pub fn surface<S, E>(&self, surface: S) -> Result<ui_core::widget::Element, &'static str>
426 where
427 S: ResourceSurface<R, E> + 'static,
428 E: Effect,
429 {
430 if self.alive.strong_count() == 0
431 || self.scope.as_ref().is_some_and(|scope| !scope.active())
432 {
433 return Err("the parent surface has been retired");
434 }
435 Ok(self.host_surface(surface))
436 }
437
438 pub(crate) fn host_surface<S, E>(&self, mut surface: S) -> ui_core::widget::Element
439 where
440 S: ResourceSurface<R, E> + 'static,
441 E: Effect,
442 {
443 let scope = SurfaceScope::under(self.alive.clone(), self.scope.as_ref());
444 let jobs = self.effects.scoped_surface_jobs(&scope);
445 let mut cx = self.clone();
446 cx.jobs = jobs.clone();
447 cx.env.alive = scope.alive();
448 cx.scope = Some(Rc::clone(&scope));
449 surface.attach(cx);
450 surface.present(&self.live.get());
451 let host = SurfaceHost::new(
452 self.live.clone(),
453 self.dispatch.clone(),
454 &self.effects,
455 jobs,
456 Rc::clone(&scope),
457 surface,
458 );
459 stage_hosted(&self.stages, scope, host.staging());
460 HostedSurface::element(Box::new(host))
461 }
462 pub fn jobs(&self) -> Jobs<R> {
466 self.jobs.clone()
467 }
468
469 pub fn actions(&self) -> Actions<R> {
474 self.actions.clone()
475 }
476
477 pub fn live(&self) -> Live<R> {
482 self.live.clone()
483 }
484
485 pub fn app_id(&self) -> &'static str {
488 self.dispatch.app_id()
489 }
490
491 pub fn secrets(&self) -> ui_core::secrets::SecretStore {
494 self.storage.storage().secrets()
495 }
496
497 pub fn files(&self) -> ui_core::storage::AppFiles {
499 self.storage.storage().files()
500 }
501}
502
503#[derive(Clone, Debug, PartialEq)]
506pub enum ResourceSelection {
507 Item(String),
509 Caret { item: String, at: usize },
511 Range {
513 item: String,
514 start: usize,
515 end: usize,
516 },
517 Pointer { item: String, x: f32, y: f32 },
521}
522
523impl ResourceSelection {
524 pub fn presence<R: Resource>(&self, app_id: &str) -> (PresenceKey, PresencePayload) {
527 self.presence_of(R::KIND, app_id)
528 }
529
530 pub fn presence_of(&self, kind: &str, app_id: &str) -> (PresenceKey, PresencePayload) {
532 let key = |item: &str| PresenceKey::Resource {
533 app: app_id.to_owned(),
534 id: item.to_owned(),
535 kind: kind.to_owned(),
536 };
537 match self {
538 Self::Item(item) => (key(item), PresencePayload::Selection { start: 0, end: 0 }),
539 Self::Caret { item, at } => (key(item), PresencePayload::Caret { pos: *at }),
540 Self::Range { item, start, end } => (
541 key(item),
542 PresencePayload::Selection {
543 start: *start,
544 end: *end,
545 },
546 ),
547 Self::Pointer { item, x, y } => (key(item), PresencePayload::Pointer { x: *x, y: *y }),
548 }
549 }
550}
551
552impl From<tilo_resource::Place> for ResourceSelection {
553 fn from(place: tilo_resource::Place) -> Self {
554 match place {
555 tilo_resource::Place::Item(item) => Self::Item(item),
556 tilo_resource::Place::Caret { item, at } => Self::Caret { item, at },
557 tilo_resource::Place::Range { item, start, end } => Self::Range { item, start, end },
558 }
559 }
560}
561
562#[repr(C)]
571pub(crate) struct SurfaceHost<R: Resource, S, E> {
572 surface: S,
573 live: Live<R>,
574 presented: (u64, u64),
577 landings: u64,
579 dispatch: Dispatcher<R>,
580 read_only: bool,
581 staging: Staging,
583 effects: Option<EffectRunner<R, S, E>>,
586 statuses: Rc<RefCell<Vec<EffectStatus>>>,
588 jobs: OwnedJobs<R, S>,
590 scope: Rc<SurfaceScope>,
591}
592
593impl<R: Resource, S, E> Drop for SurfaceHost<R, S, E> {
594 fn drop(&mut self) {
595 self.scope.retire();
596 }
597}
598
599pub(crate) type Staging = Rc<RefCell<Option<Option<String>>>>;
602
603impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceHost<R, S, E> {
604 pub(crate) fn new(
605 live: Live<R>,
606 dispatch: Dispatcher<R>,
607 effects: &Effects<R>,
608 jobs: Jobs<R>,
609 scope: Rc<SurfaceScope>,
610 surface: S,
611 ) -> Self {
612 let statuses = Rc::new(RefCell::new(surface.effects()));
613 let runner = effects
614 .env()
615 .map(|env| EffectRunner::new(env.scoped(&scope)));
616 if !E::declarations().is_empty() {
619 let published = Rc::clone(&statuses);
620 let lifetime = Rc::clone(&scope);
621 let kept = super::effect::KeptSpecs::default();
622 effects.publish::<E>(scope.alive(), move || {
623 if lifetime.active() {
624 super::effect::kept_specs::<E>(&kept, &published.borrow())
625 } else {
626 Vec::new()
627 }
628 });
629 }
630 Self {
631 presented: (live.revision(), live.loads()),
632 landings: live.next_landing(),
633 live,
634 read_only: dispatch.read_only(),
635 dispatch,
636 staging: Staging::default(),
637 effects: runner,
638 statuses,
639 jobs: OwnedJobs::new(jobs),
640 surface,
641 scope,
642 }
643 }
644
645 pub(crate) fn staging(&self) -> Staging {
646 Rc::clone(&self.staging)
647 }
648}
649
650pub(crate) trait SurfaceDriver: SurfacePublication {
653 fn surface(&self) -> &(dyn Widget + 'static);
655 fn surface_mut(&mut self) -> &mut (dyn Widget + 'static);
656 fn surface_any(&self) -> &dyn std::any::Any;
659 fn surface_any_mut(&mut self) -> &mut dyn std::any::Any;
660 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool;
663 fn sync(&mut self, state: &AppModel, path: &ModelPath);
665 fn start_effect(
667 &mut self,
668 op: &ResourceOperation<'_>,
669 doc: &AutoCommit,
670 ) -> Result<Option<ResourceOperationStart>, String>;
671 fn poll_effect(
673 &mut self,
674 invocation: tilo_actions::InvocationId,
675 cancellation_requested: bool,
676 ) -> Option<tilo_actions::InvocationUpdate>;
677}
678
679impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfaceDriver
680 for SurfaceHost<R, S, E>
681{
682 fn surface(&self) -> &(dyn Widget + 'static) {
683 &self.surface
684 }
685
686 fn surface_mut(&mut self) -> &mut (dyn Widget + 'static) {
687 &mut self.surface
688 }
689
690 fn surface_any(&self) -> &dyn std::any::Any {
691 &self.surface
692 }
693
694 fn surface_any_mut(&mut self) -> &mut dyn std::any::Any {
695 &mut self.surface
696 }
697
698 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
699 (self.live.revision(), self.live.loads()) == self.presented
700 && self.staging.borrow().is_none()
701 && self.dispatch.read_only() == self.read_only
702 && self.surface.view_only_scroll(event)
703 }
704
705 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
706 if !self.scope.active() {
707 self.effects = None;
708 return;
709 }
710 let read_only = self.dispatch.read_only();
711 if read_only != self.read_only {
712 self.read_only = read_only;
713 self.surface.read_only_changed(read_only);
714 }
715 let held = (self.live.revision(), self.live.loads());
716 if held != self.presented {
717 self.presented = held;
718 self.surface.present(&self.live.get());
719 }
720 let (landed, next) = self.live.landings_since(self.landings);
721 self.landings = next;
722 for (participant, place) in &landed {
723 self.surface.landed(participant, place);
724 }
725 let staged = self.staging.borrow_mut().take();
726 if let Some(scenario) = staged {
727 self.surface.stage(scenario.as_deref());
728 }
729 self.jobs.poll(&mut self.surface);
730 self.surface.sync(state, path);
731 if self.effects.is_some() {
732 note_statuses(&self.statuses, self.surface.effects());
733 }
734 }
735 fn start_effect(
736 &mut self,
737 op: &ResourceOperation<'_>,
738 doc: &AutoCommit,
739 ) -> Result<Option<ResourceOperationStart>, String> {
740 if !self.scope.active() {
741 return Err("the surface has been retired".into());
742 }
743 match self.effects.as_mut() {
744 Some(runner) => runner.start(op, &mut self.surface, Some(doc), S::effect),
745 None => Ok(None),
746 }
747 }
748
749 fn poll_effect(
750 &mut self,
751 invocation: tilo_actions::InvocationId,
752 cancellation_requested: bool,
753 ) -> Option<tilo_actions::InvocationUpdate> {
754 if !self.scope.active() {
755 self.effects = None;
756 return None;
757 }
758 self.effects
759 .as_mut()?
760 .poll(invocation, cancellation_requested, &mut self.surface)
761 }
762}
763
764pub(crate) struct HostedSurface {
767 host: Box<dyn SurfaceDriver>,
768}
769
770impl HostedSurface {
771 #[inline(never)]
775 pub(crate) fn element(host: Box<dyn SurfaceDriver>) -> ui_core::widget::Element {
776 Box::new(Self { host })
777 }
778}
779
780impl Decorator for HostedSurface {
785 fn provide<'a>(&'a self, request: &mut ui_core::widget::capability::Request<'a, '_>) {
786 if request.wants::<dyn SurfacePublication>() {
787 request.provide::<dyn SurfacePublication>(self);
788 return;
789 }
790 if request.wants::<dyn ui_core::resources::PublishesResources>() {
791 request.provide::<dyn ui_core::resources::PublishesResources>(self);
792 return;
793 }
794 if request.wants::<dyn ResourceOperations>() {
795 request.provide::<dyn ResourceOperations>(self);
796 return;
797 }
798 self.host.surface().provide(request);
799 }
800
801 fn provide_mut<'a>(
802 &'a mut self,
803 request: &mut ui_core::widget::capability::RequestMut<'a, '_>,
804 ) {
805 if request.wants::<dyn SurfacePublication>() {
806 request.provide::<dyn SurfacePublication>(self);
807 return;
808 }
809 if request.wants::<dyn ui_core::resources::PublishesResources>() {
810 request.provide::<dyn ui_core::resources::PublishesResources>(self);
811 return;
812 }
813 if request.wants::<dyn ResourceOperations>() {
814 request.provide::<dyn ResourceOperations>(self);
815 return;
816 }
817 self.host.surface_mut().provide_mut(request);
818 }
819
820 type Inner = dyn Widget;
821
822 fn inner(&self) -> &(dyn Widget + 'static) {
823 self.host.surface()
824 }
825
826 fn inner_mut(&mut self) -> &mut (dyn Widget + 'static) {
827 self.host.surface_mut()
828 }
829
830 fn view_only_scroll(&self, event: &ui_core::winit::event::WindowEvent) -> bool {
831 self.host.view_only_scroll(event)
832 }
833
834 fn take_structure_change(&mut self) -> ui_core::widget::StructureChange {
837 self.host.surface_mut().take_structure_change()
838 }
839
840 fn as_any(&self) -> Option<&dyn std::any::Any> {
843 Some(self.host.surface_any())
844 }
845
846 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
847 Some(self.host.surface_any_mut())
848 }
849
850 fn settle(&mut self, edits: &mut ui_core::widget::Edits<'_>, path: &ObjId) {
851 self.host.surface_mut().settle(edits, path);
852 }
853
854 fn sync(&mut self, state: &AppModel, path: &ModelPath) {
855 self.host.sync(state, path);
856 }
857
858 fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
859 self.host.surface_mut().update(state, path)
860 }
861
862 fn handle_event(
863 &mut self,
864 event: &ui_core::winit::event::WindowEvent,
865 ctx: &mut ui_core::widget::EventCtx<'_>,
866 ) {
867 let before = ctx.state.pending_ops();
868 self.host.surface_mut().handle_event(event, ctx);
869 refuse_raw_writes(ctx.state.doc(), before, "handle_event");
870 }
871
872 fn perform_semantic_action(
873 &mut self,
874 target: &str,
875 action: &ui_core::a11y::Action,
876 state: &mut ui_core::widget::Edits<'_>,
877 path: &ModelPath,
878 ) -> bool {
879 let surface = self.host.surface_mut();
880 let mut performed = false;
881 only_through_actions(state, path, "perform_semantic_action", |state, path| {
882 performed = surface.perform_semantic_action(target, action, state, path);
883 });
884 performed
885 }
886}
887
888impl ResourceOperations for HostedSurface {
889 fn resource_operation_handlers(&self) -> std::collections::HashSet<String> {
890 let surface = self.host.surface();
891 let mut handlers = ui_core::host_operation::resource_operation_handlers(surface);
892 if decorates_routing(surface) {
895 surface.for_each_child(&mut |child| {
896 handlers.extend(ui_core::host_operation::resource_operation_handlers(child));
897 });
898 }
899 handlers
900 }
901
902 fn start_operation(
903 &mut self,
904 op: &ResourceOperation<'_>,
905 doc: &mut ui_core::widget::Edits<'_>,
906 path: &ObjId,
907 ) -> Result<Option<ResourceOperationStart>, String> {
908 if let Some(started) = self.host.start_effect(op, doc.doc())? {
909 return Ok(Some(started));
910 }
911 let before = doc.pending_ops();
920 let surface = self.host.surface_mut();
921 let started = if decorates_routing(surface) {
922 match ui_core::host_operation::start_operation(surface, op, doc, path) {
923 Ok(None) => ui_core::host_operation::start_in_children(surface, op, doc, path),
924 answered => answered,
925 }
926 } else {
927 ui_core::host_operation::start_in_children(surface, op, doc, path)
928 };
929 refuse_raw_writes(doc, before, "start_operation");
930 started
931 }
932
933 fn poll_resource_operation(
934 &mut self,
935 invocation: tilo_actions::InvocationId,
936 cancellation_requested: bool,
937 doc: &AutoCommit,
938 path: &ObjId,
939 ) -> Option<tilo_actions::InvocationUpdate> {
940 if let Some(update) = self.host.poll_effect(invocation, cancellation_requested) {
941 return Some(update);
942 }
943 let surface = self.host.surface_mut();
944 if decorates_routing(surface)
945 && let Some(update) = ui_core::host_operation::poll_resource_operation(
946 surface,
947 invocation,
948 cancellation_requested,
949 doc,
950 path,
951 )
952 {
953 return Some(update);
954 }
955 surface.children_mut().into_iter().find_map(|child| {
956 ui_core::host_operation::poll_resource_operation(
957 child,
958 invocation,
959 cancellation_requested,
960 doc,
961 path,
962 )
963 })
964 }
965}
966
967fn decorates_routing(surface: &(impl Widget + ?Sized)) -> bool {
971 ui_core::widget::capability::capability::<dyn ResourceOperations>(surface).is_some()
972}
973
974impl SurfacePublication for HostedSurface {
975 fn resource(&self) -> std::any::TypeId {
976 self.host.resource()
977 }
978 fn live_rows(&self) -> Vec<LiveRow> {
979 self.host.live_rows()
980 }
981 fn live_row(&self, id: &str) -> Option<LiveRow> {
982 self.host.live_row(id)
983 }
984 fn supports_device_shell(&self) -> bool {
985 self.host.supports_device_shell()
986 }
987 fn device_shell_operation(
988 &mut self,
989 operation: &ui_core::floating_surface::DeviceShellOperation,
990 ) -> Result<serde_json::Value, String> {
991 self.host.device_shell_operation(operation)
992 }
993 fn request_device_shell(&mut self) -> bool {
994 self.host.request_device_shell()
995 }
996 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
997 self.host.take_host_intent()
998 }
999}
1000
1001impl<R: Resource, S: ResourceSurface<R, E> + 'static, E: Effect> SurfacePublication
1002 for SurfaceHost<R, S, E>
1003{
1004 fn resource(&self) -> std::any::TypeId {
1005 std::any::TypeId::of::<R>()
1006 }
1007
1008 fn supports_device_shell(&self) -> bool {
1009 self.surface.supports_device_shell()
1010 }
1011 fn device_shell_operation(
1012 &mut self,
1013 operation: &ui_core::floating_surface::DeviceShellOperation,
1014 ) -> Result<serde_json::Value, String> {
1015 self.surface.device_shell_operation(operation)
1016 }
1017 fn request_device_shell(&mut self) -> bool {
1018 self.surface.request_device_shell()
1019 }
1020
1021 fn take_host_intent(&mut self) -> Option<ui_core::widget::AppHostIntent> {
1022 self.surface.take_host_intent()
1023 }
1024
1025 fn live_rows(&self) -> Vec<LiveRow> {
1026 self.surface.live_rows()
1027 }
1028
1029 fn live_row(&self, id: &str) -> Option<LiveRow> {
1030 self.surface.live_row(id)
1031 }
1032}
1033
1034impl ui_core::resources::PublishesResources for HostedSurface {
1037 fn resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::ResourceSpec> {
1038 ui_core::resources::resources(self.host.surface(), doc)
1039 .into_iter()
1040 .map(outside_document)
1041 .collect()
1042 }
1043
1044 fn caller_view_resources(&self, doc: &AutoCommit) -> Vec<tilo_actions::ResourceSpec> {
1045 ui_core::resources::caller_view_resources(self.host.surface(), doc)
1046 .into_iter()
1047 .map(outside_document)
1048 .collect()
1049 }
1050
1051 fn action_directory_resource_page(
1052 &self,
1053 doc: &AutoCommit,
1054 cursor: &[u8],
1055 max_results: usize,
1056 ) -> Option<ui_core::widget::ActionDirectoryResourcePage> {
1057 let mut page = ui_core::resources::action_directory_resource_page(
1058 self.host.surface(),
1059 doc,
1060 cursor,
1061 max_results,
1062 )?;
1063 page.resources = page.resources.into_iter().map(outside_document).collect();
1064 Some(page)
1065 }
1066
1067 fn action_directory_revision(&self, doc: &AutoCommit) -> Option<String> {
1068 ui_core::resources::action_directory_revision(self.host.surface(), doc)
1069 }
1070
1071 fn action_directory_invocation_domain(
1072 &self,
1073 doc: &AutoCommit,
1074 ) -> ui_core::widget::ActionDirectoryInvocationDomain {
1075 ui_core::resources::action_directory_invocation_domain(self.host.surface(), doc)
1076 }
1077
1078 fn resource(&self, doc: &AutoCommit, id: &str) -> Option<tilo_actions::ResourceSpec> {
1079 ui_core::resources::resource(self.host.surface(), doc, id).map(resolved_outside_document)
1080 }
1081
1082 fn resources_for(
1083 &self,
1084 participant: Option<&tilo_actions::ParticipantId>,
1085 doc: &AutoCommit,
1086 ) -> Vec<tilo_actions::ResourceSpec> {
1087 ui_core::resources::resources_for(self.host.surface(), participant, doc)
1088 .into_iter()
1089 .map(outside_document)
1090 .collect()
1091 }
1092
1093 fn resource_for(
1094 &self,
1095 participant: Option<&tilo_actions::ParticipantId>,
1096 doc: &AutoCommit,
1097 id: &str,
1098 ) -> Option<tilo_actions::ResourceSpec> {
1099 ui_core::resources::resource_for(self.host.surface(), participant, doc, id)
1100 .map(resolved_outside_document)
1101 }
1102
1103 fn action_directory_visibility(
1104 &self,
1105 doc: &AutoCommit,
1106 resource_id: &str,
1107 ) -> Option<tilo_actions::ActionAddress> {
1108 ui_core::resources::action_directory_visibility(self.host.surface(), doc, resource_id)
1109 }
1110
1111 fn document_action_result(
1112 &self,
1113 doc: &AutoCommit,
1114 address: &tilo_actions::ActionAddress,
1115 changes: &[ui_core::automerge::Patch],
1116 ) -> Option<serde_json::Value> {
1117 ui_core::resources::document_action_result(self.host.surface(), doc, address, changes)
1118 }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use super::*;
1124 use ui_core::automerge::ROOT;
1125 use ui_core::automerge::transaction::Transactable;
1126
1127 #[test]
1128 fn what_a_surface_applied_through_actions_stays() {
1129 let mut doc = AutoCommit::new();
1130 doc.put(ROOT, "title", "applied").unwrap();
1131 doc.commit();
1132 let heads = doc.get_heads();
1133 refuse_raw_writes(&doc, 0, "sync");
1134 assert_eq!(doc.get_heads(), heads);
1135 }
1136
1137 #[test]
1138 #[should_panic(expected = "outside an action")]
1139 fn a_surface_writing_around_its_dispatcher_is_stopped() {
1140 let mut doc = AutoCommit::new();
1141 doc.put(ROOT, "title", "raw").unwrap();
1142 refuse_raw_writes(&doc, 0, "sync");
1143 }
1144}