1use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::{Rc, Weak};
7
8use tilo_resource::{
9 ActionDecl, Binds, Collection, Edit, Effect, Id, Model, Params, Path, Resource, Segment,
10 Settable,
11};
12use ui_core::LocalState;
13use ui_core::arriving::Arriving;
14use ui_core::i18n::LabelKey;
15use ui_core::record::RecordSaveStatus;
16use ui_core::widget::{ArrivingView, SkeletonShape};
17use ui_core::widget::{Button, Checkbox, Element, Text, TextInput};
18use ui_core::workspace_sync::WorkspaceUnreachable;
19
20use super::controls::{ActionControl, Actions};
21use super::dispatch::{Dispatcher, Submit};
22use super::each::Each;
23use super::effect::{EffectAnswer, EffectCx, EffectStatus, Effects};
24use super::env::RecordEnv;
25use super::form::{Attached, Form, FormField, Forms, RecordField};
26use super::live::Live;
27use super::reactive::{Reads, Updaters};
28use super::scenario::Run;
29use super::settings::ViewStorage;
30use super::stage::Stages;
31use super::surface::{ResourceSurface, SurfaceCx};
32use super::switch::Switch;
33
34type ScopedPart = dyn Fn(&Rc<()>) -> Element;
37
38fn arrived_part(build: Box<ScopedPart>) -> Element {
41 Switch::keyed(
42 LocalState::new(()),
43 Box::new(move |(), alive: &Rc<()>| build(alive)),
44 )
45}
46
47fn workspace_arrival(
50 holdings: &Arriving<(), WorkspaceUnreachable>,
51 part: Rc<ScopedPart>,
52) -> Element {
53 let view = ArrivingView::new(holdings, move |()| {
54 let part = Rc::clone(&part);
55 arrived_part(Box::new(move |alive: &Rc<()>| part(alive)))
56 });
57 Box::new(view.shaped(SkeletonShape::Page))
58}
59
60fn disabled(mut button: Button) -> Button {
61 button.set_enabled(false);
62 button
63}
64
65type Bind<R> = Box<dyn Fn(&<R as Resource>::State) -> Vec<ActionDecl<R>>>;
66type BindList<R> = Vec<(Weak<()>, Bind<R>)>;
67
68pub(crate) struct Bindings<R: Resource>(Rc<RefCell<BindList<R>>>);
72
73impl<R: Resource> Clone for Bindings<R> {
74 fn clone(&self) -> Self {
75 Self(Rc::clone(&self.0))
76 }
77}
78
79impl<R: Resource> Bindings<R> {
80 pub(crate) fn new() -> Self {
81 Self(Rc::default())
82 }
83
84 pub(crate) fn declarations(&self, state: &R::State) -> Vec<ActionDecl<R>> {
86 let mut bindings = self.0.borrow_mut();
87 bindings.retain(|(alive, _)| alive.strong_count() > 0);
88 bindings.iter().flat_map(|(_, bind)| bind(state)).collect()
89 }
90}
91
92pub struct Mounts<R: Resource, T: 'static = <R as Resource>::State> {
97 parent: Weak<()>,
99 scope: Rc<PartScope<R, T>>,
100}
101
102type PartScope<R, T> = dyn Fn(&Rc<()>) -> ViewCx<R, T>;
104
105impl<R: Resource, T: 'static> Clone for Mounts<R, T> {
106 fn clone(&self) -> Self {
107 Self {
108 parent: self.parent.clone(),
109 scope: Rc::clone(&self.scope),
110 }
111 }
112}
113
114impl<R: Resource, T: 'static> Mounts<R, T> {
115 pub fn mount(
121 &self,
122 build: impl FnOnce(&mut ViewCx<R, T>) -> Element,
123 ) -> Result<Element, &'static str> {
124 if self.parent.strong_count() == 0 {
125 return Err("the view that mounts it has been retired");
126 }
127 let alive = Rc::new(());
128 let part = build(&mut (self.scope)(&alive));
129 Ok(Switch::mounted(alive, part))
130 }
131}
132
133pub(crate) type Focus<R, T> = Rc<dyn for<'a> Fn(&'a <R as Resource>::State) -> Option<&'a T>>;
136
137pub struct ViewCx<R: Resource, T: 'static = <R as Resource>::State> {
143 pub(crate) env: RecordEnv<R>,
144 pub(crate) focus: Focus<R, T>,
145 pub(crate) updaters: Updaters<R>,
146 pub(crate) gate: Option<super::reactive::Gate>,
149 pub(crate) action_bindings: Actions<R>,
150 pub(crate) item: Option<Id>,
151 pub(crate) path: Option<Vec<Segment>>,
155 pub(crate) controls: Rc<RefCell<HashMap<String, u32>>>,
156 pub(crate) forms: Forms,
158 pub(crate) stages: Stages,
159 pub(crate) feeds: super::history::HistoryFeeds,
160 pub(crate) bindings: Bindings<R>,
161}
162
163impl<R: Resource, T: 'static> std::ops::Deref for ViewCx<R, T> {
164 type Target = RecordEnv<R>;
165 fn deref(&self) -> &RecordEnv<R> {
166 &self.env
167 }
168}
169
170impl<R: Resource> ViewCx<R> {
171 pub(crate) fn root(
174 live: Live<R>,
175 alive: &Rc<()>,
176 dispatch: Dispatcher<R>,
177 published: tilo_actions::ResourceSpec,
178 run: Run,
179 storage: Rc<ViewStorage>,
180 ) -> Self {
181 let published = with_history_verbs(published);
182 let app = dispatch.app_id();
183 let fresh = FreshScope::new();
184 Self {
185 env: RecordEnv {
186 live,
187 alive: Rc::downgrade(alive),
188 dispatch,
189 saved: fresh.saved,
190 saves: fresh.saves,
191 run,
192 storage,
193 effects: Effects::inert(),
194 },
195 focus: Rc::new(|state| Some(state)),
196 updaters: Updaters::new(),
197 gate: None,
198 action_bindings: Actions::new(app, published),
199 item: None,
200 path: Some(Vec::new()),
201 controls: fresh.controls,
202 forms: fresh.forms,
203 stages: fresh.stages,
204 feeds: fresh.feeds,
205 bindings: Bindings::new(),
206 }
207 }
208}
209
210#[inline(never)]
214fn current_specs(
215 source: &StatusSource,
216 kept: &super::effect::KeptSpecs,
217 specs: fn() -> Vec<tilo_actions::ActionSpec>,
218) -> Vec<tilo_actions::ActionSpec> {
219 let now = source
220 .borrow()
221 .as_ref()
222 .map(|status| status())
223 .unwrap_or_default();
224 super::effect::kept_specs_of(kept, &now, specs)
225}
226
227struct FreshScope {
231 saved: LocalState<RecordSaveStatus>,
232 saves: super::Saves,
233 controls: Rc<RefCell<HashMap<String, u32>>>,
234 forms: Forms,
235 stages: Stages,
236 feeds: super::history::HistoryFeeds,
237}
238
239impl FreshScope {
240 #[inline(never)]
241 fn new() -> Self {
242 Self {
243 saved: LocalState::new(RecordSaveStatus::Saved),
244 saves: super::Saves::default(),
245 controls: Rc::default(),
246 forms: Forms::default(),
247 stages: Stages::new(),
248 feeds: super::history::HistoryFeeds::default(),
249 }
250 }
251}
252
253#[inline(never)]
257fn with_history_verbs(published: tilo_actions::ResourceSpec) -> Rc<tilo_actions::ResourceSpec> {
258 let mut published = published;
259 for spec in super::history::specs() {
260 if !published.actions.iter().any(|action| action.id == spec.id) {
261 published = published.with_action(spec);
262 }
263 }
264 Rc::new(published)
265}
266
267impl<R: Resource, T: 'static> ViewCx<R, T> {
268 pub fn live(&self) -> &Live<R> {
270 &self.live
271 }
272
273 pub fn permission_state(&self, need: tilo_actions::ActionNeed) -> LocalState<bool> {
279 self.dispatch.permission_state(need)
280 }
281
282 pub fn standing(&self) -> impl Fn() -> Option<ui_core::permissions::Standing> + 'static {
287 self.dispatch.standing_source()
288 }
289
290 pub fn govern_state(&self) -> LocalState<bool> {
293 self.dispatch.govern_state()
294 }
295
296 pub fn arrived_state(&self) -> LocalState<bool> {
304 self.dispatch.arrived_state()
305 }
306
307 pub fn read_only_state(&self) -> LocalState<bool> {
310 self.dispatch.read_only_state()
311 }
312
313 pub fn item(&self) -> Option<&Id> {
315 self.item.as_ref()
316 }
317
318 pub fn history(&self, last: usize) -> LocalState<Vec<super::history::HistoryVersion>> {
323 self.feeds.follow(self.alive.clone(), last)
324 }
325
326 pub fn read<V>(&self, read: impl FnOnce(Option<&T>) -> V) -> V {
329 self.live.with(|state| read((self.focus)(state)))
330 }
331
332 pub fn local<V: Send + Sync + 'static>(&self, initial: V) -> LocalState<V> {
334 LocalState::new(initial)
335 }
336
337 pub fn on_open<O: tilo_resource::Outcome>(&self, mut open: impl FnMut(&str) -> O + 'static) {
346 self.stages.push_opener(
347 self.alive.clone(),
348 Box::new(move |anchor| open(anchor).into_result().map_err(|refusal| refusal.0)),
349 );
350 }
351
352 pub fn on_open_item(
357 &self,
358 mut open: impl FnMut(&Id) -> Result<(), tilo_resource::Refusal> + 'static,
359 ) {
360 self.stages.push_opener(
361 self.alive.clone(),
362 Box::new(move |anchor| {
363 let (_, item) = tilo_resource::items::item_of_resource::<R>(anchor)
364 .ok_or_else(|| format!("`{anchor}` is not an item of {}", R::LABEL))?;
365 open(&item).map_err(|refusal| refusal.0)
366 }),
367 );
368 }
369
370 pub fn reference(
376 &self,
377 app: &str,
378 record: Option<&str>,
379 reference: &tilo_reference::ResourceRef,
380 ) -> ui_core::widget::Chip {
381 let dispatch = self.dispatch.clone();
382 let (app, record, target) = (app.to_owned(), record.map(str::to_owned), reference.clone());
383 ui_core::widget::Chip::reference(reference.label.clone(), &reference.id)
384 .on_view(move || dispatch.open_reference(&app, record.as_deref(), &target))
385 }
386
387 pub fn open_search(&self, request: &ui_core::search::SearchRequest) {
391 self.dispatch.open_search(request);
392 }
393
394 pub fn search_button(&self, request: ui_core::search::SearchRequest) -> Button {
397 ui_core::search::search_button(request)
398 }
399
400 pub fn on_stage(&self, stage: impl FnMut(Option<&str>) + 'static) {
407 self.stages.push(self.alive.clone(), Box::new(stage));
408 }
409
410 fn reader<V: Default + 'static>(
411 &self,
412 read: impl Fn(&T) -> V + 'static,
413 ) -> impl Fn(&R::State) -> V + 'static {
414 let focus = Rc::clone(&self.focus);
415 move |state| (focus)(state).map(&read).unwrap_or_default()
416 }
417
418 fn control_id(&self, action: &str) -> String {
421 let base = match &self.item {
422 Some(item) => format!("{action}-{item}"),
423 None => action.to_owned(),
424 };
425 let mut seen = self.controls.borrow_mut();
426 let count = seen.entry(base.clone()).or_insert(0);
427 *count = count.saturating_add(1);
428 if *count == 1 {
429 base
430 } else {
431 format!("{base}-{count}")
432 }
433 }
434
435 fn follow<V: PartialEq + Default + Clone + 'static>(
438 &self,
439 read: impl Fn(&T) -> V + 'static,
440 sink: impl FnMut(V) + 'static,
441 ) -> V {
442 self.follow_state(Reads::Always, self.reader(read), sink)
443 }
444
445 fn follow_state<V: PartialEq + Clone + 'static>(
448 &self,
449 reads: Reads,
450 read: impl Fn(&R::State) -> V + 'static,
451 mut sink: impl FnMut(V) + 'static,
452 ) -> V {
453 let initial = self.live.with(&read);
454 let mut last = initial.clone();
455 self.updaters.push(
456 self.alive.clone(),
457 self.gate.clone(),
458 reads,
459 Box::new(move |state| {
460 let next = read(state);
461 if next != last {
462 last.clone_from(&next);
463 sink(next);
464 }
465 }),
466 );
467 initial
468 }
469
470 pub fn text(&self, read: impl Fn(&T) -> String + 'static) -> Text {
472 let live = self.live.clone();
473 let read = self.reader(read);
474 Text::dynamic(move || live.with(&read))
475 }
476
477 pub fn button(&self, label: impl Into<LabelKey>, submit: impl Submit<R>) -> Button {
481 let label: LabelKey = label.into();
482 let id = self.control_id(submit.action_id());
483 let button = Button::new(&id, label.as_str());
484 match submit.control_binding(&self.actions()) {
485 Some(binding) => button.on_action(binding),
486 None => disabled(button),
487 }
488 }
489
490 pub fn form<P: Params<Resource = R>>(&self) -> Form<P> {
497 self.forms.get::<P>()
498 }
499
500 pub fn input<M>(&self, field: impl Bindable<R, String, M>) -> TextInput {
504 match field.attach(self, Path::replace) {
505 Attached::Record { id, state, binding } => {
506 let input = TextInput::from_state(&state).with_id(&id);
507 match binding {
508 Some(binding) => input.on_action(*binding),
509 None => input.disabled(true),
510 }
511 }
512 Attached::Local { id, state } => TextInput::from_state(&state).with_id(&id),
513 }
514 }
515
516 pub fn checkbox<M>(
519 &self,
520 label: impl Into<LabelKey>,
521 field: impl Bindable<R, bool, M>,
522 ) -> Checkbox {
523 let label: LabelKey = label.into();
524 match field.attach(self, Path::set) {
525 Attached::Record { id, state, binding } => {
526 let checkbox = Checkbox::from_state(&state).with_id(&id).with_label(label);
527 match binding {
528 Some(binding) => checkbox.on_action(*binding),
529 None => checkbox.with_enabled(false),
530 }
531 }
532 Attached::Local { id, state } => {
533 Checkbox::from_state(&state).with_id(&id).with_label(label)
534 }
535 }
536 }
537
538 pub fn control<V, C, M>(
542 &self,
543 make: impl FnOnce(&LocalState<V>) -> C,
544 field: impl Bindable<R, V, M>,
545 ) -> C
546 where
547 V: Settable + 'static,
548 C: ActionControl<V>,
549 {
550 match field.attach(self, Path::set) {
551 Attached::Record { state, binding, .. } => {
552 let control = make(&state);
553 match binding {
554 Some(binding) => control.bind_action(*binding),
555 None => control.unavailable(),
556 }
557 }
558 Attached::Local { state, .. } => make(&state),
559 }
560 }
561
562 pub fn bind_declarations(&self, bind: impl Fn(&R::State) -> Vec<ActionDecl<R>> + 'static) {
572 if let Some(gate) = &self.gate {
574 gate.read_while_unmounted();
575 }
576 self.bindings
577 .0
578 .borrow_mut()
579 .push((self.alive.clone(), Box::new(bind)));
580 }
581
582 pub fn actions(&self) -> Actions<R> {
585 self.action_bindings.clone()
586 }
587
588 #[must_use]
592 pub fn toolbar(&self) -> super::WindowBar<R> {
593 super::WindowBar::new(self.dispatcher())
594 }
595
596 pub fn value<V>(&self, read: impl Fn(&T) -> V + 'static) -> LocalState<V>
599 where
600 V: PartialEq + Default + Clone + 'static,
601 {
602 let state = LocalState::new(V::default());
603 let sink = state.clone();
604 let initial = self.follow(read, move |value| sink.set(value));
605 state.set(initial);
606 state
607 }
608
609 pub fn control_with<V, C>(
617 &self,
618 make: impl FnOnce(&LocalState<V>) -> C,
619 read: impl Fn(&T) -> V + 'static,
620 action: impl Binds<R, V>,
621 ) -> C
622 where
623 V: PartialEq + Default + Clone + 'static,
624 C: ActionControl<V>,
625 {
626 let control = make(&self.value(read));
627 self.actions().bind(control, action)
628 }
629
630 pub fn switch<K>(
634 &self,
635 key: &LocalState<K>,
636 part: impl Fn(&mut ViewCx<R, T>, &K) -> Element + 'static,
637 ) -> Element
638 where
639 K: Clone + PartialEq + 'static,
640 {
641 let scope = self.part_scope();
642 Switch::keyed(
643 key.clone(),
644 Box::new(move |value: &K, alive: &Rc<()>| part(&mut scope(alive), value)),
645 )
646 }
647
648 pub fn arriving<V, E>(
656 &self,
657 arriving: &Arriving<V, E>,
658 part: impl Fn(&mut ViewCx<R, T>, &V) -> Element + 'static,
659 ) -> ArrivingView
660 where
661 V: Clone + 'static,
662 E: std::fmt::Display + 'static,
663 {
664 let scope = Rc::new(self.part_scope());
665 let part = Rc::new(part);
666 ArrivingView::new(arriving, move |value: &V| {
667 let (scope, part, value) = (Rc::clone(&scope), Rc::clone(&part), value.clone());
668 arrived_part(Box::new(move |alive: &Rc<()>| {
669 part(&mut scope(alive), &value)
670 }))
671 })
672 }
673
674 pub(crate) fn arriving_workspace(
679 &self,
680 holdings: &Arriving<(), WorkspaceUnreachable>,
681 part: impl Fn(&mut ViewCx<R, T>) -> Element + 'static,
682 ) -> Element {
683 let scope = self.part_scope();
684 workspace_arrival(
685 holdings,
686 Rc::new(move |alive: &Rc<()>| part(&mut scope(alive))),
687 )
688 }
689
690 pub fn mounts(&self) -> Mounts<R, T> {
694 Mounts {
695 parent: self.env.alive.clone(),
696 scope: Rc::new(self.part_scope()),
697 }
698 }
699
700 fn part_scope(&self) -> impl Fn(&Rc<()>) -> ViewCx<R, T> + 'static {
703 let env = self.env.clone();
704 let focus = Rc::clone(&self.focus);
705 let updaters = self.updaters.clone();
706 let gate = self.gate.clone();
707 let action_bindings = self.action_bindings.clone();
708 let item = self.item.clone();
709 let path = self.path.clone();
710 let stages = self.stages.clone();
711 let feeds = self.feeds.clone();
712 let bindings = self.bindings.clone();
713 move |alive: &Rc<()>| ViewCx {
714 env: env.living(alive),
715 focus: Rc::clone(&focus),
716 updaters: updaters.clone(),
717 gate: gate.clone(),
718 action_bindings: action_bindings.clone(),
719 item: item.clone(),
720 path: path.clone(),
721 controls: Rc::default(),
723 forms: Forms::default(),
724 stages: stages.clone(),
725 feeds: feeds.clone(),
726 bindings: bindings.clone(),
727 }
728 }
729
730 pub fn surface<S, E>(&self, surface: S) -> Element
734 where
735 S: ResourceSurface<R, E> + 'static,
736 E: Effect,
737 {
738 SurfaceCx {
739 env: self.env.clone(),
740 jobs: self.effects.surface_jobs(),
741 actions: self.actions(),
742 stages: self.stages.clone(),
743 scope: None,
744 }
745 .host_surface(surface)
746 }
747
748 pub fn publishing(&self, publishes: super::Publishes<R>, content: Element) -> Element {
753 let wake = self.env.effects.env().map(|env| env.wake.clone());
754 super::publishing::publishing(publishes, content, wake)
755 }
756
757 pub fn upkeep(&self, upkeep: super::Upkeep, content: Element) -> Element {
763 super::upkeep::upkeep(upkeep, content)
764 }
765
766 pub fn effects<E: Effect>(
778 &self,
779 handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
780 ) -> EffectsHandle {
781 self.answer_effects::<E>(|effects, alive| effects.handle(alive, handle))
782 }
783
784 pub fn window_effects<E: Effect, W: ui_core::widget::Widget + 'static>(
792 &self,
793 handle: impl FnMut(&mut W, E, &mut EffectCx<R>) -> EffectAnswer + 'static,
794 ) -> EffectsHandle {
795 self.answer_effects::<E>(|effects, alive| effects.handle_in_window(alive, handle))
796 }
797
798 pub fn window_host_effects<E: Effect, W: ui_core::widget::Widget + 'static>(
804 &self,
805 handle: impl FnMut(&mut W, E, &mut EffectCx<R, W>) -> EffectAnswer + 'static,
806 ) -> EffectsHandle {
807 self.answer_effects::<E>(|effects, alive| effects.handle_hosted_in_window(alive, handle))
808 }
809
810 fn answer_effects<E: Effect>(
813 &self,
814 register: impl FnOnce(&Effects<R>, std::rc::Weak<()>),
815 ) -> EffectsHandle {
816 let status: StatusSource = Rc::default();
817 if let Some(gate) = &self.gate {
820 gate.read_while_unmounted();
821 }
822 let source = Rc::clone(&status);
825 let kept = super::effect::KeptSpecs::default();
826 self.effects.publish::<E>(self.alive.clone(), move || {
827 current_specs(&source, &kept, E::specs)
828 });
829 if self.effects.env().is_some() {
830 register(&self.effects, self.alive.clone());
831 }
832 EffectsHandle { status }
833 }
834
835 pub fn jobs(&self) -> super::Jobs<R> {
838 self.effects.view_jobs()
839 }
840
841 pub fn window_jobs<W: ui_core::widget::Widget + 'static>(&self) -> super::Jobs<R> {
847 self.effects.window_jobs::<W>(self.alive.clone())
848 }
849
850 pub fn effect_button<E: Effect>(&self, label: impl Into<LabelKey>, effect: &E) -> Button {
853 let id = self.control_id(effect.id());
854 self.actions()
855 .bind_effect_invocation(Button::new(&id, label.into().as_str()), effect)
856 }
857}
858
859impl<R: Resource, T: Model + 'static> ViewCx<R, T> {
860 #[allow(
869 clippy::panic,
870 reason = "a row whose collection has no path is a mistake the first run of the view shows"
871 )]
872 pub fn paths(&self) -> T::Paths<R> {
873 let Some(path) = &self.path else {
874 panic!(
875 "this row's collection is not a field of the value `each` read it from; read it as `|value| &value.field`"
876 );
877 };
878 T::paths_at(Path::from_segments(path.clone()))
879 }
880
881 pub fn each<I: Model + 'static>(
890 &self,
891 items: impl Fn(&T) -> &Collection<I> + 'static,
892 row: impl Fn(&mut ViewCx<R, I>) -> Element + 'static,
893 ) -> Each<R> {
894 let items: ItemsOf<T, I> = Rc::new(items);
895 let located = self.path.as_ref().and_then(|base| {
896 self.read(|value| {
897 let value = value?;
898 let relative = locate(value, items(value))?;
899 let mut path = base.clone();
900 path.extend(relative);
901 Some(path)
902 })
903 });
904 let focus = Rc::clone(&self.focus);
905 let read_items = Rc::clone(&items);
906 let ids = move |state: &R::State| -> Vec<Id> {
907 (focus)(state).map_or_else(Vec::new, |value| read_items(value).ids().cloned().collect())
908 };
909 let stale = Rc::new(std::cell::Cell::new(false));
910 let mark = Rc::clone(&stale);
911 self.updaters.push(
912 self.alive.clone(),
913 self.gate.clone(),
914 located.clone().map_or(Reads::Always, Reads::Members),
915 Box::new(move |_| mark.set(true)),
916 );
917 let parent_focus = Rc::clone(&self.focus);
918 let env = self.env.clone();
919 let updaters = self.updaters.clone();
920 let action_bindings = self.action_bindings.clone();
921 let controls = Rc::clone(&self.controls);
922 let stages = self.stages.clone();
923 let feeds = self.feeds.clone();
924 let bindings = self.bindings.clone();
925 Each::new(
926 self.live.clone(),
927 self.dispatch.clone(),
928 Box::new(ids),
929 stale,
930 self.updaters.clone(),
931 Box::new(
932 move |id: &Id, alive: &Rc<()>, gate: &super::reactive::Gate| {
933 let parent = Rc::clone(&parent_focus);
934 let items = Rc::clone(&items);
935 let key = id.clone();
936 let focus: Focus<R, I> = Rc::new(move |state| {
937 (parent)(state).and_then(|value| items(value).get(&key))
938 });
939 let path = located.as_ref().map(|located| {
940 let mut path = located.clone();
941 path.push(Segment::Item(id.clone()));
942 path
943 });
944 let mut cx = ViewCx {
945 env: env.living(alive),
946 focus,
947 updaters: updaters.clone(),
948 gate: Some(gate.clone()),
949 action_bindings: action_bindings.clone(),
950 item: Some(id.clone()),
951 path,
952 controls: Rc::clone(&controls),
953 forms: Forms::default(),
954 stages: stages.clone(),
955 feeds: feeds.clone(),
956 bindings: bindings.clone(),
957 };
958 row(&mut cx)
959 },
960 ),
961 )
962 }
963}
964
965type ItemsOf<T, I> = Rc<dyn Fn(&T) -> &Collection<I>>;
967
968fn locate<T: Model + 'static, I: 'static>(
971 value: &T,
972 target: &Collection<I>,
973) -> Option<Vec<Segment>> {
974 fn walk<T: Model + 'static>(
975 value: &T,
976 shape: tilo_resource::Shape,
977 prefix: &mut Vec<Segment>,
978 target: *const u8,
979 ) -> bool {
980 for field in shape.fields() {
981 prefix.push(Segment::Field(field.key.to_owned()));
982 let found = match field.shape {
983 tilo_resource::Shape::Collection(_) => value
984 .get_at(prefix)
985 .is_some_and(|at| std::ptr::eq(std::ptr::from_ref(at).cast::<u8>(), target)),
986 tilo_resource::Shape::Record(_) => walk(value, field.shape, prefix, target),
987 tilo_resource::Shape::Value
988 | tilo_resource::Shape::Keyed(_)
989 | tilo_resource::Shape::Map(_) => false,
990 };
991 if found {
992 return true;
993 }
994 prefix.pop();
995 }
996 false
997 }
998 let mut path = Vec::new();
999 walk(
1000 value,
1001 T::SHAPE,
1002 &mut path,
1003 std::ptr::from_ref(target).cast::<u8>(),
1004 )
1005 .then_some(path)
1006}
1007
1008impl<R, T: 'static> ViewCx<R, T>
1010where
1011 R: Resource<State = R> + Model,
1012{
1013 fn follow_path<V: PartialEq + Default + Clone + 'static>(
1016 &self,
1017 path: &Path<R, V>,
1018 sink: impl FnMut(V) + 'static,
1019 ) -> V {
1020 let read = path.clone();
1021 self.follow_state(
1022 Reads::At(path.segments().to_vec()),
1023 move |state| read.get(state).cloned().unwrap_or_default(),
1024 sink,
1025 )
1026 }
1027
1028 fn path_control_id<V>(&self, path: &Path<R, V>) -> String {
1031 let base = path
1032 .segments()
1033 .iter()
1034 .map(ToString::to_string)
1035 .collect::<Vec<_>>()
1036 .join("-");
1037 let mut seen = self.controls.borrow_mut();
1038 let count = seen.entry(base.clone()).or_insert(0);
1039 *count = count.saturating_add(1);
1040 if *count == 1 {
1041 base
1042 } else {
1043 format!("{base}-{count}")
1044 }
1045 }
1046
1047 pub fn value_at<V>(&self, path: &Path<R, V>) -> LocalState<V>
1051 where
1052 V: PartialEq + Default + Clone + 'static,
1053 {
1054 let state = LocalState::new(V::default());
1055 let sink = state.clone();
1056 let initial = self.follow_path(path, move |value| sink.set(value));
1057 state.set(initial);
1058 state
1059 }
1060
1061 pub fn route_graph_text(
1065 &self,
1066 values: &ui_core::UiValues,
1067 ui_path: &str,
1068 path: Path<R, String>,
1069 ) {
1070 let shown = self.value_at(&path);
1071 if let Some(binding) = self.actions().edit_binding(path, Path::replace) {
1072 values.route(ui_path, shown, binding);
1073 }
1074 }
1075
1076 pub fn route_graph_bool(&self, values: &ui_core::UiValues, ui_path: &str, path: Path<R, bool>) {
1079 let shown = self.value_at(&path);
1080 if let Some(binding) = self.actions().edit_binding(path, Path::set) {
1081 values.route_bool(ui_path, shown, binding);
1082 }
1083 }
1084
1085 pub fn control_map<V, I, C>(
1091 &self,
1092 make: impl FnOnce(&LocalState<I>) -> C,
1093 path: Path<R, V>,
1094 show: impl Fn(&V) -> I + 'static,
1095 store: impl Fn(I) -> V + Send + Sync + 'static,
1096 ) -> C
1097 where
1098 V: Settable + PartialEq + Default + Clone + 'static,
1099 I: PartialEq + Clone + Send + Sync + 'static,
1100 C: ActionControl<I>,
1101 {
1102 let read = path.clone();
1103 let initial = show(&V::default());
1104 let state = LocalState::new(initial);
1105 let sink = state.clone();
1106 let shown = self.follow_state(
1107 Reads::At(path.segments().to_vec()),
1108 move |record| show(&read.get(record).cloned().unwrap_or_default()),
1109 move |value| sink.set(value),
1110 );
1111 state.set(shown);
1112 let control = make(&state);
1113 let binding = self
1114 .actions()
1115 .edit_binding(path.cast::<I>(), move |path, input| {
1116 path.clone().cast::<V>().set(store(input))
1117 });
1118 match binding {
1119 Some(binding) => control.bind_action(binding),
1120 None => control.unavailable(),
1121 }
1122 }
1123}
1124
1125pub trait Bindable<R: Resource, V, M>: Sized {
1129 #[doc(hidden)]
1130 fn attach<T: 'static>(
1131 self,
1132 cx: &ViewCx<R, T>,
1133 edit: fn(&Path<R, V>, V) -> Edit<R>,
1134 ) -> Attached<V>;
1135}
1136
1137impl<R, V> Bindable<R, V, RecordField> for Path<R, V>
1138where
1139 R: Resource<State = R> + Model,
1140 V: PartialEq + Default + Clone + Send + Sync + 'static,
1141{
1142 fn attach<T: 'static>(
1143 self,
1144 cx: &ViewCx<R, T>,
1145 edit: fn(&Path<R, V>, V) -> Edit<R>,
1146 ) -> Attached<V> {
1147 let id = cx.path_control_id(&self);
1148 let state = cx.value_at(&self);
1149 let binding = cx.actions().edit_binding(self, edit).map(Box::new);
1150 Attached::Record { id, state, binding }
1151 }
1152}
1153
1154impl<R, P, V> Bindable<R, V, FormField> for Path<P, V>
1155where
1156 R: Resource,
1157 P: Params<Resource = R>,
1158 V: serde::Serialize + Default + Send + Sync + 'static,
1159{
1160 fn attach<T: 'static>(
1161 self,
1162 cx: &ViewCx<R, T>,
1163 _edit: fn(&Path<R, V>, V) -> Edit<R>,
1164 ) -> Attached<V> {
1165 let state = cx.form::<P>().field(&self);
1166 let id = std::iter::once(P::ACTION.id.to_owned())
1167 .chain(self.segments().iter().map(ToString::to_string))
1168 .collect::<Vec<_>>()
1169 .join("-");
1170 Attached::Local { id, state }
1171 }
1172}
1173
1174type StatusSource = Rc<RefCell<Option<Box<dyn Fn() -> Vec<EffectStatus>>>>>;
1175
1176pub struct EffectsHandle {
1178 status: StatusSource,
1179}
1180
1181impl EffectsHandle {
1182 pub fn status(self, status: impl Fn() -> Vec<EffectStatus> + 'static) {
1186 *self.status.borrow_mut() = Some(Box::new(status));
1187 }
1188}