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::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
34/// What builds a part with bindings of its own, retired with the `Rc` it
35/// is handed.
36type ScopedPart = dyn Fn(&Rc<()>) -> Element;
37
38/// A part showing an answer that landed, built again with its own
39/// bindings (retired with `alive`) for each newer one.
40fn arrived_part(build: Box<ScopedPart>) -> Element {
41    Switch::keyed(
42        LocalState::new(()),
43        Box::new(move |(), alive: &Rc<()>| build(alive)),
44    )
45}
46
47/// The window's view once the workspace's holdings arrived, the page they
48/// reserve until then.
49fn 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
68/// The edits views bound to their local state
69/// ([`ViewCx::bind_declarations`]), each tied to the view scope that bound
70/// it.
71pub(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    /// Every live view's bound edits over `state`.
85    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
92/// Mounts parts of a view while it runs ([`ViewCx::mounts`]): a child view
93/// a window builds once some work finished, with its own effects, rows and
94/// bindings, as a part of the view that handed out the handle. Clones
95/// mount into the same view.
96pub struct Mounts<R: Resource, T: 'static = <R as Resource>::State> {
97    /// The scope of the view that handed out the handle.
98    parent: Weak<()>,
99    scope: Rc<PartScope<R, T>>,
100}
101
102/// What builds a scope for a part retired with the `Rc` it is handed.
103type 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    /// The part `build` lays out, for the window to show where it likes.
116    /// What the part registers lasts as long as the element does.
117    ///
118    /// # Errors
119    /// Refuses once the view that handed out the handle was retired.
120    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
133/// Projects the resource state onto the value a view scope reads: the whole
134/// state at the root, one item inside an [`Each`] row.
135pub(crate) type Focus<R, T> = Rc<dyn for<'a> Fn(&'a <R as Resource>::State) -> Option<&'a T>>;
136
137/// Everything a view needs: reads of the resource state, bindings of controls
138/// to actions, local state, and custom surfaces.
139///
140/// `T` is what this scope reads — the resource state at the root, one item in
141/// a row built by [`ViewCx::each`].
142pub 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    /// The row of an [`Each`] this scope builds, whose readers run while it
147    /// is mounted; `None` outside a row.
148    pub(crate) gate: Option<super::reactive::Gate>,
149    pub(crate) action_bindings: Actions<R>,
150    pub(crate) item: Option<Id>,
151    /// Where this scope's value sits in the record: the root, or the item a
152    /// row stands for. `None` inside a row of a collection the view reads
153    /// in a way no path names.
154    pub(crate) path: Option<Vec<Segment>>,
155    pub(crate) controls: Rc<RefCell<HashMap<String, u32>>>,
156    /// The forms filled in in this scope ([`Self::form`]).
157    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    /// The scope of a freshly built root view: its own updaters, save status
172    /// and staging, none of which another root shares.
173    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/// The specs `specs` builds as callers see them now, at what `source`
211/// says of them, kept in `kept`. Not generic, so every app's views share
212/// it.
213#[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
227/// What a freshly built root view scope starts with that does not name its
228/// resource type: made here once rather than in every app's
229/// [`ViewCx::root`].
230struct 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/// `published` with the history verbs every app answers, so they are
254/// bindable like its own. Not generic, so it is compiled once rather than
255/// for every resource.
256#[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    /// The live resource state, for reads a binding does not cover.
269    pub fn live(&self) -> &Live<R> {
270        &self.live
271    }
272
273    /// Observe what this participant may do, without storing their role in
274    /// the shared resource. Use this with [`Self::switch`] for an owner-only
275    /// pane, or with a bound widget for a permission-specific label. Changes
276    /// follow the host even when the document and write access stay unchanged.
277    /// This controls presentation; the action door remains the authority.
278    pub fn permission_state(&self, need: tilo_actions::ActionNeed) -> LocalState<bool> {
279        self.dispatch.permission_state(need)
280    }
281
282    /// This participant's standing in the workspace as the host keeps it when
283    /// called (who they are, the workspace's policy, what bounds them), for a
284    /// runtime that decides its own invocations by the policy the door
285    /// decides by. `None` outside a host.
286    pub fn standing(&self) -> impl Fn() -> Option<ui_core::permissions::Standing> + 'static {
287        self.dispatch.standing_source()
288    }
289
290    /// Whether this participant may govern the workspace, observed: for an
291    /// owner-only pane with [`Self::switch`].
292    pub fn govern_state(&self) -> LocalState<bool> {
293        self.dispatch.govern_state()
294    }
295
296    /// Whether what the workspace holds has arrived, as state that follows
297    /// its sync: until it does, a record this window joined may still be
298    /// missing the content its creator wrote. Enable a choice made from that
299    /// content (a free seat, the next number) with it, so nobody acts on a
300    /// copy that is about to change under them. A view never writes on open
301    /// to make up for it: only the record's creator writes what it starts
302    /// with.
303    pub fn arrived_state(&self) -> LocalState<bool> {
304        self.dispatch.arrived_state()
305    }
306
307    /// Whether this participant may only view, as state that follows role
308    /// changes while the app runs.
309    pub fn read_only_state(&self) -> LocalState<bool> {
310        self.dispatch.read_only_state()
311    }
312
313    /// The item this scope stands for, inside an [`Each`] row.
314    pub fn item(&self) -> Option<&Id> {
315        self.item.as_ref()
316    }
317
318    /// The resource's last `last` versions, oldest first, kept current as
319    /// the document moves: an in-app history list of what was done and by
320    /// whom, each version addressable by the `undo`, `restore` and
321    /// `checkpoint` actions every app publishes.
322    pub fn history(&self, last: usize) -> LocalState<Vec<super::history::HistoryVersion>> {
323        self.feeds.follow(self.alive.clone(), last)
324    }
325
326    /// Read the scope's value now (at build time). For values that must
327    /// follow the resource, use a binding such as [`Self::text`].
328    pub fn read<V>(&self, read: impl FnOnce(Option<&T>) -> V) -> V {
329        self.live.with(|state| read((self.focus)(state)))
330    }
331
332    /// Typed process-local view state: never persisted, shared or undone.
333    pub fn local<V: Send + Sync + 'static>(&self, initial: V) -> LocalState<V> {
334        LocalState::new(initial)
335    }
336
337    /// Show the item a search hit opens: `open` runs with the hit's anchor
338    /// (a [`SearchEntry`](tilo_resource::SearchEntry)'s `anchor`) when the
339    /// person picks the hit, or when an agent invokes the resource's `open`
340    /// action. Select the item, switch to its page, scroll it into view:
341    /// set the [`LocalState`]s that show it, as the person's own input would.
342    ///
343    /// `open` may refuse an anchor it cannot show (a row that is gone) by
344    /// returning `Err`; the person or agent is told why.
345    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    /// Show the item of the resource a search hit or an agent's `open`
353    /// names: `open` runs with its id, and refuses one it cannot show (an
354    /// item that no longer exists), which the person or agent is told.
355    /// Anchors that name no item of the resource are refused for it.
356    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    /// `reference`, which `app` keeps in `record` (`None`: the app's own
371    /// record of the workspace), as a chip that opens it where it lives
372    /// ([`Dispatcher::open_reference`]): an issue reads `#12 Title`, a page
373    /// its name. A click, Enter or Space, and assistive technology's
374    /// activate all open it; a refusal is told like any refused action.
375    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    /// Open the platform search with `request`, narrowed to this app or one
388    /// part of it. Usually a control does this ([`Self::search_button`]); a
389    /// custom handler calls [`Dispatcher::open_search`] on its dispatcher.
390    pub fn open_search(&self, request: &ui_core::search::SearchRequest) {
391        self.dispatch.open_search(request);
392    }
393
394    /// The search launcher for this view's chrome: an icon button that opens
395    /// the platform search with `request`. Apps never draw a search field.
396    pub fn search_button(&self, request: ui_core::search::SearchRequest) -> Button {
397        ui_core::search::search_button(request)
398    }
399
400    /// Stage view state for a screenshot scenario: `stage` runs with the
401    /// scenario's name (`None` for a plain `--shots`) after the scenario's
402    /// actions applied and were laid out, and before the frame is captured.
403    /// Set the [`LocalState`]s a scenario shows — a selection, an open
404    /// dialog — the way the user's input would. It never runs in a live
405    /// window.
406    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    /// A stable, addressable control name: the action id, qualified by the
419    /// row's item, numbered only when the same binding repeats.
420    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    /// Keep `sink` in step with a value derived from the resource, read
436    /// again after every change.
437    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    /// Keep `sink` in step with `read` of the state, read again after the
446    /// changes `reads` names.
447    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    /// Text that always shows `read` of the resource.
471    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    /// A button that submits one fully specified action: a verb with its
478    /// arguments (`Snippets::NEW_SNIPPET.with(..)`), a field edit
479    /// (`p.favorite.set(true)`) or a form ([`Form::submit`]).
480    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    /// The form of the verb whose parameters are `P`
491    /// (`cx.form::<bookmarks::Add>()`): local values, never synced or
492    /// undone, that controls bind to with the parameters' paths
493    /// (`cx.input(form.paths().url)`) and a button submits as one
494    /// invocation (`cx.button("bookmarks-add", form.submit())`). The same
495    /// form is answered for `P` everywhere in this scope.
496    pub fn form<P: Params<Resource = R>>(&self) -> Form<P> {
497        self.forms.get::<P>()
498    }
499
500    /// A text field for the text at `field`: a path into the record, whose
501    /// committed edits replace it ([`Path::replace`]; the keystrokes of one
502    /// word are one undo step), or into a form ([`Self::form`]).
503    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    /// A checkbox for the flag at `field`, a path into the record or a form;
517    /// toggling sets it.
518    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    /// Any toolkit control for the value at `field`, a path into the record
539    /// or a form, setting it with each committed value:
540    /// `cx.control(Slider::from_state, p.volume)`.
541    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    /// Publish edits bound to this view's local state — the folder shown,
563    /// the row selected — on the resource beside its declared actions, as
564    /// typed builders of its action: `bind` runs whenever the published
565    /// tree is read, with the current resource state, and reads the local
566    /// state it captured. Invoking one dispatches what it builds through the
567    /// same permission check, confirmation and undo as any other action. The
568    /// binding lasts as long as this view scope (a row's ends with the row);
569    /// an id the resource already publishes keeps the resource's action
570    /// unless the bound one refines it ([`tilo_resource::bind_declaration`]).
571    pub fn bind_declarations(&self, bind: impl Fn(&R::State) -> Vec<ActionDecl<R>> + 'static) {
572        // The published tree reads what `bind` captured, mounted row or not.
573        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    /// The resource's published actions, for binding controls built outside
583    /// the view function: the rows a table, list or tree creates on demand.
584    pub fn actions(&self) -> Actions<R> {
585        self.action_bindings.clone()
586    }
587
588    /// The window's bar, its entries bound to the record's actions through
589    /// this view's dispatcher ([`super::WindowBar`]): the view declares what
590    /// the window shows and does, and the framework makes the slots of it.
591    #[must_use]
592    pub fn toolbar(&self) -> super::WindowBar<R> {
593        super::WindowBar::new(self.dispatcher())
594    }
595
596    /// A value that follows `read` of the resource, for any toolkit control
597    /// built from a [`LocalState`] (`Slider::from_state`, `Dropdown::from_state`).
598    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    /// A toolkit control bound to a verb: it shows `read` of the resource and
610    /// invokes `action` with each committed value, for a value that is a
611    /// verb's argument rather than a field
612    /// (`cx.control_with(TextInput::from_state, |p| p.shape(&node), Project::SET_SHAPE.map(..))`).
613    /// The control is disabled when the action is not published. A field of
614    /// the record is bound by its path instead ([`Self::input`],
615    /// [`Self::checkbox`], [`Self::control`]).
616    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    /// A part of the view chosen by view-local state — the detail pane of the
631    /// selected item, the page of a wizard. The part is built again, with its
632    /// own bindings, whenever `key` changes; the previous one is dropped.
633    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    /// A part of the view showing data that arrives later: a resource read
649    /// that is not here yet, a query of another app, a file read on a
650    /// worker ([`Arriving`]). `part` builds what shows once the data is
651    /// there, with its own bindings, again whenever a newer answer lands;
652    /// until then, and when the read fails, the framework shows the
653    /// placeholder and the failure ([`ArrivingView`]), with every timing.
654    /// Say what the content looks like with [`ArrivingView::shaped`].
655    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    /// [`Self::arriving`] for the window's workspace: its whole view, a
675    /// page, once the workspace's holdings arrived. Only `part`'s scope
676    /// depends on the app; the arrival around it is built once, in the
677    /// runtime.
678    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    /// What mounts parts of this view later, while it runs: a window that
691    /// shows a child window once work finished keeps it and builds the
692    /// child through it. See [`Mounts`].
693    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    /// What builds a part of this scope with its own bindings, retired
701    /// with the part (`alive`).
702    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            // Each build of the part names its controls afresh.
722            controls: Rc::default(),
723            forms: Forms::default(),
724            stages: stages.clone(),
725            feeds: feeds.clone(),
726            bindings: bindings.clone(),
727        }
728    }
729
730    /// Host a custom widget or renderer that takes part through the
731    /// [`ResourceSurface`] contract, answering its effect vocabulary `E`
732    /// (none unless it implements `ResourceSurface<R, E>`).
733    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    /// `content`, publishing on the resource what [`Publishes`](super::Publishes)
749    /// says beside what its bindings publish: the live rows it shows, search
750    /// entries for what the resource does not hold. It lasts as long as
751    /// `content` stands in the view.
752    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    /// `content`, kept up as [`Upkeep`](super::Upkeep) says: the work the
758    /// view takes in on every sync, the frames and wakes it owes the
759    /// window, what a capture waits for, what it asks of the host and the
760    /// presentation it keeps with the window. It lasts as long as `content`
761    /// stands in the view.
762    pub fn upkeep(&self, upkeep: super::Upkeep, content: Element) -> Element {
763        super::upkeep::upkeep(upkeep, content)
764    }
765
766    /// Answer the effect vocabulary `E` — work with no document delta that a
767    /// button, the palette, an agent or a script invokes — with `handle`,
768    /// and publish `E` on the resource while this part of the view lives.
769    ///
770    /// `handle` receives each invocation as a typed value. It finishes at
771    /// once ([`Effected::done`](super::Effected::done)) or starts a job
772    /// through the [`EffectCx`] (for an effect declared `job`); a job's
773    /// completion runs on the UI thread and changes the document through the
774    /// context's dispatcher. Handlers never run in History mode. Say which
775    /// effects are unavailable or relabelled right now with
776    /// [`EffectsHandle::status`].
777    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    /// Answer the effect vocabulary `E` on the window `W` the view laid
785    /// out, as [`Self::effects`] does: `handle` gets the window itself for
786    /// the call, so an effect answers at once from what the window shows
787    /// (the page, the caret). The window is found by its type in the view's
788    /// tree when the effect starts; an effect invoked while it is not open
789    /// is refused. What finishes later reaches state the window shares, or
790    /// the window itself with [`Self::window_host_effects`].
791    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    /// [`Self::window_effects`] for a window that hosts its work, as a
799    /// hosted surface does: the completions of the jobs its effects start
800    /// get the window `W` too (`EffectCx<R, W>`). Work whose window is not
801    /// in the view waits for it to show again; what is still running when
802    /// this part of the view goes is stopped.
803    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    /// Publish `E` while this part of the view lives, and `register` its
811    /// handler when the view runs live.
812    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        // The handler and the status the resource publishes read the scope's
818        // local state, mounted row or not.
819        if let Some(gate) = &self.gate {
820            gate.read_while_unmounted();
821        }
822        // Captures and History still describe the controls they draw. Only
823        // execution needs a live environment; publication is inert metadata.
824        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    /// Jobs the view starts on its own, with nobody invoking an effect: a
836    /// fetch when what it shows changes. See [`Jobs`](super::Jobs).
837    pub fn jobs(&self) -> super::Jobs<R> {
838        self.effects.view_jobs()
839    }
840
841    /// Jobs the window `W` the view laid out starts on its own, whose
842    /// completions get the window (`jobs.start::<W>(…)`), as a hosted
843    /// surface's get the surface. They finish before the view syncs, wait
844    /// while the window is not in the view, and stop once this part of the
845    /// view goes. See [`Jobs`](super::Jobs).
846    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    /// A button that invokes one effect, through the same door (and the
851    /// same confirmation, for a destructive one) as the palette.
852    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    /// The typed paths of this scope's value: the record's fields at the
861    /// root, the item's in a row of [`Self::each`]. Controls bind to them
862    /// (`row.checkbox("snippets-favorite", p.favorite)`) and edits are made
863    /// at them (`p.favorite.set(true)`).
864    ///
865    /// # Panics
866    /// Panics inside a row of a collection [`Self::each`] could not find
867    /// among the fields of the value it was given.
868    #[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    /// One row per item of a collection, kept in collection order:
882    /// `cx.each(|snippets| &snippets.snippets, |row| ...)`. A row is built
883    /// once per item and retained while the item exists, so focus and caret
884    /// inside it survive other items changing; rows are added, removed and
885    /// moved only when the collection's membership changes. In a row,
886    /// [`Self::paths`] are rooted at its item. A row's context menu
887    /// (right-click, the Menu key or Shift+F10) offers the actions the
888    /// resource publishes on its item, with the item already bound.
889    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
965/// How a scope reads the collection [`ViewCx::each`] shows.
966type ItemsOf<T, I> = Rc<dyn Fn(&T) -> &Collection<I>>;
967
968/// Where `target` sits among the fields of `value`, through nested records:
969/// the path of the collection a view's `each` reads.
970fn 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
1008/// Binding controls to the record's fields by path.
1009impl<R, T: 'static> ViewCx<R, T>
1010where
1011    R: Resource<State = R> + Model,
1012{
1013    /// Keep `sink` in step with the value at `path`, read again only after
1014    /// changes that touched it.
1015    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    /// A stable, addressable control name for a control bound to `path`:
1029    /// its fields and items joined, numbered only when the path repeats.
1030    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    /// The value at `path`, as local state that follows it: for any toolkit
1048    /// control built from a [`LocalState`], or a part of the view that shows
1049    /// it. It is read again only after changes that touched it.
1050    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    /// Route a serialized screen's text inputs over `ui_path` to the text
1062    /// field at `path` ([`UiValues::route`](ui_core::UiValues::route)): they
1063    /// show it and each edit replaces it.
1064    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    /// Route a serialized screen's checked controls over `ui_path` to the
1077    /// flag at `path`: they show it and each toggle sets it.
1078    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    /// A toolkit control whose value is not the field's own type: a slider
1086    /// (`f64`) for an optional hue (`Option<f64>`), a dropdown of names for
1087    /// a flag. It shows `show` of the value at `path` and sets the field to
1088    /// `store` of each committed value:
1089    /// `cx.control_map(Slider::from_state, p.hue, |hue| hue.unwrap_or(0.5), Some)`.
1090    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
1125/// What a control shows and changes: a field of the record, by its
1126/// [`Path`], or a field of a form ([`ViewCx::form`]), by the path into the
1127/// verb's parameters. `M` tells the two apart and is always inferred.
1128pub 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
1176/// A registered effect handler ([`ViewCx::effects`]).
1177pub struct EffectsHandle {
1178    status: StatusSource,
1179}
1180
1181impl EffectsHandle {
1182    /// Publish the effects as `status` says they are whenever the resource
1183    /// is published: unavailable while offline, relabelled while recording.
1184    /// Read the view's [`LocalState`]s in it.
1185    pub fn status(self, status: impl Fn() -> Vec<EffectStatus> + 'static) {
1186        *self.status.borrow_mut() = Some(Box::new(status));
1187    }
1188}