Skip to main content

tilo_app/framework/
hosted_actions.rs

1//! Actions a host publishes on the door and runs on its own thread.
2//!
3//! A [`ResourceApp`](super::ResourceApp) declares its actions on its resource
4//! and runs them in its views. A host whose actions act on state it keeps
5//! itself, outside any document (the desktop shell's chrome), mounts a
6//! [`HostedActions`] as its app's root instead: the widget publishes the
7//! resources the host hands its [`HostedActionsLink`], admits each invocation
8//! of the link's operation type, and leaves it with the link. The host takes
9//! the invocations from there on its own loop, where its state is not
10//! borrowed, runs them, and answers each through the link; the app reports
11//! the answer to whoever invoked it. So the host's actions reach people,
12//! scripts, agents and other devices through the same door, broker and
13//! policy as any app's.
14
15use std::cell::{Cell, RefCell};
16use std::collections::{HashMap, VecDeque};
17use std::rc::Rc;
18
19use tilo_actions::{ActionInvoker, InvocationId, InvocationUpdate, ResourceSpec};
20use ui_core::automerge::{AutoCommit, ObjId};
21use ui_core::host_operation::{ResourceOperation, ResourceOperationStart, ResourceOperations};
22use ui_core::resources::PublishesResources;
23use ui_core::taffy::{NodeId, Style, TaffyTree};
24use ui_core::theme::Theme;
25use ui_core::vello::kurbo::{Point, Rect};
26use ui_core::widget::{
27    AppKind, AppModel, AppWidget, Edits, IconDescriptor, ModelPath, TaffyTreeExt as _, Widget,
28};
29
30/// One invocation the door admitted, for the host to run.
31#[derive(Debug)]
32pub struct HostedInvocation {
33    pub invocation: InvocationId,
34    /// What the action's spec encoded from the caller's arguments.
35    pub payload: Vec<u8>,
36    /// Who invoked it, as the door admitted them; `None` is the person at
37    /// this device acting for themselves.
38    pub invoker: Option<ActionInvoker>,
39}
40
41#[derive(Default)]
42struct Shared {
43    resources: RefCell<Vec<ResourceSpec>>,
44    /// Bumped whenever `resources` change, so the app publishes them anew.
45    generation: Cell<u64>,
46    asked: RefCell<VecDeque<HostedInvocation>>,
47    answers: RefCell<HashMap<InvocationId, InvocationUpdate>>,
48    asked_wake: RefCell<Option<Rc<dyn Fn()>>>,
49}
50
51/// The host's side of a [`HostedActions`]: what it publishes, the
52/// invocations waiting for it, and its answers.
53#[derive(Clone)]
54pub struct HostedActionsLink {
55    operation: &'static str,
56    shared: Rc<Shared>,
57}
58
59impl HostedActionsLink {
60    /// A link whose actions all encode as operation type `operation`.
61    #[must_use]
62    pub fn new(operation: &'static str) -> Self {
63        Self {
64            operation,
65            shared: Rc::default(),
66        }
67    }
68
69    /// Publish `resources` in place of what was published.
70    pub fn publish(&self, resources: Vec<ResourceSpec>) {
71        *self.shared.resources.borrow_mut() = resources;
72        self.shared
73            .generation
74            .set(self.shared.generation.get().wrapping_add(1));
75    }
76
77    /// Call `wake` whenever an invocation arrives, so the host's loop runs.
78    pub fn on_asked(&self, wake: impl Fn() + 'static) {
79        *self.shared.asked_wake.borrow_mut() = Some(Rc::new(wake));
80    }
81
82    /// The invocations admitted since the last call, oldest first.
83    #[must_use]
84    pub fn take_asked(&self) -> Vec<HostedInvocation> {
85        self.shared.asked.borrow_mut().drain(..).collect()
86    }
87
88    /// Answer `invocation`: the update its caller observes next. A pending
89    /// answer may be followed by a final one.
90    pub fn answer(&self, invocation: InvocationId, update: InvocationUpdate) {
91        self.shared.answers.borrow_mut().insert(invocation, update);
92    }
93}
94
95/// The root of a host's app: publishes the link's resources and admits the
96/// link's operations ([module docs](self)). It takes no workspace.
97pub struct HostedActions {
98    link: HostedActionsLink,
99    kind: AppKind,
100    title: String,
101    icon: IconDescriptor,
102    handled: &'static [&'static str],
103    published: u64,
104    rect: Rect,
105}
106
107impl HostedActions {
108    /// The root of the app `title`, shown by `icon` as `kind`, answering
109    /// through `link`.
110    #[must_use]
111    pub fn new(
112        link: HostedActionsLink,
113        kind: AppKind,
114        title: String,
115        icon: IconDescriptor,
116    ) -> Self {
117        Self {
118            kind,
119            title,
120            icon,
121            // One small allocation per app, which lives as long as it.
122            handled: Box::leak(Box::new([link.operation])),
123            published: link.shared.generation.get(),
124            link,
125            rect: Rect::ZERO,
126        }
127    }
128}
129
130impl Widget for HostedActions {
131    ui_core::provides!(dyn ResourceOperations, dyn PublishesResources);
132
133    fn build_layout(
134        &mut self,
135        taffy: &mut TaffyTree<()>,
136        _theme: &Theme,
137        node_map: &mut HashMap<NodeId, *mut dyn Widget>,
138    ) -> NodeId {
139        taffy.widget_leaf(
140            Style {
141                display: ui_core::taffy::Display::None,
142                ..Style::default()
143            },
144            node_map,
145            self,
146        )
147    }
148    fn apply_layout(&mut self, _taffy: &TaffyTree<()>, _node: NodeId, origin: Point) {
149        self.rect = Rect::from_origin_size(origin, (0.0, 0.0));
150    }
151    fn update(&mut self, _state: &AppModel, _path: &ModelPath) -> bool {
152        // What is published changed: the app declares it anew.
153        let generation = self.link.shared.generation.get();
154        let changed = generation != self.published;
155        self.published = generation;
156        changed
157    }
158    fn needs_redraw(&self) -> bool {
159        false
160    }
161    fn is_layout_dirty(&self) -> bool {
162        false
163    }
164    fn name(&self) -> &'static str {
165        "HostedActions"
166    }
167    fn get_rect(&self) -> Rect {
168        self.rect
169    }
170}
171
172impl AppWidget for HostedActions {
173    fn app_kind(&self) -> AppKind {
174        self.kind
175    }
176    fn app_title(&self) -> &str {
177        &self.title
178    }
179    fn app_icon(&self) -> IconDescriptor {
180        self.icon.clone()
181    }
182}
183
184impl PublishesResources for HostedActions {
185    fn resources(&self, _doc: &AutoCommit) -> Vec<ResourceSpec> {
186        self.link.shared.resources.borrow().clone()
187    }
188}
189
190impl ResourceOperations for HostedActions {
191    fn local_resource_operation_handlers(&self) -> &'static [&'static str] {
192        self.handled
193    }
194
195    fn start_operation(
196        &mut self,
197        op: &ResourceOperation<'_>,
198        _doc: &mut Edits<'_>,
199        _path: &ObjId,
200    ) -> Result<Option<ResourceOperationStart>, String> {
201        let (request, _custody) = op.with_custody();
202        if request.type_id != self.link.operation {
203            return Ok(None);
204        }
205        op.check()?;
206        self.link
207            .shared
208            .asked
209            .borrow_mut()
210            .push_back(HostedInvocation {
211                invocation: request.invocation,
212                payload: request.payload.to_vec(),
213                invoker: ui_core::invocation_scope::current_invoker(),
214            });
215        let wake = self.link.shared.asked_wake.borrow().clone();
216        if let Some(wake) = wake {
217            wake();
218        }
219        Ok(Some(ResourceOperationStart::Accepted))
220    }
221
222    fn poll_resource_operation(
223        &mut self,
224        invocation: InvocationId,
225        _cancellation_requested: bool,
226        _doc: &AutoCommit,
227        _path: &ObjId,
228    ) -> Option<InvocationUpdate> {
229        let mut answers = self.link.shared.answers.borrow_mut();
230        let update = answers.get(&invocation)?.clone();
231        if update.is_terminal() {
232            answers.remove(&invocation);
233        }
234        Some(update)
235    }
236}