1use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20use serde::{Deserialize, Serialize};
21use tilo_actions::{
22 ActionError, ActionInvoker, ActionKind, ActionNeed, EncodedAction, OperationKind,
23};
24use tilo_resource::{Fields, Resource};
25use ui_core::storage::SettingsSchema;
26
27use super::controls::Actions;
28use super::dispatch::Dispatcher;
29use super::effect::Effects;
30use super::form::Forms;
31use super::history::HistoryFeeds;
32use super::live::Live;
33use super::reactive::{Change, Updaters};
34use super::settings::ViewStorage;
35use super::surface::SurfaceCx;
36use super::view::{Bindings, ViewCx};
37use ui_core::widget::Element;
38
39pub(crate) const RECORD_ACTION_TYPE: &str = "tilo.app.record-action";
42
43#[derive(Serialize, Deserialize)]
45struct RecordAction {
46 record: String,
47 action: EncodedAction,
48}
49
50type Apply<P> = Box<dyn Fn(&<P as Resource>::Action, &ActionInvoker) -> Result<(), ActionError>>;
51type Sync<P> = Box<dyn Fn(&Live<P>)>;
52
53pub struct Held<P: Resource> {
56 id: String,
57 label: String,
58 editable: bool,
59 live: Live<P>,
60 sync: Sync<P>,
61 apply: Apply<P>,
62 published: RefCell<Option<((u64, u64), tilo_actions::Resource)>>,
65}
66
67impl<P: Resource> Held<P> {
68 pub(crate) fn new(
69 id: String,
70 label: String,
71 editable: bool,
72 live: Live<P>,
73 sync: impl Fn(&Live<P>) + 'static,
74 apply: impl Fn(&P::Action, &ActionInvoker) -> Result<(), ActionError> + 'static,
75 ) -> Self {
76 Self {
77 id,
78 label,
79 editable,
80 live,
81 sync: Box::new(sync),
82 apply: Box::new(apply),
83 published: RefCell::new(None),
84 }
85 }
86
87 pub(crate) fn id(&self) -> &str {
89 &self.id
90 }
91
92 pub(crate) fn editable(&self) -> bool {
93 self.editable
94 }
95
96 pub(crate) fn live(&self) -> &Live<P> {
98 &self.live
99 }
100
101 pub(crate) fn sync(&self) {
104 (self.sync)(&self.live);
105 let _ = self.live.take_change();
107 }
108
109 pub(crate) fn apply(
114 &self,
115 action: &P::Action,
116 invoker: &ActionInvoker,
117 ) -> Result<(), ActionError> {
118 if !self.editable {
119 return Err(ActionError::Refused(format!(
120 "{} is changed in another app",
121 self.label
122 )));
123 }
124 (self.apply)(action, invoker)?;
125 self.sync();
126 Ok(())
127 }
128
129 pub(crate) fn publish(&self) -> tilo_actions::Resource {
133 self.sync();
134 let revision = (self.live.revision(), ui_core::i18n::I18nManager::revision());
135 if let Some((at, published)) = self.published.borrow().as_ref()
136 && *at == revision
137 {
138 return published.clone();
139 }
140 let state = self.live.get();
141 let published = route(
142 tilo_resource::publish::<P>(&self.id, &self.label, &state),
143 &self.id,
144 self.editable,
145 );
146 *self.published.borrow_mut() = Some((revision, published.clone()));
147 published
148 }
149}
150
151pub(crate) fn route(
154 mut resource: tilo_actions::Resource,
155 record: &str,
156 editable: bool,
157) -> tilo_actions::Resource {
158 resource.actions = std::mem::take(&mut resource.actions)
159 .into_iter()
160 .filter(|spec| editable && spec.kind == OperationKind::Document)
161 .map(|spec| route_spec(spec, record))
162 .collect();
163 resource.children = std::mem::take(&mut resource.children)
164 .into_iter()
165 .map(|child| route(child, record, editable))
166 .collect();
167 resource
168}
169
170pub(crate) fn route_spec(spec: tilo_actions::ActionSpec, record: &str) -> tilo_actions::ActionSpec {
172 let record = record.to_owned();
173 spec.routed(RECORD_ACTION_TYPE, move |action| {
174 let routed = RecordAction {
175 record: record.clone(),
176 action,
177 };
178 EncodedAction::encode_operation(RECORD_ACTION_TYPE, &routed).unwrap_or_else(|error| {
179 EncodedAction {
180 type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
181 payload: error.to_string().into_bytes(),
182 }
183 })
184 })
185 .of_kind(ActionKind::Read)
188 .with_need_of(|_| ActionNeed::of(ActionKind::Read))
189}
190
191pub(crate) trait RecordDoor {
194 fn id(&self) -> &str;
195 fn editable(&self) -> bool;
197 fn revision(&self) -> u64;
199 fn publish(&self) -> tilo_actions::Resource;
200 fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String>;
201}
202
203impl<P: Resource> RecordDoor for Held<P> {
204 fn id(&self) -> &str {
205 &self.id
206 }
207
208 fn editable(&self) -> bool {
209 self.editable
210 }
211
212 fn revision(&self) -> u64 {
213 self.sync();
214 self.live.revision()
215 }
216
217 fn publish(&self) -> tilo_actions::Resource {
218 Held::publish(self)
219 }
220
221 fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String> {
222 if action.type_id != P::ACTION_TYPE {
223 return Err(format!(
224 "{} takes no `{}` action",
225 self.label, action.type_id
226 ));
227 }
228 let action: P::Action =
229 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
230 self.apply(&action, invoker)
231 .map_err(|error| error.to_string())
232 }
233}
234
235pub(crate) fn answer(
242 doors: &[Rc<dyn RecordDoor>],
243 me: Option<&str>,
244 payload: &[u8],
245) -> Result<tilo_actions::ActionOutput, String> {
246 let request: RecordAction =
247 tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
248 let door = doors
249 .iter()
250 .find(|door| door.id() == request.record)
251 .ok_or_else(|| format!("no record `{}` is open here", request.record))?;
252 let me = me.ok_or("this window changes no record of anyone's")?;
253 let invoker =
254 ui_core::invocation_scope::current_invoker().unwrap_or_else(|| ActionInvoker::actor(me));
255 let theirs = invoker.actor == me || invoker.on_behalf_of.as_deref() == Some(me);
256 if !theirs {
257 return Err(format!(
258 "`{}` is {me}'s own; only they change it",
259 request.record
260 ));
261 }
262 door.apply_encoded(&request.action, &invoker)?;
263 Ok(tilo_actions::ActionOutput::default())
264}
265
266pub trait OtherRecord<P: Resource>: sealed::Hold<P> {}
271
272impl<P: Resource, H: sealed::Hold<P>> OtherRecord<P> for H {}
273
274pub(crate) mod sealed {
275 use std::rc::Rc;
276
277 use tilo_resource::Resource;
278
279 pub trait Hold<P: Resource> {
281 fn held(&self, storage: Storage<'_>) -> Option<Rc<super::Held<P>>>;
283 fn door(&self, storage: Storage<'_>) -> Option<Door>;
285 }
286
287 pub struct Door(pub(crate) Rc<dyn super::RecordDoor>);
289
290 pub struct Storage<'a>(pub(crate) &'a super::ViewStorage);
292}
293
294pub struct Records<'a> {
300 storage: &'a ViewStorage,
301}
302
303impl<'a> Records<'a> {
304 pub(crate) fn new(storage: &'a ViewStorage) -> Self {
305 Self { storage }
306 }
307
308 pub fn person<P: Resource>(&mut self) -> &mut Self {
310 let _ = self.storage.person::<P>();
311 self
312 }
313
314 pub fn shared_person<P: tilo_resource::SharedRecord>(&mut self) -> &mut Self {
317 let _ = self.storage.shared_person::<P>();
318 self
319 }
320
321 pub fn settings<T>(&mut self) -> &mut Self
323 where
324 T: Resource<State = T> + Fields + SettingsSchema,
325 {
326 let settings = self.storage.storage().settings::<T>();
327 let _ = self.storage.section(&settings);
328 self
329 }
330}
331
332impl<R: Resource, T: 'static> ViewCx<R, T> {
333 pub fn record<P: Resource>(
342 &self,
343 record: &impl OtherRecord<P>,
344 part: impl FnOnce(&mut ViewCx<P>) -> Element,
345 ) -> Option<Element> {
346 let held = record.held(sealed::Storage(&self.storage))?;
347 let dispatch = record_dispatch(&self.dispatch, &held);
348 let actions = record_actions(
349 self.dispatch.app_id(),
350 &self.storage,
351 self.alive.clone(),
352 Rc::clone(&held) as Rc<dyn RecordDoor>,
353 );
354 let mut cx = ViewCx::<P> {
355 env: super::env::RecordEnv {
356 live: held.live().clone(),
357 alive: self.alive.clone(),
358 dispatch,
359 saved: self.saved.clone(),
360 run: self.run.clone(),
361 storage: Rc::clone(&self.storage),
362 effects: Effects::inert(),
363 },
364 focus: Rc::new(|state| Some(state)),
365 updaters: Updaters::new(),
366 gate: None,
367 action_bindings: actions,
368 item: None,
369 path: Some(Vec::new()),
370 controls: Rc::clone(&self.controls),
371 forms: Forms::default(),
372 stages: self.stages.clone(),
373 feeds: HistoryFeeds::default(),
374 bindings: Bindings::new(),
375 };
376 let element = part(&mut cx);
377 let updaters = cx.updaters;
378 let followed = Cell::new(held.live().revision());
379 self.storage.follow_with(self.alive.clone(), move || {
380 held.sync();
381 let revision = held.live().revision();
382 if followed.replace(revision) != revision {
383 updaters.run(&held.live().get(), &Change::Everything);
384 }
385 });
386 Some(element)
387 }
388}
389
390fn record_actions<P: Resource>(
394 app: &'static str,
395 storage: &ViewStorage,
396 alive: std::rc::Weak<()>,
397 door: Rc<dyn RecordDoor>,
398) -> Actions<P> {
399 let actions = Actions::new(app, Rc::new(door.publish())).routed_to(door.id(), door.editable());
400 follow_publication(storage, alive, door, actions.table());
401 actions
402}
403
404fn follow_publication(
406 storage: &ViewStorage,
407 alive: std::rc::Weak<()>,
408 door: Rc<dyn RecordDoor>,
409 table: super::controls::ActionTable,
410) {
411 let followed = Cell::new(door.revision());
412 storage.follow_with(alive, move || {
413 let revision = door.revision();
414 if followed.replace(revision) != revision {
415 table.update(door.publish());
416 }
417 });
418}
419
420impl<R: Resource> SurfaceCx<R> {
421 pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
428 let door = record.door(sealed::Storage(&self.storage))?.0;
429 Some(record_actions(
430 self.dispatch.app_id(),
431 &self.storage,
432 self.alive.clone(),
433 door,
434 ))
435 }
436}
437
438fn record_dispatch<R: Resource, P: Resource>(
441 parent: &Dispatcher<R>,
442 held: &Rc<Held<P>>,
443) -> Dispatcher<P> {
444 let applying = Rc::downgrade(held);
445 let me = parent.me().map(str::to_owned);
446 parent.for_record(held.id(), !held.editable(), move |action| {
447 let held = applying
448 .upgrade()
449 .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
450 let me = me
451 .as_deref()
452 .ok_or_else(|| ActionError::Refused("nobody edits a past version".into()))?;
453 held.apply(action, &ActionInvoker::actor(me))
454 })
455}