1use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20use serde::{Deserialize, Serialize};
21use tilo_actions::{ActionError, ActionInvoker, EncodedAction, OperationKind};
22use tilo_resource::{Fields, Resource};
23use ui_core::storage::SettingsSchema;
24
25use super::controls::Actions;
26use super::dispatch::Dispatcher;
27use super::effect::Effects;
28use super::form::Forms;
29use super::history::HistoryFeeds;
30use super::live::Live;
31use super::reactive::{Change, Updaters};
32use super::settings::ViewStorage;
33use super::surface::SurfaceCx;
34use super::view::{Bindings, ViewCx};
35use ui_core::widget::Element;
36
37pub(crate) const RECORD_ACTION_TYPE: &str = "tilo.app.record-action";
40
41#[derive(Serialize, Deserialize)]
43pub(crate) struct RecordAction {
44 pub(crate) record: String,
45 pub(crate) action: EncodedAction,
46}
47
48type Apply<P> = Box<dyn Fn(&<P as Resource>::Action, &ActionInvoker) -> Result<(), ActionError>>;
49type Sync<P> = Box<dyn Fn(&Live<P>)>;
50
51pub struct Held<P: Resource> {
54 id: String,
55 label: String,
56 editable: bool,
57 live: Live<P>,
58 sync: Sync<P>,
59 apply: Apply<P>,
60 published: RefCell<Option<(Published, tilo_actions::ResourceSpec)>>,
64}
65
66type Published = (u64, u64, u64, Option<String>);
69
70impl<P: Resource> Held<P> {
71 pub(crate) fn new(
72 id: String,
73 label: String,
74 editable: bool,
75 live: Live<P>,
76 sync: impl Fn(&Live<P>) + 'static,
77 apply: impl Fn(&P::Action, &ActionInvoker) -> Result<(), ActionError> + 'static,
78 ) -> Self {
79 Self {
80 id,
81 label,
82 editable,
83 live,
84 sync: Box::new(sync),
85 apply: Box::new(apply),
86 published: RefCell::new(None),
87 }
88 }
89
90 pub(crate) fn id(&self) -> &str {
91 &self.id
92 }
93
94 pub(crate) fn editable(&self) -> bool {
95 self.editable
96 }
97
98 pub(crate) fn live(&self) -> &Live<P> {
100 &self.live
101 }
102
103 pub(crate) fn sync(&self) {
106 (self.sync)(&self.live);
107 let _ = self.live.take_change();
109 }
110
111 pub(crate) fn apply(
113 &self,
114 action: &P::Action,
115 invoker: &ActionInvoker,
116 ) -> Result<(), ActionError> {
117 if !self.editable {
118 return Err(ActionError::Refused(format!(
119 "{} is changed in another app",
120 self.label
121 )));
122 }
123 (self.apply)(action, invoker)?;
124 self.sync();
125 Ok(())
126 }
127
128 pub(crate) fn publish(&self) -> tilo_actions::ResourceSpec {
132 self.sync();
133 let revision = (
137 self.live.revision(),
138 self.live.loads(),
139 ui_core::i18n::I18nManager::revision(),
140 self.live.unreadable().map(|why| why.reason),
141 );
142 if let Some((at, published)) = self.published.borrow().as_ref()
143 && *at == revision
144 {
145 return published.clone();
146 }
147 let published = if let Some(state) = self.live.get_checked() {
148 tilo_resource::publish::<P>(&self.id, &self.label, &state)
149 } else {
150 let why = self
153 .live
154 .unreadable()
155 .map_or_else(|| "the record does not read".to_owned(), |why| why.reason);
156 tilo_resource::resource::unreadable_parts::<P>(&self.id, &self.label, &why)
157 };
158 let published = route(published, &self.id, self.editable);
159 *self.published.borrow_mut() = Some((revision, published.clone()));
160 published
161 }
162}
163
164pub(crate) fn route(
167 mut resource: tilo_actions::ResourceSpec,
168 record: &str,
169 editable: bool,
170) -> tilo_actions::ResourceSpec {
171 resource.actions = std::mem::take(&mut resource.actions)
172 .into_iter()
173 .filter(|spec| editable && spec.kind == OperationKind::Document)
174 .map(|spec| route_spec(spec, record))
175 .collect();
176 resource.children = std::mem::take(&mut resource.children)
177 .into_iter()
178 .map(|child| route(child, record, editable))
179 .collect();
180 resource
181}
182
183pub(crate) fn route_spec(spec: tilo_actions::ActionSpec, record: &str) -> tilo_actions::ActionSpec {
185 let mut spec = route_operation(spec, RECORD_ACTION_TYPE, record);
186 spec.type_mut().contract.set_own_record();
190 spec
191}
192
193pub(crate) fn route_operation(
196 spec: tilo_actions::ActionSpec,
197 operation: &'static str,
198 record: &str,
199) -> tilo_actions::ActionSpec {
200 let record = record.to_owned();
201 spec.routed(operation, move |action| {
202 let routed = RecordAction {
203 record: record.clone(),
204 action,
205 };
206 EncodedAction::encode_operation(operation, &routed).unwrap_or_else(|error| EncodedAction {
207 type_id: tilo_actions::resource::INVALID_ARGUMENTS_TYPE.to_owned(),
208 payload: error.to_string().into_bytes(),
209 })
210 })
211}
212
213pub(crate) trait RecordDoor {
216 fn id(&self) -> &str;
217 fn editable(&self) -> bool;
219 fn revision(&self) -> u64;
221 fn publish(&self) -> tilo_actions::ResourceSpec;
222 fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String>;
223 fn apply_answering(
225 &self,
226 action: &EncodedAction,
227 invoker: &ActionInvoker,
228 ) -> Result<tilo_actions::ActionOutput, String> {
229 self.apply_encoded(action, invoker)
230 .map(|()| tilo_actions::ActionOutput::default())
231 }
232 fn apply_in(
236 &self,
237 action: &EncodedAction,
238 invoker: &ActionInvoker,
239 live: &mut ui_core::widget::Edits<'_>,
240 ) -> Result<tilo_actions::ActionOutput, String> {
241 let _ = live;
242 self.apply_answering(action, invoker)
243 }
244}
245
246impl<P: Resource> RecordDoor for Held<P> {
247 fn id(&self) -> &str {
248 &self.id
249 }
250
251 fn editable(&self) -> bool {
252 self.editable
253 }
254
255 fn revision(&self) -> u64 {
256 self.sync();
257 self.live.revision()
258 }
259
260 fn publish(&self) -> tilo_actions::ResourceSpec {
261 Held::publish(self)
262 }
263
264 fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String> {
265 if action.type_id != P::ACTION_TYPE {
266 return Err(format!(
267 "{} takes no `{}` action",
268 self.label, action.type_id
269 ));
270 }
271 let action: P::Action =
272 tilo_actions::decode_json(&action.payload).map_err(|error| error.to_string())?;
273 self.apply(&action, invoker)
274 .map_err(|error| error.to_string())
275 }
276}
277
278pub(crate) fn answer(
281 doors: &[Rc<dyn RecordDoor>],
282 me: Option<&str>,
283 payload: &[u8],
284) -> Result<tilo_actions::ActionOutput, String> {
285 let request: RecordAction =
286 tilo_actions::decode_json(payload).map_err(|error| error.to_string())?;
287 let door = doors
288 .iter()
289 .find(|door| door.id() == request.record)
290 .ok_or_else(|| format!("no record `{}` is open here", request.record))?;
291 let me = me.ok_or("this window changes no record of anyone's")?;
292 let invoker =
293 ui_core::invocation_scope::current_invoker().unwrap_or_else(|| ActionInvoker::actor(me));
294 let theirs = invoker.actor == me || invoker.on_behalf_of.as_deref() == Some(me);
295 if !theirs {
296 return Err(format!(
297 "`{}` is {me}'s own; only they change it",
298 request.record
299 ));
300 }
301 door.apply_encoded(&request.action, &invoker)?;
302 Ok(tilo_actions::ActionOutput::default())
303}
304
305pub trait OtherRecord<P: Resource>: sealed::Hold<P> {}
310
311impl<P: Resource, H: sealed::Hold<P>> OtherRecord<P> for H {}
312
313pub(crate) mod sealed {
314 use std::rc::Rc;
315
316 use tilo_resource::Resource;
317
318 pub trait Hold<P: Resource> {
320 fn held(&self, storage: Storage<'_>) -> Option<Rc<super::Held<P>>>;
322 fn door(&self, storage: Storage<'_>) -> Option<Door>;
324 }
325
326 pub struct Door(pub(crate) Rc<dyn super::RecordDoor>);
328
329 pub struct Storage<'a>(pub(crate) &'a super::ViewStorage);
331}
332
333pub struct Records<'a> {
341 storage: Option<&'a ViewStorage>,
344 base: Option<Rc<dyn std::any::Any>>,
347 dispatch: Option<Rc<dyn std::any::Any>>,
350 chains: Vec<tilo_resource::ResourceKind>,
352 sections: Option<Sections>,
355 provider: Option<&'static str>,
358 kept: Vec<(&'static str, Declare)>,
360}
361
362pub type Declare = fn(&mut Records<'_>);
364
365pub(crate) struct Sections {
367 pub(crate) app_id: &'static str,
369 pub(crate) document: ui_core::storage::SettingsDocument,
371 pub(crate) person: Option<Rc<super::person::PersonState>>,
374 pub(crate) doors: Vec<Rc<super::section::SectionDoor>>,
375}
376
377impl<'a> Records<'a> {
378 pub(crate) fn new(storage: &'a ViewStorage) -> Self {
379 Self {
380 storage: Some(storage),
381 base: None,
382 dispatch: None,
383 chains: Vec::new(),
384 sections: None,
385 provider: None,
386 kept: Vec::new(),
387 }
388 }
389
390 pub(crate) const fn declaring() -> Self {
394 Self {
395 storage: None,
396 base: None,
397 dispatch: None,
398 chains: Vec::new(),
399 sections: None,
400 provider: None,
401 kept: Vec::new(),
402 }
403 }
404
405 pub(crate) const fn sections(sections: Sections) -> Self {
408 Self {
409 storage: None,
410 base: None,
411 dispatch: None,
412 chains: Vec::new(),
413 sections: Some(sections),
414 provider: None,
415 kept: Vec::new(),
416 }
417 }
418
419 pub(crate) fn into_sections(self) -> Vec<Rc<super::section::SectionDoor>> {
421 self.sections
422 .map(|sections| sections.doors)
423 .unwrap_or_default()
424 }
425
426 pub(crate) fn into_chains(self) -> Vec<tilo_resource::ResourceKind> {
428 self.chains
429 }
430
431 #[must_use]
435 pub fn into_kept(self) -> Vec<(&'static str, Declare)> {
436 self.kept
437 }
438
439 pub fn kept_by(&mut self, provider: &'static str, declare: Declare) -> &mut Self {
446 self.kept.push((provider, declare));
447 let outer = self.provider.replace(provider);
448 declare(self);
449 self.provider = outer;
450 self
451 }
452
453 pub(crate) fn with_base<R: Resource>(mut self, live: Live<R>, dispatch: Dispatcher<R>) -> Self {
454 self.base = Some(Rc::new(live));
455 self.dispatch = Some(Rc::new(dispatch));
456 self
457 }
458
459 pub fn chain<P: super::Chained>(&mut self) -> &mut Self {
463 let Some(storage) = self.storage else {
464 self.chains.push(tilo_resource::ResourceKind::of::<P>());
465 return self;
466 };
467 let Some(base) = self
468 .base
469 .clone()
470 .and_then(|base| base.downcast::<Live<P::Base>>().ok())
471 else {
472 tracing::warn!(
473 kind = P::KIND,
474 "a chain's parts read the app's own resource as their base"
475 );
476 return self;
477 };
478 let _ = storage.chain::<P>(move || base.get());
479 self
480 }
481
482 pub fn children<C: super::ChildRecord>(&mut self) -> &mut Self {
486 let Some(storage) = self.storage else {
487 return self;
488 };
489 let Some(parent) = self
490 .base
491 .clone()
492 .and_then(|base| base.downcast::<Live<C::Parent>>().ok())
493 else {
494 tracing::warn!(
495 kind = C::KIND,
496 "children read the app's own resource as their parent"
497 );
498 return self;
499 };
500 let children = storage.children::<C>(move || parent.get());
501 if let (Some(children), Some(door)) = (
502 children,
503 self.dispatch
504 .clone()
505 .and_then(|door| door.downcast::<Dispatcher<C::Parent>>().ok()),
506 ) {
507 children.write_parent_through((*door).clone());
508 }
509 self
510 }
511
512 pub fn person<P: Resource>(&mut self) -> &mut Self {
514 let Some(storage) = self.storage else {
515 return self;
516 };
517 let _ = storage.person::<P>();
518 self
519 }
520
521 pub fn shared_workspace<P: Resource>(&mut self) -> &mut Self {
526 let Some(storage) = self.storage else {
527 self.chains.push(tilo_resource::ResourceKind::of::<P>());
530 return self;
531 };
532 storage.workspace_record::<P>();
533 self
534 }
535
536 pub fn settings<T>(&mut self) -> &mut Self
538 where
539 T: Resource<State = T> + Fields + SettingsSchema,
540 {
541 if !T::available_on(ui_core::storage::DeviceClass::current()) {
543 return self;
544 }
545 if let Some(sections) = &mut self.sections {
546 let settings = ui_core::storage::Settings::<T>::for_app(sections.app_id)
547 .in_document(sections.document.clone());
548 let kind = super::section::SectionKind::of(&settings);
549 sections.doors.push(match §ions.person {
550 Some(person) => person.open_section(kind),
551 None => Rc::new(super::section::SectionDoor::new(
552 kind,
553 String::new(),
554 Vec::new(),
555 )),
556 });
557 }
558 let Some(storage) = self.storage else {
559 return self;
560 };
561 let settings = storage.storage().settings::<T>();
562 let door = storage.section(&settings);
563 if let Some(provider) = self.provider {
564 storage.keep_by(door.id(), provider);
565 }
566 self
567 }
568}
569
570impl<R: Resource, T: 'static> ViewCx<R, T> {
571 pub fn record<P: Resource>(
580 &self,
581 record: &impl OtherRecord<P>,
582 part: impl FnOnce(&mut ViewCx<P>) -> Element,
583 ) -> Option<Element> {
584 let held = record.held(sealed::Storage(&self.storage))?;
585 let dispatch = record_dispatch(&self.dispatch, &held);
586 let actions = record_actions(
587 self.dispatch.app_id(),
588 &self.storage,
589 self.alive.clone(),
590 Rc::clone(&held) as Rc<dyn RecordDoor>,
591 );
592 let mut cx = ViewCx::<P> {
593 env: super::env::RecordEnv {
594 live: held.live().clone(),
595 alive: self.alive.clone(),
596 dispatch,
597 saved: self.saved.clone(),
598 saves: self.saves.clone(),
599 run: self.run.clone(),
600 storage: Rc::clone(&self.storage),
601 effects: Effects::inert(),
602 },
603 focus: Rc::new(|state| Some(state)),
604 updaters: Updaters::new(),
605 gate: None,
606 action_bindings: actions,
607 item: None,
608 path: Some(Vec::new()),
609 controls: Rc::clone(&self.controls),
610 forms: Forms::default(),
611 stages: self.stages.clone(),
612 feeds: HistoryFeeds::default(),
613 bindings: Bindings::new(),
614 };
615 let element = part(&mut cx);
616 let updaters = cx.updaters;
617 let followed = Cell::new(held.live().revision());
618 self.storage.follow_with(self.alive.clone(), move || {
619 held.sync();
620 let revision = held.live().revision();
621 if followed.replace(revision) != revision {
622 updaters.run(&held.live().get(), &Change::Everything);
623 }
624 });
625 Some(element)
626 }
627}
628
629fn record_actions<P: Resource>(
633 app: &'static str,
634 storage: &ViewStorage,
635 alive: std::rc::Weak<()>,
636 door: Rc<dyn RecordDoor>,
637) -> Actions<P> {
638 let actions = Actions::new(app, Rc::new(door.publish())).routed_to(
639 door.id(),
640 door.editable(),
641 storage.kept_provider(door.id()),
642 );
643 follow_publication(storage, alive, door, actions.table());
644 actions
645}
646
647fn follow_publication(
649 storage: &ViewStorage,
650 alive: std::rc::Weak<()>,
651 door: Rc<dyn RecordDoor>,
652 table: super::controls::ActionTable,
653) {
654 let followed = Cell::new(door.revision());
655 storage.follow_with(alive, move || {
656 let revision = door.revision();
657 if followed.replace(revision) != revision {
658 table.update(door.publish());
659 }
660 });
661}
662
663impl<R: Resource, T: 'static> ViewCx<R, T> {
664 pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
669 let door = record.door(sealed::Storage(&self.storage))?.0;
670 Some(record_actions(
671 self.dispatch.app_id(),
672 &self.storage,
673 self.alive.clone(),
674 door,
675 ))
676 }
677}
678
679impl<R: Resource> SurfaceCx<R> {
680 pub fn record_actions<P: Resource>(&self, record: &impl OtherRecord<P>) -> Option<Actions<P>> {
687 let door = record.door(sealed::Storage(&self.storage))?.0;
688 Some(record_actions(
689 self.dispatch.app_id(),
690 &self.storage,
691 self.alive.clone(),
692 door,
693 ))
694 }
695}
696
697fn record_dispatch<R: Resource, P: Resource>(
700 parent: &Dispatcher<R>,
701 held: &Rc<Held<P>>,
702) -> Dispatcher<P> {
703 let applying = Rc::downgrade(held);
704 let me = parent.me().map(str::to_owned);
705 parent.for_record(held.id(), !held.editable(), move |action| {
706 let held = applying
707 .upgrade()
708 .ok_or_else(|| ActionError::Refused("the record is closed".into()))?;
709 let me = me
710 .as_deref()
711 .ok_or_else(|| ActionError::Refused("nobody edits a past version".into()))?;
712 held.apply(action, &ActionInvoker::actor(me))
713 })
714}