Skip to main content

tilo_app/framework/
endpoint.rs

1//! An effect vocabulary answered by a widget of the window, for a host that runs documents of
2//! its own.
3//!
4//! A [`ResourceApp`](super::ResourceApp) answers its effects through its views and surface. A
5//! host whose window is its own widget (tilolang runs other apps' packages) has no such view; it
6//! mounts an [`EffectEndpoint`] anywhere in its tree and publishes the vocabulary's specs on a
7//! resource itself. The endpoint answers each invocation at once, as the typed value it
8//! decoded, with who invoked it; it starts no job and changes no document by itself, so what an invocation changes in
9//! the workspace it changes through the host's own commits.
10
11use std::collections::HashMap;
12
13use tilo_actions::ActionOutput;
14use tilo_resource::Effect;
15use ui_core::host_operation::{ResourceOperation, ResourceOperationStart, ResourceOperations};
16use ui_core::taffy::{NodeId, Style, TaffyTree};
17use ui_core::theme::Theme;
18use ui_core::vello::kurbo::{Point, Rect};
19use ui_core::widget::{Edits, TaffyTreeExt as _, Widget};
20
21/// Who invoked an effect: the invocation, and the invoker the door admitted for it. `None` is the
22/// person at this window acting for themselves; an agent's task is its delegate with the person
23/// it acts for.
24#[derive(Clone, Debug)]
25pub struct EffectCaller {
26    pub invocation: tilo_actions::InvocationId,
27    pub invoker: Option<tilo_actions::ActionInvoker>,
28}
29
30type Handler<E> = Box<dyn FnMut(E, &EffectCaller) -> Result<ActionOutput, String>>;
31
32/// A widget that takes no workspace and answers the vocabulary `E`.
33pub struct EffectEndpoint<E: Effect> {
34    handle: Handler<E>,
35    /// The operation type of `E`, in the form the host's handler registry reads.
36    handled: &'static [&'static str],
37    rect: Rect,
38}
39
40impl<E: Effect> EffectEndpoint<E> {
41    /// An endpoint that answers each effect with `handle`.
42    pub fn new(
43        handle: impl FnMut(E, &EffectCaller) -> Result<ActionOutput, String> + 'static,
44    ) -> Self {
45        Self {
46            handle: Box::new(handle),
47            // One small allocation per endpoint, which lives as long as its window.
48            handled: Box::leak(Box::new([E::OPERATION])),
49            rect: Rect::ZERO,
50        }
51    }
52}
53
54impl<E: Effect> Widget for EffectEndpoint<E> {
55    ui_core::provides!(dyn ResourceOperations);
56
57    fn build_layout(
58        &mut self,
59        taffy: &mut TaffyTree<()>,
60        _theme: &Theme,
61        node_map: &mut HashMap<NodeId, *mut dyn Widget>,
62    ) -> NodeId {
63        taffy.widget_leaf(
64            Style {
65                display: ui_core::taffy::Display::None,
66                ..Style::default()
67            },
68            node_map,
69            self,
70        )
71    }
72
73    fn apply_layout(&mut self, _taffy: &TaffyTree<()>, _node: NodeId, origin: Point) {
74        self.rect = Rect::from_origin_size(origin, (0.0, 0.0));
75    }
76
77    fn needs_redraw(&self) -> bool {
78        false
79    }
80
81    fn is_layout_dirty(&self) -> bool {
82        false
83    }
84
85    fn name(&self) -> &'static str {
86        "EffectEndpoint"
87    }
88
89    fn get_rect(&self) -> Rect {
90        self.rect
91    }
92}
93
94impl<E: Effect> ResourceOperations for EffectEndpoint<E> {
95    fn local_resource_operation_handlers(&self) -> &'static [&'static str] {
96        self.handled
97    }
98
99    fn start_operation(
100        &mut self,
101        op: &ResourceOperation<'_>,
102        _doc: &mut Edits<'_>,
103        _path: &ui_core::automerge::ObjId,
104    ) -> Result<Option<ResourceOperationStart>, String> {
105        let (request, _custody) = op.with_custody();
106        if request.type_id != E::OPERATION {
107            return Ok(None);
108        }
109        op.check()?;
110        let effect = E::from_payload(request.payload).map_err(|error| error.to_string())?;
111        let caller = EffectCaller {
112            invocation: request.invocation,
113            invoker: ui_core::invocation_scope::current_invoker(),
114        };
115        (self.handle)(effect, &caller).map(|output| Some(ResourceOperationStart::Completed(output)))
116    }
117}