Skip to main content

tilo_app/framework/
view.rs

1//! Building views: widgets whose values come from the resource and whose
2//! edits are actions.
3
4use 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::i18n::LabelKey;
14use ui_core::record::RecordSaveStatus;
15use ui_core::widget::{Button, Checkbox, Element, Text, TextInput};
16
17use super::controls::{ActionControl, Actions};
18use super::dispatch::{Dispatcher, Submit};
19use super::each::Each;
20use super::effect::{EffectAnswer, EffectCx, EffectStatus, Effects};
21use super::env::RecordEnv;
22use super::form::{Attached, Form, FormField, Forms, RecordField};
23use super::live::Live;
24use super::reactive::{Reads, Updaters};
25use super::scenario::Run;
26use super::settings::ViewStorage;
27use super::stage::Stages;
28use super::surface::{ResourceSurface, SurfaceCx, SurfaceHost};
29use super::switch::Switch;
30
31fn disabled(mut button: Button) -> Button {
32    button.set_enabled(false);
33    button
34}
35
36type Bind<R> = Box<dyn Fn(&<R as Resource>::State) -> Vec<ActionDecl<R>>>;
37type BindList<R> = Vec<(Weak<()>, Bind<R>)>;
38
39/// The edits views bound to their local state
40/// ([`ViewCx::bind_declarations`]), each tied to the view scope that bound
41/// it.
42pub(crate) struct Bindings<R: Resource>(Rc<RefCell<BindList<R>>>);
43
44impl<R: Resource> Clone for Bindings<R> {
45    fn clone(&self) -> Self {
46        Self(Rc::clone(&self.0))
47    }
48}
49
50impl<R: Resource> Bindings<R> {
51    pub(crate) fn new() -> Self {
52        Self(Rc::default())
53    }
54
55    /// Every live view's bound edits over `state`.
56    pub(crate) fn declarations(&self, state: &R::State) -> Vec<ActionDecl<R>> {
57        let mut bindings = self.0.borrow_mut();
58        bindings.retain(|(alive, _)| alive.strong_count() > 0);
59        bindings.iter().flat_map(|(_, bind)| bind(state)).collect()
60    }
61}
62
63/// Projects the resource state onto the value a view scope reads: the whole
64/// state at the root, one item inside an [`Each`] row.
65pub(crate) type Focus<R, T> = Rc<dyn for<'a> Fn(&'a <R as Resource>::State) -> Option<&'a T>>;
66
67/// Everything a view needs: reads of the resource state, bindings of controls
68/// to actions, local state, and custom surfaces.
69///
70/// `T` is what this scope reads — the resource state at the root, one item in
71/// a row built by [`ViewCx::each`].
72pub struct ViewCx<R: Resource, T: 'static = <R as Resource>::State> {
73    pub(crate) env: RecordEnv<R>,
74    pub(crate) focus: Focus<R, T>,
75    pub(crate) updaters: Updaters<R>,
76    /// The row of an [`Each`] this scope builds, whose readers run while it
77    /// is mounted; `None` outside a row.
78    pub(crate) gate: Option<super::reactive::Gate>,
79    pub(crate) action_bindings: Actions<R>,
80    pub(crate) item: Option<Id>,
81    /// Where this scope's value sits in the record: the root, or the item a
82    /// row stands for. `None` inside a row of a collection the view reads
83    /// in a way no path names.
84    pub(crate) path: Option<Vec<Segment>>,
85    pub(crate) controls: Rc<RefCell<HashMap<String, u32>>>,
86    /// The forms filled in in this scope ([`Self::form`]).
87    pub(crate) forms: Forms,
88    pub(crate) stages: Stages,
89    pub(crate) feeds: super::history::HistoryFeeds,
90    pub(crate) bindings: Bindings<R>,
91}
92
93impl<R: Resource, T: 'static> std::ops::Deref for ViewCx<R, T> {
94    type Target = RecordEnv<R>;
95    fn deref(&self) -> &RecordEnv<R> {
96        &self.env
97    }
98}
99
100impl<R: Resource> ViewCx<R> {
101    /// The scope of a freshly built root view: its own updaters, save status
102    /// and staging, none of which another root shares.
103    pub(crate) fn root(
104        live: Live<R>,
105        alive: &Rc<()>,
106        dispatch: Dispatcher<R>,
107        published: tilo_actions::Resource,
108        run: Run,
109        storage: Rc<ViewStorage>,
110    ) -> Self {
111        // The history verbs every app answers are bindable like its own.
112        let mut published = published;
113        for spec in super::history::specs() {
114            if !published.actions.iter().any(|action| action.id == spec.id) {
115                published = published.with_action(spec);
116            }
117        }
118        let app = dispatch.app_id();
119        Self {
120            env: RecordEnv {
121                live,
122                alive: Rc::downgrade(alive),
123                dispatch,
124                saved: LocalState::new(RecordSaveStatus::Saved),
125                run,
126                storage,
127                effects: Effects::inert(),
128            },
129            focus: Rc::new(|state| Some(state)),
130            updaters: Updaters::new(),
131            gate: None,
132            action_bindings: Actions::new(app, Rc::new(published)),
133            item: None,
134            path: Some(Vec::new()),
135            controls: Rc::default(),
136            forms: Forms::default(),
137            stages: Stages::new(),
138            feeds: super::history::HistoryFeeds::default(),
139            bindings: Bindings::new(),
140        }
141    }
142}
143
144impl<R: Resource, T: 'static> ViewCx<R, T> {
145    /// The live resource state, for reads a binding does not cover.
146    pub fn live(&self) -> &Live<R> {
147        &self.live
148    }
149
150    /// Observe what this participant may do, without storing their role in
151    /// the shared resource. Use this with [`Self::switch`] for an owner-only
152    /// pane, or with a bound widget for a permission-specific label. Changes
153    /// follow the host even when the document and write access stay unchanged.
154    /// This controls presentation; the action door remains the authority.
155    pub fn permission_state(&self, need: tilo_actions::ActionNeed) -> LocalState<bool> {
156        self.dispatch.permission_state(need)
157    }
158
159    /// Whether this participant may govern the room, observed: for an
160    /// owner-only pane with [`Self::switch`].
161    pub fn govern_state(&self) -> LocalState<bool> {
162        self.dispatch.govern_state()
163    }
164
165    /// Whether this participant may only view, as state that follows role
166    /// changes while the app runs.
167    pub fn read_only_state(&self) -> LocalState<bool> {
168        self.dispatch.read_only_state()
169    }
170
171    /// The item this scope stands for, inside an [`Each`] row.
172    pub fn item(&self) -> Option<&Id> {
173        self.item.as_ref()
174    }
175
176    /// The resource's last `last` versions, oldest first, kept current as
177    /// the document moves: an in-app history list of what was done and by
178    /// whom, each version addressable by the `undo`, `restore` and
179    /// `checkpoint` actions every app publishes.
180    pub fn history(&self, last: usize) -> LocalState<Vec<super::history::HistoryVersion>> {
181        self.feeds.follow(self.alive.clone(), last)
182    }
183
184    /// Read the scope's value now (at build time). For values that must
185    /// follow the resource, use a binding such as [`Self::text`].
186    pub fn read<V>(&self, read: impl FnOnce(Option<&T>) -> V) -> V {
187        self.live.with(|state| read((self.focus)(state)))
188    }
189
190    /// Typed process-local view state: never persisted, shared or undone.
191    pub fn local<V: Send + Sync + 'static>(&self, initial: V) -> LocalState<V> {
192        LocalState::new(initial)
193    }
194
195    /// Show the item a search hit opens: `open` runs with the hit's anchor
196    /// (a [`SearchEntry`](tilo_resource::SearchEntry)'s `anchor`) when the
197    /// person picks the hit, or when an agent invokes the resource's `open`
198    /// action. Select the item, switch to its page, scroll it into view:
199    /// set the [`LocalState`]s that show it, as the person's own input would.
200    ///
201    /// `open` may refuse an anchor it cannot show (a row that is gone) by
202    /// returning `Err`; the person or agent is told why.
203    pub fn on_open<O: tilo_resource::Outcome>(&self, mut open: impl FnMut(&str) -> O + 'static) {
204        self.stages.push_opener(
205            self.alive.clone(),
206            Box::new(move |anchor| open(anchor).into_result().map_err(|refusal| refusal.0)),
207        );
208    }
209
210    /// Show the item of the resource a search hit or an agent's `open`
211    /// names: `open` runs with its id, and refuses one it cannot show (an
212    /// item that no longer exists), which the person or agent is told.
213    /// Anchors that name no item of the resource are refused for it.
214    pub fn on_open_item(
215        &self,
216        mut open: impl FnMut(&Id) -> Result<(), tilo_resource::Refusal> + 'static,
217    ) {
218        self.stages.push_opener(
219            self.alive.clone(),
220            Box::new(move |anchor| {
221                let (_, item) = tilo_resource::items::item_of_resource::<R>(anchor)
222                    .ok_or_else(|| format!("`{anchor}` is not an item of {}", R::LABEL))?;
223                open(&item).map_err(|refusal| refusal.0)
224            }),
225        );
226    }
227
228    /// Open the platform search with `request`, narrowed to this app or one
229    /// part of it. Usually a control does this ([`Self::search_button`]); a
230    /// custom handler calls [`Dispatcher::open_search`] on its dispatcher.
231    pub fn open_search(&self, request: &ui_core::search::SearchRequest) {
232        self.dispatch.open_search(request);
233    }
234
235    /// The search launcher for this view's chrome: an icon button that opens
236    /// the platform search with `request`. Apps never draw a search field.
237    pub fn search_button(&self, request: ui_core::search::SearchRequest) -> Button {
238        ui_core::search::search_button(request)
239    }
240
241    /// Stage view state for a screenshot scenario: `stage` runs with the
242    /// scenario's name (`None` for a plain `--shots`) after the scenario's
243    /// actions applied and were laid out, and before the frame is captured.
244    /// Set the [`LocalState`]s a scenario shows — a selection, an open
245    /// dialog — the way the user's input would. It never runs in a live
246    /// window.
247    pub fn on_stage(&self, stage: impl FnMut(Option<&str>) + 'static) {
248        self.stages.push(self.alive.clone(), Box::new(stage));
249    }
250
251    fn reader<V: Default + 'static>(
252        &self,
253        read: impl Fn(&T) -> V + 'static,
254    ) -> impl Fn(&R::State) -> V + 'static {
255        let focus = Rc::clone(&self.focus);
256        move |state| (focus)(state).map(&read).unwrap_or_default()
257    }
258
259    /// A stable, addressable control name: the action id, qualified by the
260    /// row's item, numbered only when the same binding repeats.
261    fn control_id(&self, action: &str) -> String {
262        let base = match &self.item {
263            Some(item) => format!("{action}-{item}"),
264            None => action.to_owned(),
265        };
266        let mut seen = self.controls.borrow_mut();
267        let count = seen.entry(base.clone()).or_insert(0);
268        *count = count.saturating_add(1);
269        if *count == 1 {
270            base
271        } else {
272            format!("{base}-{count}")
273        }
274    }
275
276    /// Keep `sink` in step with a value derived from the resource, read
277    /// again after every change.
278    fn follow<V: PartialEq + Default + Clone + 'static>(
279        &self,
280        read: impl Fn(&T) -> V + 'static,
281        sink: impl FnMut(V) + 'static,
282    ) -> V {
283        self.follow_state(Reads::Always, self.reader(read), sink)
284    }
285
286    /// Keep `sink` in step with `read` of the state, read again after the
287    /// changes `reads` names.
288    fn follow_state<V: PartialEq + Clone + 'static>(
289        &self,
290        reads: Reads,
291        read: impl Fn(&R::State) -> V + 'static,
292        mut sink: impl FnMut(V) + 'static,
293    ) -> V {
294        let initial = self.live.with(&read);
295        let mut last = initial.clone();
296        self.updaters.push(
297            self.alive.clone(),
298            self.gate.clone(),
299            reads,
300            Box::new(move |state| {
301                let next = read(state);
302                if next != last {
303                    last.clone_from(&next);
304                    sink(next);
305                }
306            }),
307        );
308        initial
309    }
310
311    /// Text that always shows `read` of the resource.
312    pub fn text(&self, read: impl Fn(&T) -> String + 'static) -> Text {
313        let live = self.live.clone();
314        let read = self.reader(read);
315        Text::dynamic(move || live.with(&read))
316    }
317
318    /// A button that submits one fully specified action: a verb with its
319    /// arguments (`Snippets::NEW_SNIPPET.with(..)`), a field edit
320    /// (`p.favorite.set(true)`) or a form ([`Form::submit`]).
321    pub fn button(&self, label: impl Into<LabelKey>, submit: impl Submit<R>) -> Button {
322        let label: LabelKey = label.into();
323        let id = self.control_id(submit.action_id());
324        let button = Button::new(&id, label.as_str());
325        match submit.control_binding(&self.actions()) {
326            Some(binding) => button.on_action(binding),
327            None => disabled(button),
328        }
329    }
330
331    /// The form of the verb whose parameters are `P`
332    /// (`cx.form::<bookmarks::Add>()`): local values, never synced or
333    /// undone, that controls bind to with the parameters' paths
334    /// (`cx.input(form.paths().url)`) and a button submits as one
335    /// invocation (`cx.button("bookmarks-add", form.submit())`). The same
336    /// form is answered for `P` everywhere in this scope.
337    pub fn form<P: Params<Resource = R>>(&self) -> Form<P> {
338        self.forms.get::<P>()
339    }
340
341    /// A text field for the text at `field`: a path into the record, whose
342    /// committed edits replace it ([`Path::replace`]; the keystrokes of one
343    /// word are one undo step), or into a form ([`Self::form`]).
344    pub fn input<M>(&self, field: impl Bindable<R, String, M>) -> TextInput {
345        match field.attach(self, Path::replace) {
346            Attached::Record { id, state, binding } => {
347                let input = TextInput::from_state(&state).with_id(&id);
348                match binding {
349                    Some(binding) => input.on_action(*binding),
350                    None => input.disabled(true),
351                }
352            }
353            Attached::Local { id, state } => TextInput::from_state(&state).with_id(&id),
354        }
355    }
356
357    /// A checkbox for the flag at `field`, a path into the record or a form;
358    /// toggling sets it.
359    pub fn checkbox<M>(
360        &self,
361        label: impl Into<LabelKey>,
362        field: impl Bindable<R, bool, M>,
363    ) -> Checkbox {
364        let label: LabelKey = label.into();
365        match field.attach(self, Path::set) {
366            Attached::Record { id, state, binding } => {
367                let checkbox = Checkbox::from_state(&state).with_id(&id).with_label(label);
368                match binding {
369                    Some(binding) => checkbox.on_action(*binding),
370                    None => checkbox.with_enabled(false),
371                }
372            }
373            Attached::Local { id, state } => {
374                Checkbox::from_state(&state).with_id(&id).with_label(label)
375            }
376        }
377    }
378
379    /// Any toolkit control for the value at `field`, a path into the record
380    /// or a form, setting it with each committed value:
381    /// `cx.control(Slider::from_state, p.volume)`.
382    pub fn control<V, C, M>(
383        &self,
384        make: impl FnOnce(&LocalState<V>) -> C,
385        field: impl Bindable<R, V, M>,
386    ) -> C
387    where
388        V: Settable + 'static,
389        C: ActionControl<V>,
390    {
391        match field.attach(self, Path::set) {
392            Attached::Record { state, binding, .. } => {
393                let control = make(&state);
394                match binding {
395                    Some(binding) => control.bind_action(*binding),
396                    None => control.unavailable(),
397                }
398            }
399            Attached::Local { state, .. } => make(&state),
400        }
401    }
402
403    /// Publish edits bound to this view's local state — the folder shown,
404    /// the row selected — on the resource beside its declared actions, as
405    /// typed builders of its action: `bind` runs whenever the published
406    /// tree is read, with the current resource state, and reads the local
407    /// state it captured. Invoking one dispatches what it builds through the
408    /// same permission check, confirmation and undo as any other action. The
409    /// binding lasts as long as this view scope (a row's ends with the row);
410    /// an id the resource already publishes keeps the resource's action
411    /// unless the bound one refines it ([`tilo_resource::bind_declaration`]).
412    pub fn bind_declarations(&self, bind: impl Fn(&R::State) -> Vec<ActionDecl<R>> + 'static) {
413        // The published tree reads what `bind` captured, mounted row or not.
414        if let Some(gate) = &self.gate {
415            gate.read_while_unmounted();
416        }
417        self.bindings
418            .0
419            .borrow_mut()
420            .push((self.alive.clone(), Box::new(bind)));
421    }
422
423    /// The resource's published actions, for binding controls built outside
424    /// the view function: the rows a table, list or tree creates on demand.
425    pub fn actions(&self) -> Actions<R> {
426        self.action_bindings.clone()
427    }
428
429    /// A value that follows `read` of the resource, for any toolkit control
430    /// built from a [`LocalState`] (`Slider::from_state`, `Dropdown::from_state`).
431    pub fn value<V>(&self, read: impl Fn(&T) -> V + 'static) -> LocalState<V>
432    where
433        V: PartialEq + Default + Clone + 'static,
434    {
435        let state = LocalState::new(V::default());
436        let sink = state.clone();
437        let initial = self.follow(read, move |value| sink.set(value));
438        state.set(initial);
439        state
440    }
441
442    /// A toolkit control bound to a verb: it shows `read` of the resource and
443    /// invokes `action` with each committed value, for a value that is a
444    /// verb's argument rather than a field
445    /// (`cx.control_with(TextInput::from_state, |p| p.shape(&node), Project::SET_SHAPE.map(..))`).
446    /// The control is disabled when the action is not published. A field of
447    /// the record is bound by its path instead ([`Self::input`],
448    /// [`Self::checkbox`], [`Self::control`]).
449    pub fn control_with<V, C>(
450        &self,
451        make: impl FnOnce(&LocalState<V>) -> C,
452        read: impl Fn(&T) -> V + 'static,
453        action: impl Binds<R, V>,
454    ) -> C
455    where
456        V: PartialEq + Default + Clone + 'static,
457        C: ActionControl<V>,
458    {
459        let control = make(&self.value(read));
460        self.actions().bind(control, action)
461    }
462
463    /// A part of the view chosen by view-local state — the detail pane of the
464    /// selected item, the page of a wizard. The part is built again, with its
465    /// own bindings, whenever `key` changes; the previous one is dropped.
466    pub fn switch<K>(
467        &self,
468        key: &LocalState<K>,
469        part: impl Fn(&mut ViewCx<R, T>, &K) -> Element + 'static,
470    ) -> Element
471    where
472        K: Clone + PartialEq + 'static,
473    {
474        let env = self.env.clone();
475        let focus = Rc::clone(&self.focus);
476        let updaters = self.updaters.clone();
477        let gate = self.gate.clone();
478        let action_bindings = self.action_bindings.clone();
479        let item = self.item.clone();
480        let path = self.path.clone();
481        let stages = self.stages.clone();
482        let feeds = self.feeds.clone();
483        let bindings = self.bindings.clone();
484        Switch::keyed(
485            key.clone(),
486            Box::new(move |value: &K, alive: &Rc<()>| {
487                let mut cx = ViewCx {
488                    env: env.living(alive),
489                    focus: Rc::clone(&focus),
490                    updaters: updaters.clone(),
491                    gate: gate.clone(),
492                    action_bindings: action_bindings.clone(),
493                    item: item.clone(),
494                    path: path.clone(),
495                    // Each build of the part names its controls afresh.
496                    controls: Rc::default(),
497                    forms: Forms::default(),
498                    stages: stages.clone(),
499                    feeds: feeds.clone(),
500                    bindings: bindings.clone(),
501                };
502                part(&mut cx, value)
503            }),
504        )
505    }
506
507    /// Host a custom widget or renderer that takes part through the
508    /// [`ResourceSurface`] contract, answering its effect vocabulary `E`
509    /// (none unless it implements `ResourceSurface<R, E>`).
510    pub fn surface<S, E>(&self, mut surface: S) -> Element
511    where
512        S: ResourceSurface<R, E> + 'static,
513        E: Effect,
514    {
515        let jobs = self.effects.surface_jobs();
516        surface.attach(SurfaceCx {
517            env: self.env.clone(),
518            jobs: jobs.clone(),
519            actions: self.actions(),
520        });
521        surface.present(&self.live.get());
522        let host = SurfaceHost::new(
523            self.live.clone(),
524            self.dispatch.clone(),
525            &self.effects,
526            jobs,
527            self.alive.clone(),
528            surface,
529        );
530        let pending = host.staging();
531        self.stages.push(
532            self.alive.clone(),
533            Box::new(move |scenario| {
534                *pending.borrow_mut() = Some(scenario.map(str::to_owned));
535            }),
536        );
537        super::surface::HostedSurface::element(Box::new(host))
538    }
539
540    /// Answer the effect vocabulary `E` — work with no document delta that a
541    /// button, the palette, an agent or a script invokes — with `handle`,
542    /// and publish `E` on the resource while this part of the view lives.
543    ///
544    /// `handle` receives each invocation as a typed value. It finishes at
545    /// once ([`Effected::done`](super::Effected::done)) or starts a job
546    /// through the [`EffectCx`] (for an effect declared `job`); a job's
547    /// completion runs on the UI thread and changes the document through the
548    /// context's dispatcher. Handlers never run in History mode. Say which
549    /// effects are unavailable or relabelled right now with
550    /// [`EffectsHandle::status`].
551    pub fn effects<E: Effect>(
552        &self,
553        handle: impl FnMut(E, &mut EffectCx<R>) -> EffectAnswer + 'static,
554    ) -> EffectsHandle {
555        let status: StatusSource = Rc::default();
556        // The handler and the status the resource publishes read the scope's
557        // local state, mounted row or not.
558        if let Some(gate) = &self.gate {
559            gate.read_while_unmounted();
560        }
561        // Captures and History still describe the controls they draw. Only
562        // execution needs a live environment; publication is inert metadata.
563        let source = Rc::clone(&status);
564        self.effects.publish::<E>(self.alive.clone(), move || {
565            let now = source
566                .borrow()
567                .as_ref()
568                .map(|status| status())
569                .unwrap_or_default();
570            super::effect::published_specs::<E>(&now)
571        });
572        if self.effects.env().is_some() {
573            self.effects.handle(self.alive.clone(), handle);
574        }
575        EffectsHandle { status }
576    }
577
578    /// Jobs the view starts on its own, with nobody invoking an effect: a
579    /// fetch when what it shows changes. See [`Jobs`](super::Jobs).
580    pub fn jobs(&self) -> super::Jobs<R> {
581        self.effects.view_jobs()
582    }
583
584    /// A button that invokes one effect, through the same door (and the
585    /// same confirmation, for a destructive one) as the palette.
586    pub fn effect_button<E: Effect>(&self, label: impl Into<LabelKey>, effect: &E) -> Button {
587        let id = self.control_id(effect.id());
588        self.actions()
589            .bind_effect_invocation(Button::new(&id, label.into().as_str()), effect)
590    }
591}
592
593impl<R: Resource, T: Model + 'static> ViewCx<R, T> {
594    /// The typed paths of this scope's value: the record's fields at the
595    /// root, the item's in a row of [`Self::each`]. Controls bind to them
596    /// (`row.checkbox("snippets-favorite", p.favorite)`) and edits are made
597    /// at them (`p.favorite.set(true)`).
598    ///
599    /// # Panics
600    /// Panics inside a row of a collection [`Self::each`] could not find
601    /// among the fields of the value it was given.
602    #[allow(
603        clippy::panic,
604        reason = "a row whose collection has no path is a mistake the first run of the view shows"
605    )]
606    pub fn paths(&self) -> T::Paths<R> {
607        let Some(path) = &self.path else {
608            panic!(
609                "this row's collection is not a field of the value `each` read it from; read it as `|value| &value.field`"
610            );
611        };
612        T::paths_at(Path::from_segments(path.clone()))
613    }
614
615    /// One row per item of a collection, kept in collection order:
616    /// `cx.each(|snippets| &snippets.snippets, |row| ...)`. A row is built
617    /// once per item and retained while the item exists, so focus and caret
618    /// inside it survive other items changing; rows are added, removed and
619    /// moved only when the collection's membership changes. In a row,
620    /// [`Self::paths`] are rooted at its item. A row's context menu
621    /// (right-click, the Menu key or Shift+F10) offers the actions the
622    /// resource publishes on its item, with the item already bound.
623    pub fn each<I: Model + 'static>(
624        &self,
625        items: impl Fn(&T) -> &Collection<I> + 'static,
626        row: impl Fn(&mut ViewCx<R, I>) -> Element + 'static,
627    ) -> Each<R> {
628        let items: ItemsOf<T, I> = Rc::new(items);
629        let located = self.path.as_ref().and_then(|base| {
630            self.read(|value| {
631                let value = value?;
632                let relative = locate(value, items(value))?;
633                let mut path = base.clone();
634                path.extend(relative);
635                Some(path)
636            })
637        });
638        let focus = Rc::clone(&self.focus);
639        let read_items = Rc::clone(&items);
640        let ids = move |state: &R::State| -> Vec<Id> {
641            (focus)(state).map_or_else(Vec::new, |value| read_items(value).ids().cloned().collect())
642        };
643        let stale = Rc::new(std::cell::Cell::new(false));
644        let mark = Rc::clone(&stale);
645        self.updaters.push(
646            self.alive.clone(),
647            self.gate.clone(),
648            located.clone().map_or(Reads::Always, Reads::Members),
649            Box::new(move |_| mark.set(true)),
650        );
651        let parent_focus = Rc::clone(&self.focus);
652        let env = self.env.clone();
653        let updaters = self.updaters.clone();
654        let action_bindings = self.action_bindings.clone();
655        let controls = Rc::clone(&self.controls);
656        let stages = self.stages.clone();
657        let feeds = self.feeds.clone();
658        let bindings = self.bindings.clone();
659        Each::new(
660            self.live.clone(),
661            self.dispatch.clone(),
662            Box::new(ids),
663            stale,
664            self.updaters.clone(),
665            Box::new(
666                move |id: &Id, alive: &Rc<()>, gate: &super::reactive::Gate| {
667                    let parent = Rc::clone(&parent_focus);
668                    let items = Rc::clone(&items);
669                    let key = id.clone();
670                    let focus: Focus<R, I> = Rc::new(move |state| {
671                        (parent)(state).and_then(|value| items(value).get(&key))
672                    });
673                    let path = located.as_ref().map(|located| {
674                        let mut path = located.clone();
675                        path.push(Segment::Item(id.clone()));
676                        path
677                    });
678                    let mut cx = ViewCx {
679                        env: env.living(alive),
680                        focus,
681                        updaters: updaters.clone(),
682                        gate: Some(gate.clone()),
683                        action_bindings: action_bindings.clone(),
684                        item: Some(id.clone()),
685                        path,
686                        controls: Rc::clone(&controls),
687                        forms: Forms::default(),
688                        stages: stages.clone(),
689                        feeds: feeds.clone(),
690                        bindings: bindings.clone(),
691                    };
692                    row(&mut cx)
693                },
694            ),
695        )
696    }
697}
698
699/// How a scope reads the collection [`ViewCx::each`] shows.
700type ItemsOf<T, I> = Rc<dyn Fn(&T) -> &Collection<I>>;
701
702/// Where `target` sits among the fields of `value`, through nested records:
703/// the path of the collection a view's `each` reads.
704fn locate<T: Model + 'static, I: 'static>(
705    value: &T,
706    target: &Collection<I>,
707) -> Option<Vec<Segment>> {
708    fn walk<T: Model + 'static>(
709        value: &T,
710        shape: tilo_resource::Shape,
711        prefix: &mut Vec<Segment>,
712        target: *const u8,
713    ) -> bool {
714        for field in shape.fields() {
715            prefix.push(Segment::Field(field.key.to_owned()));
716            let found = match field.shape {
717                tilo_resource::Shape::Collection(_) => value
718                    .get_at(prefix)
719                    .is_some_and(|at| std::ptr::eq(std::ptr::from_ref(at).cast::<u8>(), target)),
720                tilo_resource::Shape::Record(_) => walk(value, field.shape, prefix, target),
721                tilo_resource::Shape::Value
722                | tilo_resource::Shape::Keyed(_)
723                | tilo_resource::Shape::Map(_) => false,
724            };
725            if found {
726                return true;
727            }
728            prefix.pop();
729        }
730        false
731    }
732    let mut path = Vec::new();
733    walk(
734        value,
735        T::SHAPE,
736        &mut path,
737        std::ptr::from_ref(target).cast::<u8>(),
738    )
739    .then_some(path)
740}
741
742/// Binding controls to the record's fields by path.
743impl<R, T: 'static> ViewCx<R, T>
744where
745    R: Resource<State = R> + Model,
746{
747    /// Keep `sink` in step with the value at `path`, read again only after
748    /// changes that touched it.
749    fn follow_path<V: PartialEq + Default + Clone + 'static>(
750        &self,
751        path: &Path<R, V>,
752        sink: impl FnMut(V) + 'static,
753    ) -> V {
754        let read = path.clone();
755        self.follow_state(
756            Reads::At(path.segments().to_vec()),
757            move |state| read.get(state).cloned().unwrap_or_default(),
758            sink,
759        )
760    }
761
762    /// A stable, addressable control name for a control bound to `path`:
763    /// its fields and items joined, numbered only when the path repeats.
764    fn path_control_id<V>(&self, path: &Path<R, V>) -> String {
765        let base = path
766            .segments()
767            .iter()
768            .map(ToString::to_string)
769            .collect::<Vec<_>>()
770            .join("-");
771        let mut seen = self.controls.borrow_mut();
772        let count = seen.entry(base.clone()).or_insert(0);
773        *count = count.saturating_add(1);
774        if *count == 1 {
775            base
776        } else {
777            format!("{base}-{count}")
778        }
779    }
780
781    /// The value at `path`, as local state that follows it: for any toolkit
782    /// control built from a [`LocalState`], or a part of the view that shows
783    /// it. It is read again only after changes that touched it.
784    pub fn value_at<V>(&self, path: &Path<R, V>) -> LocalState<V>
785    where
786        V: PartialEq + Default + Clone + 'static,
787    {
788        let state = LocalState::new(V::default());
789        let sink = state.clone();
790        let initial = self.follow_path(path, move |value| sink.set(value));
791        state.set(initial);
792        state
793    }
794
795    /// Route a serialized screen's text inputs over `ui_path` to the text
796    /// field at `path` ([`UiValues::route`](ui_core::UiValues::route)): they
797    /// show it and each edit replaces it.
798    pub fn route_graph_text(
799        &self,
800        values: &ui_core::UiValues,
801        ui_path: &str,
802        path: Path<R, String>,
803    ) {
804        let shown = self.value_at(&path);
805        if let Some(binding) = self.actions().edit_binding(path, Path::replace) {
806            values.route(ui_path, shown, binding);
807        }
808    }
809
810    /// Route a serialized screen's checked controls over `ui_path` to the
811    /// flag at `path`: they show it and each toggle sets it.
812    pub fn route_graph_bool(&self, values: &ui_core::UiValues, ui_path: &str, path: Path<R, bool>) {
813        let shown = self.value_at(&path);
814        if let Some(binding) = self.actions().edit_binding(path, Path::set) {
815            values.route_bool(ui_path, shown, binding);
816        }
817    }
818
819    /// A toolkit control whose value is not the field's own type: a slider
820    /// (`f64`) for an optional hue (`Option<f64>`), a dropdown of names for
821    /// a flag. It shows `show` of the value at `path` and sets the field to
822    /// `store` of each committed value:
823    /// `cx.control_map(Slider::from_state, p.hue, |hue| hue.unwrap_or(0.5), Some)`.
824    pub fn control_map<V, I, C>(
825        &self,
826        make: impl FnOnce(&LocalState<I>) -> C,
827        path: Path<R, V>,
828        show: impl Fn(&V) -> I + 'static,
829        store: impl Fn(I) -> V + Send + Sync + 'static,
830    ) -> C
831    where
832        V: Settable + PartialEq + Default + Clone + 'static,
833        I: PartialEq + Clone + Send + Sync + 'static,
834        C: ActionControl<I>,
835    {
836        let read = path.clone();
837        let initial = show(&V::default());
838        let state = LocalState::new(initial);
839        let sink = state.clone();
840        let shown = self.follow_state(
841            Reads::At(path.segments().to_vec()),
842            move |record| show(&read.get(record).cloned().unwrap_or_default()),
843            move |value| sink.set(value),
844        );
845        state.set(shown);
846        let control = make(&state);
847        let binding = self
848            .actions()
849            .edit_binding(path.cast::<I>(), move |path, input| {
850                path.clone().cast::<V>().set(store(input))
851            });
852        match binding {
853            Some(binding) => control.bind_action(binding),
854            None => control.unavailable(),
855        }
856    }
857}
858
859/// What a control shows and changes: a field of the record, by its
860/// [`Path`], or a field of a form ([`ViewCx::form`]), by the path into the
861/// verb's parameters. `M` tells the two apart and is always inferred.
862pub trait Bindable<R: Resource, V, M>: Sized {
863    #[doc(hidden)]
864    fn attach<T: 'static>(
865        self,
866        cx: &ViewCx<R, T>,
867        edit: fn(&Path<R, V>, V) -> Edit<R>,
868    ) -> Attached<V>;
869}
870
871impl<R, V> Bindable<R, V, RecordField> for Path<R, V>
872where
873    R: Resource<State = R> + Model,
874    V: PartialEq + Default + Clone + Send + Sync + 'static,
875{
876    fn attach<T: 'static>(
877        self,
878        cx: &ViewCx<R, T>,
879        edit: fn(&Path<R, V>, V) -> Edit<R>,
880    ) -> Attached<V> {
881        let id = cx.path_control_id(&self);
882        let state = cx.value_at(&self);
883        let binding = cx.actions().edit_binding(self, edit).map(Box::new);
884        Attached::Record { id, state, binding }
885    }
886}
887
888impl<R, P, V> Bindable<R, V, FormField> for Path<P, V>
889where
890    R: Resource,
891    P: Params<Resource = R>,
892    V: serde::Serialize + Default + Send + Sync + 'static,
893{
894    fn attach<T: 'static>(
895        self,
896        cx: &ViewCx<R, T>,
897        _edit: fn(&Path<R, V>, V) -> Edit<R>,
898    ) -> Attached<V> {
899        let state = cx.form::<P>().field(&self);
900        let id = std::iter::once(P::ACTION.id.to_owned())
901            .chain(self.segments().iter().map(ToString::to_string))
902            .collect::<Vec<_>>()
903            .join("-");
904        Attached::Local { id, state }
905    }
906}
907
908type StatusSource = Rc<RefCell<Option<Box<dyn Fn() -> Vec<EffectStatus>>>>>;
909
910/// A registered effect handler ([`ViewCx::effects`]).
911pub struct EffectsHandle {
912    status: StatusSource,
913}
914
915impl EffectsHandle {
916    /// Publish the effects as `status` says they are whenever the resource
917    /// is published: unavailable while offline, relabelled while recording.
918    /// Read the view's [`LocalState`]s in it.
919    pub fn status(self, status: impl Fn() -> Vec<EffectStatus> + 'static) {
920        *self.status.borrow_mut() = Some(Box::new(status));
921    }
922}