Skip to main content

tilo_app/framework/
settings.rs

1//! An app's typed settings, files and secrets as views and surfaces reach
2//! them: from their context, scoped to the app and the settings document its
3//! host gave it.
4
5use std::any::{Any, TypeId};
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::rc::{Rc, Weak};
9
10use ui_core::LocalState;
11use ui_core::storage::{AppStorage, Settings, SettingsRevision, SettingsSchema};
12
13use super::record::{Held, RecordDoor};
14use super::section::{SectionDoor, SectionKind};
15
16/// One typed settings value, kept current while the view lives: a write
17/// from this window, another window, the Settings app or another of the
18/// person's devices (for [`Scope::Personal`](ui_core::storage::Scope))
19/// reaches [`Self::state`] on the next frame.
20///
21/// Bind controls to [`Self::state`] and write with [`Self::set`]. Settings
22/// are not the document: they are not undone and never seen by
23/// collaborators.
24///
25/// A section that is a resource model (`#[derive(Model)]` and an empty
26/// `#[resource]`) is also a record its views edit by path
27/// ([`ViewCx::record`](super::ViewCx::record)): its field edits and verbs
28/// are published as `settings:<kind>`, each change is checked by the
29/// model's invariants and recorded in the settings document with what made
30/// it and who.
31pub struct Setting<T> {
32    settings: Settings<T>,
33    value: LocalState<T>,
34}
35
36impl<T> Clone for Setting<T> {
37    fn clone(&self) -> Self {
38        Self {
39            settings: self.settings.clone(),
40            value: self.value.clone(),
41        }
42    }
43}
44
45impl<T: SettingsSchema + Clone + PartialEq + Send + Sync> Setting<T> {
46    /// The current value.
47    #[must_use]
48    pub fn get(&self) -> T {
49        self.value.with(Clone::clone)
50    }
51
52    /// The value as state that follows every write.
53    #[must_use]
54    pub fn state(&self) -> LocalState<T> {
55        self.value.clone()
56    }
57
58    /// Store `value`.
59    ///
60    /// # Errors
61    ///
62    /// When the document refuses it (see [`Settings::set`]); the value shown
63    /// stays the stored one.
64    pub fn set(&self, value: T) -> std::io::Result<()> {
65        self.settings.set(&value)?;
66        self.value.set(value);
67        Ok(())
68    }
69
70    /// Change the stored value with `change`.
71    ///
72    /// # Errors
73    ///
74    /// As [`Self::set`].
75    pub fn update(&self, change: impl FnOnce(&mut T)) -> std::io::Result<()> {
76        let value = self.settings.update(change)?;
77        self.value.set(value);
78        Ok(())
79    }
80
81    /// The underlying settings, for code that runs off the view (a job).
82    #[must_use]
83    pub fn settings(&self) -> &Settings<T> {
84        &self.settings
85    }
86}
87
88type Refresh = Box<dyn FnMut()>;
89
90impl<T> super::record::sealed::Hold<T> for Setting<T>
91where
92    T: tilo_resource::Resource<State = T>
93        + tilo_resource::Fields
94        + SettingsSchema
95        + Clone
96        + PartialEq
97        + Send
98        + Sync,
99{
100    fn held(&self, storage: super::record::sealed::Storage<'_>) -> Option<Rc<Held<T>>> {
101        storage.0.section_held(&self.settings)
102    }
103
104    fn door(
105        &self,
106        storage: super::record::sealed::Storage<'_>,
107    ) -> Option<super::record::sealed::Door> {
108        Some(super::record::sealed::Door(
109            storage.0.section(&self.settings) as Rc<dyn RecordDoor>,
110        ))
111    }
112}
113
114/// The app's storage and the settings its views follow. Shared by every
115/// scope of one root; the root refreshes it each sync.
116pub(crate) struct ViewStorage {
117    storage: AppStorage,
118    followed: RefCell<Vec<(Weak<()>, Refresh)>>,
119    /// The person's own records, when the host keeps them: not in a view of
120    /// a past version, which shows the record it is a version of.
121    person: Option<super::person::PersonState>,
122    /// This device, in a live window.
123    device: Option<String>,
124    /// The settings sections views and surfaces edit as records, by kind.
125    sections: RefCell<HashMap<&'static str, Rc<SectionDoor>>>,
126    /// Those a view part edits through its type, by type.
127    held_sections: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
128    /// Those sections, as the door reaches them.
129    section_doors: RefCell<Vec<Rc<dyn RecordDoor>>>,
130    /// The records another provider keeps, by id, with that provider
131    /// ([`Records::kept_by`](super::Records::kept_by)): views show them and
132    /// bind to them, the window neither publishes nor runs their actions.
133    kept_by: RefCell<HashMap<String, &'static str>>,
134    /// The store of the app's records in the workspace, for the records it
135    /// keeps beside its own: not in a view of a past version.
136    records: Option<super::chain::WorkspaceRecords>,
137    /// The chains the views opened ([`super::Chain`]).
138    chains: super::chain::Chains,
139    /// The records every app of the workspace opens, when the host keeps them:
140    /// not in a view of a past version.
141    workspace: Option<Rc<super::workspace::WorkspaceState>>,
142    /// The workspace's emoji, as the window's views draw them.
143    emoji: Option<Rc<super::workspace_emoji::WindowEmoji>>,
144}
145
146/// What the host gives a live window beside its record: the person's own
147/// records and this device's id. Neither reaches a view of a past version.
148#[derive(Default)]
149pub(crate) struct Hosted {
150    pub(crate) person: Option<ui_core::record::person::PersonRecords>,
151    pub(crate) device: Option<String>,
152    /// The store of the app's records in the workspace, who edits there and
153    /// what they may do.
154    pub(crate) records: Option<(
155        ui_core::record::RecordStore,
156        String,
157        ui_core::permissions::LivePermissions,
158    )>,
159    /// The records every app of the workspace opens, and the window's wake
160    /// scope, against which the workspace's emoji are registered.
161    pub(crate) workspace: Option<(
162        ui_core::record::workspace::WorkspaceRecords,
163        ui_core::wake::WakeHandle,
164    )>,
165}
166
167/// [`Hosted`], with the person's records opened for one app.
168pub(crate) struct Attached {
169    person: Option<super::person::PersonState>,
170    device: Option<String>,
171    records: Option<super::chain::WorkspaceRecords>,
172    workspace: Option<Rc<super::workspace::WorkspaceState>>,
173    emoji: Option<Rc<super::workspace_emoji::WindowEmoji>>,
174}
175
176impl Hosted {
177    pub(crate) fn attach(self, app_id: &'static str, app_title: &'static str) -> Attached {
178        let permissions = self
179            .records
180            .as_ref()
181            .map(|(_, _, permissions)| permissions.clone());
182        let workspace = self.workspace.map(|(records, wake)| {
183            let state = Rc::new(super::workspace::WorkspaceState::new(
184                records,
185                app_id,
186                app_title,
187                permissions,
188                Some(wake.clone()),
189            ));
190            let emoji = super::workspace_emoji::register(&wake, Rc::clone(&state));
191            (state, emoji)
192        });
193        let (workspace, emoji) = workspace.unzip();
194        Attached {
195            workspace,
196            emoji,
197            // The app's own records of a kind are kept with its others in
198            // the workspace it is open in.
199            person: self.person.map(|records| {
200                let own = self.records.as_ref().and_then(|(store, _, _)| {
201                    let (_, workspace) = store.package_and_home();
202                    records
203                        .in_workspace(&workspace)
204                        .map_err(|error| {
205                            tracing::warn!(%error, "the app's records of a kind are unavailable");
206                        })
207                        .ok()
208                });
209                super::person::PersonState::new(records, own, app_id, app_title)
210            }),
211            device: self.device,
212            records: self
213                .records
214                .map(|(store, me, permissions)| super::chain::WorkspaceRecords {
215                    store,
216                    me,
217                    permissions: Some(permissions),
218                    app_id,
219                    app_title,
220                }),
221        }
222    }
223}
224
225impl ViewStorage {
226    pub(crate) fn new(storage: AppStorage, hosted: Attached) -> Rc<Self> {
227        Rc::new(Self {
228            storage,
229            followed: RefCell::default(),
230            person: hosted.person,
231            device: hosted.device,
232            sections: RefCell::default(),
233            held_sections: RefCell::default(),
234            section_doors: RefCell::default(),
235            kept_by: RefCell::default(),
236            records: hosted.records,
237            chains: super::chain::Chains::default(),
238            workspace: hosted.workspace,
239            emoji: hosted.emoji,
240        })
241    }
242
243    /// The records every app of the window's workspace opens, for what
244    /// settles after the view that asked for one: `None` where the window
245    /// keeps no workspace records (a view of a past version).
246    pub(crate) fn workspace_handle(&self) -> Option<super::WorkspaceHandle> {
247        self.workspace
248            .as_ref()
249            .map(super::workspace::WorkspaceState::handle)
250    }
251
252    /// The chain of `P` the app keeps beside its record, opened on first
253    /// use, its actions reading the app's record from `base`. `None` where
254    /// the window keeps no records in a workspace (a view of a past version).
255    pub(crate) fn chain<P: super::Chained>(
256        &self,
257        base: impl Fn() -> Rc<<P::Base as tilo_resource::Resource>::State> + 'static,
258    ) -> Option<Rc<super::chain::ChainState<P>>> {
259        super::controls::opened_record::<P>();
260        self.chains.open::<P>(self.records.as_ref(), base)
261    }
262
263    /// The chain of `P` that lives beside the record `base` rather than
264    /// beside the app's own ([`super::Chained::chain_beside`]), opened on
265    /// first use, its actions reading that record from `base_state`.
266    pub(crate) fn chain_beside<P: super::Chained>(
267        &self,
268        base: &ui_core::record::RecordId,
269        base_state: impl Fn() -> Rc<<P::Base as tilo_resource::Resource>::State> + 'static,
270    ) -> Option<Rc<super::chain::ChainState<P>>> {
271        super::controls::opened_record::<P>();
272        self.chains
273            .open_beside::<P>(self.records.as_ref(), base, base_state)
274    }
275
276    /// The children of `C` the parent lists, held on first use, the parent
277    /// read from `parent`. `None` where the window keeps no records in a
278    /// workspace (a view of a past version).
279    pub(crate) fn children<C: super::children::ChildRecord>(
280        &self,
281        parent: impl Fn() -> Rc<<C::Parent as tilo_resource::Resource>::State> + 'static,
282    ) -> Option<Rc<super::children::ChildrenState<C>>> {
283        super::controls::opened_record::<C>();
284        self.chains
285            .open_children::<C>(self.records.as_ref(), parent)
286    }
287
288    /// Answer one published action of a chain the views opened, invoked by
289    /// someone the window knows as `me`.
290    pub(crate) fn chain_action(
291        &self,
292        me: Option<&str>,
293        payload: &[u8],
294        live: &mut ui_core::widget::Edits<'_>,
295    ) -> Result<tilo_actions::ActionOutput, String> {
296        self.chains.answer(me, payload, live)
297    }
298
299    pub(crate) fn storage(&self) -> &AppStorage {
300        &self.storage
301    }
302
303    /// The store of the app's records in the workspace; `None` in a view of
304    /// a past version.
305    pub(crate) fn workspace_store(&self) -> Option<&ui_core::record::RecordStore> {
306        self.records.as_ref().map(|records| &records.store)
307    }
308
309    /// This device's id, in a live window.
310    pub(crate) fn device(&self) -> Option<&str> {
311        self.device.as_deref()
312    }
313
314    /// The person's record of `P`, opened on first use.
315    pub(crate) fn person<P: tilo_resource::Resource>(
316        &self,
317    ) -> Option<super::person::PersonRecord<P>> {
318        super::controls::opened_record::<P>();
319        self.person.as_ref()?.open::<P>()
320    }
321
322    /// Open the record `P` every app of the workspace opens that this app
323    /// keeps.
324    pub(crate) fn workspace_record<P: tilo_resource::Resource>(&self) {
325        if let Some(workspace) = &self.workspace {
326            let _ = workspace.open::<P>(workspace.app_id());
327        }
328    }
329
330    /// The record `P` every app of the workspace opens that `owner` keeps,
331    /// opened on first use.
332    pub(crate) fn shared_workspace<P: tilo_resource::Resource>(
333        &self,
334        owner: &'static str,
335    ) -> Option<super::workspace::WorkspaceRecord<P>> {
336        self.workspace.as_ref()?.open::<P>(owner)
337    }
338
339    /// The record `id` that `owner` keeps, opened on first use.
340    pub(crate) fn shared_record<P: tilo_resource::Resource>(
341        &self,
342        owner: &'static str,
343        id: &str,
344    ) -> Option<super::workspace::WorkspaceRecord<P>> {
345        self.workspace.as_ref()?.open_at::<P>(owner, id)
346    }
347
348    /// The shared record of the person's `P`.
349    pub(crate) fn shared_person<P: tilo_resource::SharedRecord>(
350        &self,
351    ) -> Option<super::person::PersonRecord<P>> {
352        super::controls::opened_record::<P>();
353        self.person.as_ref()?.open_shared::<P>()
354    }
355
356    /// The records other than the app's own the views opened — the
357    /// person's records, settings sections — as published resources.
358    /// What [`Self::record_resources`] publishes depends on: each record's
359    /// id and revision, in publication order.
360    pub(crate) fn records_key(&self) -> Vec<(String, u64)> {
361        let mut doors = self
362            .person
363            .as_ref()
364            .map(super::person::PersonState::doors)
365            .unwrap_or_default();
366        doors.extend(self.section_doors.borrow().iter().cloned());
367        doors.extend(self.chains.doors());
368        doors
369            .iter()
370            .map(|door| (door.id().to_owned(), door.revision()))
371            .collect()
372    }
373
374    /// The person's records a view asked for and could not open, as they
375    /// are published: their declared actions, unavailable for why.
376    pub(crate) fn unavailable_records(&self) -> Vec<tilo_actions::ResourceSpec> {
377        self.person
378            .as_ref()
379            .map(super::person::PersonState::unavailable)
380            .unwrap_or_default()
381    }
382
383    pub(crate) fn record_resources(&self) -> Vec<tilo_actions::ResourceSpec> {
384        let mut resources = self
385            .person
386            .as_ref()
387            .map(super::person::PersonState::resources)
388            .unwrap_or_default();
389        resources.extend(
390            self.section_doors
391                .borrow()
392                .iter()
393                .map(|door| door.publish()),
394        );
395        resources.extend(self.chains.doors().iter().map(|door| door.publish()));
396        let kept_by = self.kept_by.borrow();
397        if !kept_by.is_empty() {
398            for resource in &mut resources {
399                if let Some(provider) = kept_by.get(resource.id.as_str()) {
400                    run_by(resource, provider);
401                }
402            }
403        }
404        resources
405    }
406
407    /// Mark the record `id` as kept by `provider`
408    /// ([`Records::kept_by`](super::Records::kept_by)).
409    pub(crate) fn keep_by(&self, id: &str, provider: &'static str) {
410        self.kept_by.borrow_mut().insert(id.to_owned(), provider);
411    }
412
413    /// The provider keeping the record `id`, when another one does.
414    pub(crate) fn kept_provider(&self, id: &str) -> Option<&'static str> {
415        self.kept_by.borrow().get(id).copied()
416    }
417
418    /// Answer one published action of those records, invoked by someone
419    /// the window knows as `me`.
420    pub(crate) fn record_action(
421        &self,
422        me: Option<&str>,
423        payload: &[u8],
424    ) -> Result<tilo_actions::ActionOutput, String> {
425        let mut doors = self
426            .person
427            .as_ref()
428            .map(super::person::PersonState::doors)
429            .unwrap_or_default();
430        doors.extend(self.section_doors.borrow().iter().cloned());
431        // Another provider runs what it keeps; the window never does.
432        let kept_by = self.kept_by.borrow();
433        doors.retain(|door| !kept_by.contains_key(door.id()));
434        let me = self
435            .person
436            .as_ref()
437            .map(super::person::PersonState::me)
438            .or(me);
439        super::record::answer(&doors, me, payload)
440    }
441
442    /// The settings section `T` held as a record, opened on first use: with
443    /// the undo and redo of the person's steps on it, unless the window
444    /// keeps nothing of theirs (a private one).
445    pub(crate) fn section<T>(&self, settings: &Settings<T>) -> Rc<SectionDoor>
446    where
447        T: tilo_resource::Resource<State = T> + tilo_resource::Fields + SettingsSchema,
448    {
449        super::controls::opened_section::<T>();
450        if let Some(open) = self.sections.borrow().get(T::KIND) {
451            return Rc::clone(open);
452        }
453        self.open_section(SectionKind::of(settings))
454    }
455
456    /// [`Self::section`] of the section `kind` describes.
457    fn open_section(&self, kind: Rc<SectionKind>) -> Rc<SectionDoor> {
458        let name = kind.kind;
459        let door = if let Some(person) = &self.person {
460            person.open_section(kind)
461        } else {
462            let door = Rc::new(SectionDoor::new(kind, String::new(), Vec::new()));
463            self.section_doors
464                .borrow_mut()
465                .push(Rc::clone(&door) as Rc<dyn RecordDoor>);
466            door
467        };
468        self.sections.borrow_mut().insert(name, Rc::clone(&door));
469        door
470    }
471
472    /// The settings section `T` as a record a view part edits through its
473    /// type ([`ViewCx::record`](super::ViewCx::record)).
474    pub(crate) fn section_held<T>(&self, settings: &Settings<T>) -> Option<Rc<Held<T>>>
475    where
476        T: tilo_resource::Resource<State = T> + tilo_resource::Fields + SettingsSchema,
477    {
478        if let Some(held) = self.held_sections.borrow().get(&TypeId::of::<T>()) {
479            return Rc::clone(held).downcast::<Held<T>>().ok();
480        }
481        let held = Rc::new(self.section(settings).held::<T>());
482        self.held_sections
483            .borrow_mut()
484            .insert(TypeId::of::<T>(), Rc::clone(&held) as Rc<dyn Any>);
485        Some(held)
486    }
487
488    /// Run `refresh` with every refresh while `alive` lives.
489    pub(crate) fn follow_with(&self, alive: Weak<()>, refresh: impl FnMut() + 'static) {
490        self.followed.borrow_mut().push((alive, Box::new(refresh)));
491    }
492
493    /// Answer an undo or redo of one of the person's records.
494    pub(crate) fn person_history(
495        &self,
496        payload: &[u8],
497    ) -> Result<tilo_actions::ActionOutput, String> {
498        self.person
499            .as_ref()
500            .ok_or_else(|| "this window keeps no records of yours".to_owned())?
501            .answer(payload)
502    }
503
504    /// The person's records the views opened, and the routes of the
505    /// records every app of the workspace opens.
506    pub(crate) fn person_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
507        let mut owners = self
508            .person
509            .as_ref()
510            .map(super::person::PersonState::owners)
511            .unwrap_or_default();
512        if let Some(workspace) = &self.workspace {
513            owners.extend(workspace.owners());
514        }
515        owners
516    }
517
518    /// `T`, followed for as long as `alive` is.
519    pub(crate) fn follow<T>(&self, alive: Weak<()>) -> Setting<T>
520    where
521        T: SettingsSchema + Clone + PartialEq + Send + Sync,
522    {
523        let settings = self.storage.settings::<T>();
524        let mut seen: SettingsRevision = settings.revision();
525        let value = LocalState::new(settings.get());
526        let setting = Setting {
527            settings: settings.clone(),
528            value: value.clone(),
529        };
530        self.followed.borrow_mut().push((
531            alive,
532            Box::new(move || {
533                let revision = settings.revision();
534                if revision == seen {
535                    return;
536                }
537                seen = revision;
538                let stored = settings.get();
539                if value.with(|shown| *shown != stored) {
540                    value.set(stored);
541                }
542            }),
543        ));
544        setting
545    }
546
547    /// Take up a newer head of each chain that arrived on this device: in
548    /// a frame's settle, where the store takes the writer opening it needs,
549    /// so the views' next sync reads it.
550    pub(crate) fn settle(&self) {
551        for chain in self.chains.doors() {
552            let _ = chain.revision();
553        }
554    }
555
556    /// Read every followed setting whose document changed, dropping those
557    /// of views that are gone.
558    pub(crate) fn refresh(&self) {
559        let _profile = tilo_resource::profiling::Phase::start("framework.view_storage_refresh");
560        if let Some(person) = &self.person {
561            person.refresh();
562        }
563        if let Some(workspace) = &self.workspace {
564            workspace.refresh();
565        }
566        if let Some(emoji) = &self.emoji {
567            emoji.settle();
568        }
569        for chain in self.chains.doors() {
570            let _ = chain.revision();
571        }
572        // Take the list so a refresh that builds a view (a switch reacting
573        // to the new value) may follow more without a reentrant borrow.
574        let mut current = std::mem::take(&mut *self.followed.borrow_mut());
575        current.retain_mut(|(alive, refresh)| {
576            if alive.strong_count() == 0 {
577                return false;
578            }
579            refresh();
580            true
581        });
582        let mut list = self.followed.borrow_mut();
583        current.append(&mut list);
584        *list = current;
585    }
586}
587
588/// `resource`'s actions as `provider` runs them, its parts' too.
589fn run_by(resource: &mut tilo_actions::ResourceSpec, provider: &'static str) {
590    resource.actions = std::mem::take(&mut resource.actions)
591        .into_iter()
592        .map(|spec| spec.run_by(provider))
593        .collect();
594    for child in &mut resource.children {
595        run_by(child, provider);
596    }
597}