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        storage
109            .0
110            .section(&self.settings)
111            .map(|door| super::record::sealed::Door(door as Rc<dyn RecordDoor>))
112    }
113}
114
115/// The app's storage and the settings its views follow. Shared by every
116/// scope of one root; the root refreshes it each sync.
117pub(crate) struct ViewStorage {
118    storage: AppStorage,
119    followed: RefCell<Vec<(Weak<()>, Refresh)>>,
120    /// The person's own records, when the host keeps them: not in a view of
121    /// a past version, which shows the record it is a version of.
122    person: Option<super::person::PersonState>,
123    /// This device, in a live window.
124    device: Option<String>,
125    /// The settings sections views and surfaces edit as records, by kind.
126    sections: RefCell<HashMap<&'static str, Rc<SectionDoor>>>,
127    /// Those a view part edits through its type, by type.
128    held_sections: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
129    /// Those sections, as the door reaches them.
130    section_doors: RefCell<Vec<Rc<dyn RecordDoor>>>,
131}
132
133/// What the host gives a live window beside its record: the person's own
134/// records and this device's id. Neither reaches a view of a past version.
135#[derive(Default)]
136pub(crate) struct Hosted {
137    pub(crate) person: Option<ui_core::record::person::PersonRecords>,
138    pub(crate) device: Option<String>,
139}
140
141/// [`Hosted`], with the person's records opened for one app.
142pub(crate) struct Attached {
143    person: Option<super::person::PersonState>,
144    device: Option<String>,
145}
146
147impl Hosted {
148    pub(crate) fn attach(self, app_id: &'static str, app_title: &'static str) -> Attached {
149        Attached {
150            person: self
151                .person
152                .map(|records| super::person::PersonState::new(records, app_id, app_title)),
153            device: self.device,
154        }
155    }
156}
157
158impl ViewStorage {
159    pub(crate) fn new(storage: AppStorage, hosted: Attached) -> Rc<Self> {
160        Rc::new(Self {
161            storage,
162            followed: RefCell::default(),
163            person: hosted.person,
164            device: hosted.device,
165            sections: RefCell::default(),
166            held_sections: RefCell::default(),
167            section_doors: RefCell::default(),
168        })
169    }
170
171    pub(crate) fn storage(&self) -> &AppStorage {
172        &self.storage
173    }
174
175    /// This device's id, in a live window.
176    pub(crate) fn device(&self) -> Option<&str> {
177        self.device.as_deref()
178    }
179
180    /// The person's record of `P`, opened on first use.
181    pub(crate) fn person<P: tilo_resource::Resource>(
182        &self,
183    ) -> Option<super::person::PersonRecord<P>> {
184        super::controls::opened::<P>();
185        self.person.as_ref()?.open::<P>()
186    }
187
188    /// The shared record of the person's `P`.
189    pub(crate) fn shared_person<P: tilo_resource::SharedRecord>(
190        &self,
191    ) -> Option<super::person::PersonRecord<P>> {
192        super::controls::opened::<P>();
193        self.person.as_ref()?.open_shared::<P>()
194    }
195
196    /// The records other than the app's own the views opened — the
197    /// person's records, settings sections — as published resources.
198    /// What [`Self::record_resources`] publishes depends on: each record's
199    /// id and revision, in publication order.
200    pub(crate) fn records_key(&self) -> Vec<(String, u64)> {
201        let mut doors = self
202            .person
203            .as_ref()
204            .map(super::person::PersonState::doors)
205            .unwrap_or_default();
206        doors.extend(self.section_doors.borrow().iter().cloned());
207        doors
208            .iter()
209            .map(|door| (door.id().to_owned(), door.revision()))
210            .collect()
211    }
212
213    pub(crate) fn record_resources(&self) -> Vec<tilo_actions::Resource> {
214        let mut resources = self
215            .person
216            .as_ref()
217            .map(super::person::PersonState::resources)
218            .unwrap_or_default();
219        resources.extend(
220            self.section_doors
221                .borrow()
222                .iter()
223                .map(|door| door.publish()),
224        );
225        resources
226    }
227
228    /// Answer one published action of those records, invoked by someone
229    /// the window knows as `me`.
230    pub(crate) fn record_action(
231        &self,
232        me: Option<&str>,
233        payload: &[u8],
234    ) -> Result<tilo_actions::ActionOutput, String> {
235        let mut doors = self
236            .person
237            .as_ref()
238            .map(super::person::PersonState::doors)
239            .unwrap_or_default();
240        doors.extend(self.section_doors.borrow().iter().cloned());
241        let me = self
242            .person
243            .as_ref()
244            .map(super::person::PersonState::me)
245            .or(me);
246        super::record::answer(&doors, me, payload)
247    }
248
249    /// The settings section `T` held as a record, opened on first use: a
250    /// record of the person's, whose edits they undo and redo, or in a
251    /// window that keeps nothing of theirs (a private one) one that lives as
252    /// long as the window.
253    pub(crate) fn section<T>(&self, settings: &Settings<T>) -> Option<Rc<SectionDoor>>
254    where
255        T: tilo_resource::Resource<State = T> + tilo_resource::Fields + SettingsSchema,
256    {
257        super::controls::opened::<T>();
258        if let Some(open) = self.sections.borrow().get(T::KIND) {
259            return Some(Rc::clone(open));
260        }
261        self.open_section(SectionKind::of(settings))
262    }
263
264    /// [`Self::section`] of the section `kind` describes.
265    fn open_section(&self, kind: Rc<SectionKind>) -> Option<Rc<SectionDoor>> {
266        let name = kind.kind;
267        let door = if let Some(person) = &self.person {
268            person.open_section(kind, self.device())?
269        } else {
270            let door = SectionDoor::window(kind, true)
271                .map_err(
272                    |error| tracing::warn!(kind = name, %error, "settings section unavailable"),
273                )
274                .ok()?;
275            let door = Rc::new(door);
276            self.section_doors
277                .borrow_mut()
278                .push(Rc::clone(&door) as Rc<dyn RecordDoor>);
279            door
280        };
281        self.sections.borrow_mut().insert(name, Rc::clone(&door));
282        Some(door)
283    }
284
285    /// The settings section `T` as a record a view part edits through its
286    /// type ([`ViewCx::record`](super::ViewCx::record)).
287    pub(crate) fn section_held<T>(&self, settings: &Settings<T>) -> Option<Rc<Held<T>>>
288    where
289        T: tilo_resource::Resource<State = T> + tilo_resource::Fields + SettingsSchema,
290    {
291        if let Some(held) = self.held_sections.borrow().get(&TypeId::of::<T>()) {
292            return Rc::clone(held).downcast::<Held<T>>().ok();
293        }
294        let held = self
295            .section(settings)?
296            .held::<T>()
297            .map_err(|error| tracing::warn!(kind = T::KIND, %error, "settings section unavailable"))
298            .ok()?;
299        let held = Rc::new(held);
300        self.held_sections
301            .borrow_mut()
302            .insert(TypeId::of::<T>(), Rc::clone(&held) as Rc<dyn Any>);
303        Some(held)
304    }
305
306    /// Run `refresh` with every refresh while `alive` lives.
307    pub(crate) fn follow_with(&self, alive: Weak<()>, refresh: impl FnMut() + 'static) {
308        self.followed.borrow_mut().push((alive, Box::new(refresh)));
309    }
310
311    /// Answer an undo or redo of one of the person's records.
312    pub(crate) fn person_history(
313        &self,
314        payload: &[u8],
315    ) -> Result<tilo_actions::ActionOutput, String> {
316        self.person
317            .as_ref()
318            .ok_or_else(|| "this window keeps no records of yours".to_owned())?
319            .answer(payload)
320    }
321
322    /// The routes of the person's records the views opened.
323    pub(crate) fn person_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
324        self.person
325            .as_ref()
326            .map(super::person::PersonState::owners)
327            .unwrap_or_default()
328    }
329
330    /// `T`, followed for as long as `alive` is.
331    pub(crate) fn follow<T>(&self, alive: Weak<()>) -> Setting<T>
332    where
333        T: SettingsSchema + Clone + PartialEq + Send + Sync,
334    {
335        let settings = self.storage.settings::<T>();
336        let mut seen: SettingsRevision = settings.revision();
337        let value = LocalState::new(settings.get());
338        let setting = Setting {
339            settings: settings.clone(),
340            value: value.clone(),
341        };
342        self.followed.borrow_mut().push((
343            alive,
344            Box::new(move || {
345                let revision = settings.revision();
346                if revision == seen {
347                    return;
348                }
349                seen = revision;
350                let stored = settings.get();
351                if value.with(|shown| *shown != stored) {
352                    value.set(stored);
353                }
354            }),
355        ));
356        setting
357    }
358
359    /// Read every followed setting whose document changed, dropping those
360    /// of views that are gone.
361    pub(crate) fn refresh(&self) {
362        let _profile = tilo_resource::profiling::Phase::start("framework.view_storage_refresh");
363        if let Some(person) = &self.person {
364            person.refresh();
365        }
366        // Take the list so a refresh that builds a view (a switch reacting
367        // to the new value) may follow more without a reentrant borrow.
368        let mut current = std::mem::take(&mut *self.followed.borrow_mut());
369        current.retain_mut(|(alive, refresh)| {
370            if alive.strong_count() == 0 {
371                return false;
372            }
373            refresh();
374            true
375        });
376        let mut list = self.followed.borrow_mut();
377        current.append(&mut list);
378        *list = current;
379    }
380}