Skip to main content

egui_charts/services/
settings.rs

1//! The user's settings, kept in one document of the host's storage.
2
3use super::document::{Received, StoredDocument};
4use super::{ConflictChoice, Storage, StoreKind};
5use crate::config::DrawingToolType;
6use crate::model::{ReplaySession, Timeframe, Watchlist};
7use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use std::time::Duration;
11
12/// What the library keeps of the user between sessions, in the [`StoreKind::Settings`] document
13/// `settings` that [`SettingsStore`] reads and writes.
14///
15/// Build it with `Default` and assign fields: later releases add fields without a breaking change.
16#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(default)]
18#[non_exhaustive]
19pub struct UserSettings {
20    /// Name of the theme preset, as listed by [`presets`](crate::theme::presets).
21    pub theme: Option<String>,
22    /// The symbol shown last.
23    pub symbol: Option<String>,
24    /// The interval shown last.
25    pub interval: Option<Timeframe>,
26    /// The drawing tools on the drawing toolbar's favourites bar, in order. A tool this release
27    /// does not know is left out when reading.
28    #[serde(deserialize_with = "known_tools")]
29    pub drawing_favorites: Vec<DrawingToolType>,
30    /// Whether the user hid the favourites strip with its close or toggle button.
31    pub drawing_favorites_hidden: bool,
32    /// The top-left corner of the favourites strip, in logical points, where the user left it;
33    /// `None` until it has been shown, when it starts beside the drawing toolbar.
34    pub drawing_favorites_pos: Option<(f32, f32)>,
35    /// Whether the user dismissed the drawing toolbar's card that explains the magnet; a document
36    /// without it reads as not dismissed.
37    pub magnet_hint_dismissed: bool,
38    /// The layout open last, to open again on start.
39    pub layout: Option<String>,
40    /// Whether the open layout is saved after each change.
41    pub layout_autosave: bool,
42    /// The names of the starred layouts.
43    pub layout_favorites: Vec<String>,
44    /// Factory names of the starred studies of the indicator dialog, in order.
45    pub indicator_favorites: Vec<String>,
46    /// The intervals starred in the interval menu, in order.
47    pub interval_favorites: Vec<Timeframe>,
48    /// The properties each drawing tool starts with, keyed by tool, as the JSON of
49    /// its `DrawingTemplate`. A tool without an entry starts with
50    /// the built-in defaults.
51    pub drawing_defaults: BTreeMap<String, serde_json::Value>,
52    /// The watchlists, in order.
53    pub watchlists: Vec<Watchlist>,
54    /// The market heatmap's universe, size metric and colour metric.
55    pub market_heatmap: MarketHeatmapSelection,
56    /// The replay position to resume, with the interval its history was fetched at.
57    pub replay_session: Option<(ReplaySession, Timeframe)>,
58}
59
60/// What the market heatmap shows, by the identifiers the host gave its universes and metrics.
61/// `None` is the panel's default. An identifier the host no longer offers stays chosen, so the
62/// choice returns when the host offers it again.
63#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(default)]
65#[non_exhaustive]
66pub struct MarketHeatmapSelection {
67    /// The universe shown.
68    pub universe: Option<String>,
69    /// The metric that sizes the tiles.
70    pub size: Option<String>,
71    /// The metric that colours the tiles.
72    pub colour: Option<String>,
73}
74
75fn known_tools<'de, D: Deserializer<'de>>(
76    deserializer: D,
77) -> Result<Vec<DrawingToolType>, D::Error> {
78    Ok(Vec::<serde_json::Value>::deserialize(deserializer)?
79        .into_iter()
80        .filter_map(|tool| serde_json::from_value(tool).ok())
81        .collect())
82}
83
84#[derive(Serialize, Deserialize)]
85struct Document<T> {
86    version: u32,
87    #[serde(flatten)]
88    settings: T,
89}
90
91impl UserSettings {
92    /// The version of the document [`to_json`](Self::to_json) writes.
93    pub const VERSION: u32 = 1;
94
95    /// The settings as a versioned JSON document.
96    pub fn to_json(&self) -> String {
97        serde_json::to_string(&Document {
98            version: Self::VERSION,
99            settings: self,
100        })
101        .expect("settings always serialize")
102    }
103
104    /// Reads a document [`to_json`](Self::to_json) wrote, migrating older versions. Fails on
105    /// invalid JSON and on a version this release does not know. A missing field takes its
106    /// default.
107    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
108        let document: Document<serde_json::Value> = serde_json::from_str(json)?;
109        match document.version {
110            Self::VERSION => serde_json::from_value(document.settings),
111            version => Err(serde::de::Error::custom(format!(
112                "unsupported settings version {version}, expected {}",
113                Self::VERSION
114            ))),
115        }
116    }
117}
118
119/// Keeps [`UserSettings`] in the host's [`Storage`]: reads them once, then writes them a second
120/// after they last changed. A failed write keeps the settings in memory and is reported once by
121/// [`take_error`](Self::take_error); a later poll or explicit flush retries it. Settings that
122/// could not be read are never written over. If the host keeps revisions of documents (see
123/// [`Storage`]), a change by another client is read once nothing is unsaved and returned like the
124/// first read, and a write over a newer version is refused: [`conflict`](Self::conflict) then
125/// asks for the user's choice and [`resolve_conflict`](Self::resolve_conflict) applies it.
126pub struct SettingsStore {
127    storage: Arc<dyn Storage>,
128    document: StoredDocument,
129    /// The settings confirmed in storage; `None` until read.
130    written: Option<UserSettings>,
131    /// Settings that differ from `written`, with the input time they were first seen.
132    changed: Option<(UserSettings, f64)>,
133    /// Stored settings read since the last poll, for the host to apply.
134    restored: Option<UserSettings>,
135    error: Option<String>,
136}
137
138impl SettingsStore {
139    /// How long settings stay the same before they are written.
140    pub const DELAY: Duration = Duration::from_secs(1);
141
142    /// Starts reading the settings from `storage`.
143    pub fn new(storage: Arc<dyn Storage>) -> Self {
144        let mut document = StoredDocument::open(&*storage, StoreKind::Settings, "settings");
145        document.follow_changes(&*storage);
146        Self {
147            document,
148            storage,
149            written: None,
150            changed: None,
151            restored: None,
152            error: None,
153        }
154    }
155
156    /// Call every frame with the settings as they are. Returns the stored settings in the frame
157    /// they arrive, for the host to apply: once read, and again after a change by another client
158    /// or a conflict settled by reloading; until the first read nothing is written. Writes
159    /// `current` once it has stayed the same for [`DELAY`](Self::DELAY), and at once while a
160    /// conflict is unsettled, so that the choice applies to the newest settings.
161    pub fn poll(&mut self, ctx: &egui::Context, current: &UserSettings) -> Option<UserSettings> {
162        self.document.hold(
163            self.written
164                .as_ref()
165                .is_some_and(|written| written != current),
166        );
167        let received = self.document.poll(&*self.storage, ctx);
168        self.receive(received);
169        if let Some(settings) = self.restored.take() {
170            self.changed = None;
171            // Remote changes wait until the host has applied these.
172            ctx.request_repaint();
173            return Some(settings);
174        }
175        if self
176            .written
177            .as_ref()
178            .is_none_or(|written| written == current)
179        {
180            self.changed = None;
181            return None;
182        }
183        let now = ctx.input(|i| i.time);
184        let since = match &self.changed {
185            Some((settings, since)) if settings == current => *since,
186            _ => {
187                self.changed = Some((current.clone(), now));
188                now
189            }
190        };
191        let wait = Self::DELAY.as_secs_f64() - (now - since);
192        if wait > 0.0 && !self.document.conflicted() {
193            ctx.request_repaint_after(Duration::from_secs_f64(wait));
194        } else {
195            self.document.save(&*self.storage, current.to_json());
196        }
197        None
198    }
199
200    /// Writes `current` now if it differs from the stored settings, such as when the
201    /// application exits, and returns whether storage has acknowledged it. Nothing is written
202    /// before the settings are read, after they could not be read, while stored settings that
203    /// [`poll`](Self::poll) has not returned yet are pending, nor while a conflict is unsettled;
204    /// it returns `false` then, so a host that waits for `true` bounds the wait.
205    pub fn flush(&mut self, current: &UserSettings) -> bool {
206        if self.written.is_none() {
207            return false;
208        }
209        // `current` may hold changes made after the last poll.
210        self.document.hold(self.written.as_ref() != Some(current));
211        let received = self.document.receive(&*self.storage);
212        self.receive(received);
213        if self.restored.is_some() {
214            return false;
215        }
216        if self
217            .written
218            .as_ref()
219            .is_some_and(|written| written != current || self.document.saving())
220        {
221            self.changed = None;
222            self.document.save(&*self.storage, current.to_json());
223        }
224        let received = self.document.flush(&*self.storage);
225        self.receive(received);
226        self.written.as_ref() == Some(current) && !self.document.saving()
227    }
228
229    /// Takes in what the settings document received.
230    fn receive(&mut self, received: Vec<Received>) {
231        for received in received {
232            match received {
233                Received::Loaded(document) => {
234                    match document.as_deref().map(UserSettings::from_json).transpose() {
235                        Ok(settings) => {
236                            // Before the first read, a missing document leaves the host's own.
237                            if settings.is_some() || self.written.is_some() {
238                                self.restored = Some(settings.clone().unwrap_or_default());
239                            }
240                            self.written = Some(settings.unwrap_or_default());
241                        }
242                        Err(error) => {
243                            self.document.refuse_writes();
244                            self.error = Some(format!("Could not read settings: {error}"));
245                        }
246                    }
247                }
248                Received::Failed(error) => self.error = Some(error),
249                // Reported by `conflict`.
250                Received::Conflict => {}
251                Received::Saved(document) => {
252                    self.written =
253                        Some(UserSettings::from_json(&document).expect("saved settings"));
254                }
255            }
256        }
257    }
258
259    /// Whether the host refused to write the settings because another client changed them, and
260    /// the user's choice is awaited. The settings stay in memory and nothing is written until
261    /// [`resolve_conflict`](Self::resolve_conflict) settles it.
262    pub fn conflict(&self) -> bool {
263        self.document.conflicted() && !self.document.copying()
264    }
265
266    /// Settles a refused write with the user's choice. Reloading the stored settings returns them
267    /// from the next [`poll`](Self::poll), like the first read; keeping a local copy saves the
268    /// unsaved settings as a new document first, named `settings (local copy)`, or
269    /// `settings (local copy 2)` and so on if that name is taken.
270    pub fn resolve_conflict(&mut self, choice: ConflictChoice) {
271        self.document.resolve(&*self.storage, choice);
272    }
273
274    /// The error of a failed read or write since the last call, to show to the user.
275    pub fn take_error(&mut self) -> Option<String> {
276        self.error.take()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::data::Reply;
284    use crate::services::document::tests::Revised;
285    use std::sync::Mutex;
286
287    /// The default settings with the layout `name` open.
288    fn layout(name: &str) -> UserSettings {
289        UserSettings {
290            layout: Some(name.into()),
291            ..UserSettings::default()
292        }
293    }
294
295    /// Polls `store` at input time `time` with the host's settings `current`.
296    fn poll_at(
297        ctx: &egui::Context,
298        store: &mut SettingsStore,
299        time: f64,
300        current: &UserSettings,
301    ) -> Option<UserSettings> {
302        let input = egui::RawInput {
303            time: Some(time),
304            ..Default::default()
305        };
306        let mut restored = None;
307        ctx.run_ui(input, |ui| restored = store.poll(ui.ctx(), current))
308            .drop_without_applying_deltas();
309        restored
310    }
311
312    #[test]
313    fn remote_changes_are_returned_but_never_replace_unsaved_settings() {
314        // Whether this client's change is flushed before a poll has seen it.
315        for flush in [false, true] {
316            let ctx = egui::Context::default();
317            let host = Arc::new(Revised::default());
318            host.put("settings", &layout("base").to_json());
319            let mut store = SettingsStore::new(host.clone());
320            // Another client changes them before the first read is taken in.
321            host.put("settings", &layout("theirs").to_json());
322            assert_eq!(
323                poll_at(&ctx, &mut store, 0.0, &UserSettings::default()),
324                Some(layout("theirs"))
325            );
326            // And again while this client waits to write its own.
327            let mine = layout("mine");
328            if flush {
329                host.put("settings", &layout("newest").to_json());
330                store.flush(&mine);
331            } else {
332                assert_eq!(poll_at(&ctx, &mut store, 0.1, &mine), None);
333                host.put("settings", &layout("newest").to_json());
334            }
335            for time in [0.2, 1.2, 1.3] {
336                assert_eq!(poll_at(&ctx, &mut store, time, &mine), None, "{flush}");
337            }
338            assert!(store.conflict(), "{flush}");
339            let stored = UserSettings::from_json(&host.get("settings").unwrap()).unwrap();
340            assert_eq!(stored, layout("newest"), "{flush}");
341        }
342    }
343
344    #[test]
345    fn settings_read_before_the_next_poll_are_returned_and_not_written_over() {
346        // Whether another client deletes the settings rather than changing them, whether this
347        // client had unsaved settings and reloads, and whether it flushes before the next poll.
348        for (deleted, reload, flush) in [
349            (true, true, false),
350            (false, true, true),
351            (false, false, true),
352        ] {
353            let ctx = egui::Context::default();
354            let host = Arc::new(Revised::default());
355            host.put("settings", &layout("base").to_json());
356            let mut store = SettingsStore::new(host.clone());
357            poll_at(&ctx, &mut store, 0.0, &UserSettings::default());
358            let current = layout(if reload { "mine" } else { "base" });
359            if deleted {
360                host.remove("settings");
361            } else {
362                host.put("settings", &layout("theirs").to_json());
363            }
364            if reload {
365                for time in [0.1, 1.2, 1.3] {
366                    assert_eq!(poll_at(&ctx, &mut store, time, &current), None);
367                }
368                assert!(store.conflict());
369                store.resolve_conflict(ConflictChoice::ReloadRemote);
370            }
371            if flush {
372                store.flush(&current);
373            }
374            let theirs = if deleted {
375                UserSettings::default()
376            } else {
377                layout("theirs")
378            };
379            let case = (deleted, reload, flush);
380            assert_eq!(
381                poll_at(&ctx, &mut store, 1.4, &current),
382                Some(theirs.clone()),
383                "{case:?}"
384            );
385            assert_eq!(poll_at(&ctx, &mut store, 3.0, &theirs), None, "{case:?}");
386            let stored = host
387                .get("settings")
388                .map(|json| UserSettings::from_json(&json).unwrap());
389            assert_eq!(stored, (!deleted).then_some(theirs), "{case:?}");
390        }
391    }
392
393    #[derive(Default)]
394    struct Host(Mutex<Option<String>>);
395
396    #[derive(Default)]
397    struct DelayedHost(Mutex<Vec<(String, Reply<()>)>>);
398
399    impl Storage for DelayedHost {
400        fn list(&self, _: StoreKind, _: Reply<Vec<String>>) {
401            unreachable!()
402        }
403        fn load(&self, _: StoreKind, _: &str, reply: Reply<Option<String>>) {
404            reply.send(Ok(None));
405        }
406        fn save(&self, _: StoreKind, _: &str, document: String, reply: Reply<()>) {
407            self.0.lock().unwrap().push((document, reply));
408        }
409        fn delete(&self, _: StoreKind, _: &str, _: Reply<()>) {
410            unreachable!()
411        }
412    }
413
414    #[test]
415    fn failed_saves_remain_dirty_and_pending_edits_can_return_to_stored_settings() {
416        let host = Arc::new(DelayedHost::default());
417        let mut store = SettingsStore::new(host.clone());
418        let ctx = egui::Context::default();
419        let original = UserSettings::default();
420        store.poll(&ctx, &original);
421        let mut current = original.clone();
422        current.symbol = Some("A".into());
423        for attempt in 0..3 {
424            store.flush(&current);
425            store.flush(&current);
426            let (json, reply) = host
427                .0
428                .lock()
429                .unwrap()
430                .pop()
431                .expect("dirty settings must be retried");
432            assert_eq!(UserSettings::from_json(&json).unwrap(), current);
433            if attempt < 2 {
434                reply.send(Err(crate::data::FeedError::Network("temporary".into())));
435                store.poll(&ctx, &current);
436                assert_eq!(store.take_error().is_some(), attempt == 0);
437            } else {
438                reply.send(Ok(()));
439            }
440        }
441        assert!(store.flush(&current));
442        assert!(
443            host.0.lock().unwrap().is_empty(),
444            "confirmed settings need no write"
445        );
446        current.symbol = Some("B".into());
447        assert!(!store.flush(&current));
448        let (_, reply) = host.0.lock().unwrap().pop().unwrap();
449        current.symbol = Some("A".into());
450        store.flush(&current);
451        reply.send(Ok(()));
452        store.flush(&current);
453        let (json, reply) = host.0.lock().unwrap().pop().unwrap();
454        assert_eq!(UserSettings::from_json(&json).unwrap(), current);
455        reply.send(Ok(()));
456        assert!(store.flush(&current));
457        assert!(host.0.lock().unwrap().is_empty());
458    }
459
460    impl Storage for Host {
461        fn list(&self, _: StoreKind, reply: Reply<Vec<String>>) {
462            reply.send(Ok(Vec::new()));
463        }
464        fn load(&self, kind: StoreKind, name: &str, reply: Reply<Option<String>>) {
465            assert_eq!((kind, name), (StoreKind::Settings, "settings"));
466            reply.send(Ok(self.0.lock().unwrap().clone()));
467        }
468        fn save(&self, _: StoreKind, _: &str, document: String, reply: Reply<()>) {
469            *self.0.lock().unwrap() = Some(document);
470            reply.send(Ok(()));
471        }
472        fn delete(&self, _: StoreKind, _: &str, reply: Reply<()>) {
473            reply.send(Ok(()));
474        }
475    }
476
477    #[test]
478    fn settings_restore_once_wait_a_second_and_keep_unreadable_documents() {
479        let ctx = egui::Context::default();
480        let poll = |store: &mut SettingsStore, time: f64, current: &UserSettings| {
481            let input = egui::RawInput {
482                time: Some(time),
483                ..Default::default()
484            };
485            let mut restored = None;
486            ctx.run_ui(input, |ui| restored = store.poll(ui.ctx(), current))
487                .drop_without_applying_deltas();
488            restored
489        };
490        let host = Arc::new(Host::default());
491        let mut store = SettingsStore::new(host.clone());
492        let mut settings = UserSettings::default();
493        assert_eq!(poll(&mut store, 0.0, &settings), None);
494        settings.symbol = Some("AAPL".into());
495        settings.drawing_favorites = vec![DrawingToolType::TrendLine];
496        settings.market_heatmap.size = Some("cap".into());
497        settings.drawing_favorites_pos = Some((12.0, 34.0));
498        settings.magnet_hint_dismissed = true;
499        for time in [0.5, 1.0, 1.4] {
500            assert_eq!(poll(&mut store, time, &settings), None);
501        }
502        assert!(host.0.lock().unwrap().is_none());
503        poll(&mut store, 1.5, &settings);
504        let written = host.0.lock().unwrap().clone().unwrap();
505        assert!(written.contains("\"TrendLine\""));
506        assert_eq!(
507            UserSettings::from_json(&written)
508                .unwrap()
509                .market_heatmap
510                .size,
511            Some("cap".into())
512        );
513
514        // Exit can follow an inline save before another frame polls its acknowledgement.
515        settings.symbol = Some("MSFT".into());
516        store.flush(&settings);
517        let written = host.0.lock().unwrap().clone().unwrap();
518        assert_eq!(UserSettings::from_json(&written).unwrap(), settings);
519
520        // Settings saved before the magnet card existed restore unchanged, with the card not
521        // dismissed.
522        let older = written.replace(",\"magnet_hint_dismissed\":true", "");
523        assert_ne!(older, written);
524        let mut expected = settings.clone();
525        expected.magnet_hint_dismissed = false;
526        assert_eq!(UserSettings::from_json(&older).unwrap(), expected);
527
528        // A favourite tool from a newer release is left out; the rest is restored.
529        let document = written.replace("\"TrendLine\"", "\"TrendLine\",\"FutureTool\"");
530        *host.0.lock().unwrap() = Some(document);
531        let mut store = SettingsStore::new(host.clone());
532        assert_eq!(
533            poll(&mut store, 0.0, &UserSettings::default()),
534            Some(settings.clone())
535        );
536
537        let newer = written.replacen("\"version\":1", "\"version\":2", 1);
538        *host.0.lock().unwrap() = Some(newer.clone());
539        let mut store = SettingsStore::new(host.clone());
540        assert_eq!(poll(&mut store, 0.0, &settings), None);
541        assert!(store.take_error().is_some());
542        poll(&mut store, 5.0, &UserSettings::default());
543        store.flush(&UserSettings::default());
544        assert_eq!(host.0.lock().unwrap().as_deref(), Some(newer.as_str()));
545    }
546}