1use 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#[derive(Debug)]
32pub struct HostedInvocation {
33 pub invocation: InvocationId,
34 pub payload: Vec<u8>,
36 pub invoker: Option<ActionInvoker>,
39}
40
41#[derive(Default)]
42struct Shared {
43 resources: RefCell<Vec<ResourceSpec>>,
44 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#[derive(Clone)]
54pub struct HostedActionsLink {
55 operation: &'static str,
56 shared: Rc<Shared>,
57}
58
59impl HostedActionsLink {
60 #[must_use]
62 pub fn new(operation: &'static str) -> Self {
63 Self {
64 operation,
65 shared: Rc::default(),
66 }
67 }
68
69 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 pub fn on_asked(&self, wake: impl Fn() + 'static) {
79 *self.shared.asked_wake.borrow_mut() = Some(Rc::new(wake));
80 }
81
82 #[must_use]
84 pub fn take_asked(&self) -> Vec<HostedInvocation> {
85 self.shared.asked.borrow_mut().drain(..).collect()
86 }
87
88 pub fn answer(&self, invocation: InvocationId, update: InvocationUpdate) {
91 self.shared.answers.borrow_mut().insert(invocation, update);
92 }
93}
94
95pub 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 #[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 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 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}