Skip to main content

egui_charts/widget/
stored_drawings.rs

1//! Each symbol's drawings kept in the host's storage.
2
3use super::Chart;
4use crate::data::FeedError;
5use crate::data::inbox::Inbox;
6use crate::drawings::persistence::{DrawingStorage, LoadError, StorageError, StoredPane};
7use crate::localization as l10n;
8use crate::services::document::{Received, StoredDocument, copy_number};
9use crate::services::{ConflictChoice, Revision, Storage, StoreKind};
10use std::collections::{HashMap, VecDeque};
11use std::sync::Arc;
12
13/// Formats a message for the chart's last context, with English before its first frame.
14fn message(ctx: Option<&egui::Context>, english: &str, values: &[(&str, &str)]) -> String {
15    match ctx {
16        Some(ctx) => l10n::format_text(ctx, english, values),
17        None => l10n::substitute(english, values),
18    }
19}
20
21/// What a chart asked its storage about the kept copies of its symbols' drawings.
22#[derive(Default)]
23struct LocalCopies {
24    inbox: Arc<Inbox>,
25    /// The symbol listed last, with the names of its copies by copy number.
26    listed: Option<(String, Vec<String>)>,
27    /// The request whose answer is awaited.
28    sent: Option<CopyRequest>,
29    /// The requests that wait for it, in order.
30    queued: VecDeque<CopyRequest>,
31    /// A copy read to be loaded: its symbol, name and document.
32    read: Option<(String, String, String)>,
33}
34
35impl LocalCopies {
36    /// Drops `name` from the list if the copies of `symbol` are the ones listed.
37    fn forget(&mut self, symbol: &str, name: &str) {
38        if let Some((listed, names)) = &mut self.listed
39            && listed == symbol
40        {
41            names.retain(|listed| listed != name);
42        }
43    }
44}
45
46/// A request about the kept copies of a symbol's drawings.
47enum CopyRequest {
48    /// Lists the documents, keeping the copies of `symbol`'s drawings.
49    List { symbol: String },
50    /// Reads a copy, to load it or, with `discard`, to check it before deleting it.
51    Read {
52        symbol: String,
53        name: String,
54        discard: bool,
55    },
56    /// Deletes a copy found to hold `symbol`'s drawings.
57    Delete { symbol: String, name: String },
58}
59
60impl CopyRequest {
61    fn symbol(&self) -> &str {
62        match self {
63            Self::List { symbol } | Self::Read { symbol, .. } | Self::Delete { symbol, .. } => {
64                symbol
65            }
66        }
67    }
68}
69
70pub(super) struct DrawingStore {
71    // Only attaching storage links the drawing readers and writers into an application.
72    sync: fn(&mut Chart, Option<&egui::Context>),
73    save: fn(&mut Chart),
74    storage: Arc<dyn Storage>,
75    /// The active document and inactive documents with retained drawings or edits.
76    documents: HashMap<String, StoredDocument>,
77    /// Drawings read for a symbol that was no longer shown when they arrived.
78    loaded: HashMap<String, String>,
79    /// The chart's drawing edit count last captured in a document, which may still be dirty.
80    captured: u64,
81    /// Edits made before a read finishes. The flag distinguishes edits from a restored layout.
82    pending: HashMap<String, (DrawingStorage, bool)>,
83    error: Option<StorageError>,
84    /// The last drawing context, retained for storage operations between frames.
85    ctx: Option<egui::Context>,
86    /// Documents this chart wrote, or read again at a newer revision, since the charts of its
87    /// layout that keep drawings in the same storage were told; `None` for a host without
88    /// revisions.
89    changed: HashMap<String, Option<Revision>>,
90    /// Per symbol, the stored pane of each read drawing whose study this chart does not show, by
91    /// stored id with its creation time: shown in the price pane, it is written back in its own.
92    unshown_panes: HashMap<String, HashMap<String, (i64, StoredPane)>>,
93    /// The kept copies of the shown symbol's drawings and the requests about them.
94    copies: LocalCopies,
95}
96
97impl DrawingStore {
98    pub(super) fn forget(&mut self, symbol: &str) -> bool {
99        if self.pending.contains_key(symbol)
100            || self
101                .documents
102                .get(symbol)
103                .is_some_and(StoredDocument::saving)
104        {
105            return false;
106        }
107        self.documents.remove(symbol);
108        self.loaded.remove(symbol);
109        self.unshown_panes.remove(symbol);
110        self.documents.shrink_to_fit();
111        self.loaded.shrink_to_fit();
112        true
113    }
114
115    /// Sends the queued requests about kept copies one at a time, each once no copy of its
116    /// symbol is being written and no copy read earlier waits to be loaded, and takes the
117    /// answers that arrived.
118    fn receive_copies(&mut self) {
119        loop {
120            if self.copies.sent.is_none()
121                && self.copies.read.is_none()
122                && let Some(request) = self.copies.queued.front()
123                && !self
124                    .documents
125                    .get(request.symbol())
126                    .is_some_and(StoredDocument::copying)
127            {
128                let request = self.copies.queued.pop_front().expect("queued request");
129                let inbox = &self.copies.inbox;
130                match &request {
131                    CopyRequest::List { .. } => {
132                        self.storage.list(StoreKind::Drawings, inbox.reply(0));
133                    }
134                    CopyRequest::Read { name, .. } => {
135                        self.storage.load(StoreKind::Drawings, name, inbox.reply(0));
136                    }
137                    CopyRequest::Delete { name, .. } => {
138                        self.storage
139                            .delete(StoreKind::Drawings, name, inbox.reply(0));
140                    }
141                }
142                self.copies.sent = Some(request);
143            }
144            let answers = self.copies.inbox.drain().answers;
145            if answers.is_empty() {
146                break;
147            }
148            for (_, answer) in answers {
149                if let Some(request) = self.copies.sent.take() {
150                    self.copy_answered(request, answer);
151                }
152            }
153        }
154    }
155
156    /// Finishes the answer to `request`.
157    fn copy_answered(&mut self, request: CopyRequest, answer: Box<dyn std::any::Any + Send>) {
158        match request {
159            CopyRequest::List { symbol } => {
160                let answer = *answer
161                    .downcast::<Result<Vec<String>, FeedError>>()
162                    .expect("copy list");
163                let mut copies: Vec<_> = match answer {
164                    Ok(names) => names
165                        .into_iter()
166                        .filter_map(|name| Some((copy_number(&symbol, &name)?, name)))
167                        .collect(),
168                    Err(error) => {
169                        self.error = Some(StorageError::Storage(message(
170                            self.ctx.as_ref(),
171                            "Could not list the kept copies of {symbol} drawings: {error}",
172                            &[
173                                ("symbol", symbol.as_str()),
174                                ("error", error.to_string().as_str()),
175                            ],
176                        )));
177                        Vec::new()
178                    }
179                };
180                copies.sort_unstable();
181                let names = copies.into_iter().map(|(_, name)| name).collect();
182                self.copies.listed = Some((symbol, names));
183            }
184            CopyRequest::Read {
185                symbol,
186                name,
187                discard,
188            } => {
189                let answer = *answer
190                    .downcast::<Result<Option<String>, FeedError>>()
191                    .expect("copy read");
192                match answer {
193                    Ok(None) => {
194                        self.copies.forget(&symbol, &name);
195                        if !discard {
196                            self.error = Some(StorageError::Storage(message(
197                                self.ctx.as_ref(),
198                                "{name} no longer exists",
199                                &[("name", name.as_str())],
200                            )));
201                        }
202                    }
203                    Ok(Some(json)) if !discard => self.copies.read = Some((symbol, name, json)),
204                    Ok(Some(json)) if holds_drawings_of(&json, &symbol) => {
205                        self.copies
206                            .queued
207                            .push_front(CopyRequest::Delete { symbol, name });
208                    }
209                    Ok(Some(_)) => {
210                        self.error = Some(StorageError::Storage(message(
211                            self.ctx.as_ref(),
212                            "{name} was not discarded: it does not hold drawings of {symbol}",
213                            &[("name", name.as_str()), ("symbol", symbol.as_str())],
214                        )));
215                    }
216                    Err(error) if discard => {
217                        self.error = Some(StorageError::Storage(message(
218                            self.ctx.as_ref(),
219                            "Could not discard {name}: {error}",
220                            &[
221                                ("name", name.as_str()),
222                                ("error", error.to_string().as_str()),
223                            ],
224                        )));
225                    }
226                    Err(error) => {
227                        self.error = Some(StorageError::Storage(message(
228                            self.ctx.as_ref(),
229                            "Could not read {name}: {error}",
230                            &[
231                                ("name", name.as_str()),
232                                ("error", error.to_string().as_str()),
233                            ],
234                        )))
235                    }
236                }
237            }
238            CopyRequest::Delete { symbol, name } => {
239                let answer = *answer
240                    .downcast::<Result<(), FeedError>>()
241                    .expect("copy delete");
242                match answer {
243                    Ok(()) => self.copies.forget(&symbol, &name),
244                    Err(error) => {
245                        self.error = Some(StorageError::Storage(message(
246                            self.ctx.as_ref(),
247                            "Could not discard {name}: {error}",
248                            &[
249                                ("name", name.as_str()),
250                                ("error", error.to_string().as_str()),
251                            ],
252                        )))
253                    }
254                }
255            }
256        }
257    }
258}
259
260#[cfg(feature = "ui")]
261impl DrawingStore {
262    pub(super) fn ready(&self, symbol: &str) -> bool {
263        self.pending.get(symbol).is_some_and(|(_, merge)| !merge)
264            || self
265                .documents
266                .get(symbol)
267                .is_some_and(|document| !document.reading())
268    }
269}
270
271/// Starts reading the drawings of `symbol` and follows changes other clients make to them.
272fn open_drawings(storage: &dyn Storage, symbol: &str) -> StoredDocument {
273    let mut document = StoredDocument::open(storage, StoreKind::Drawings, symbol);
274    document.follow_changes(storage);
275    document
276}
277
278/// Whether the stored document `json` names `symbol` as the symbol of its drawings, in any
279/// version of the format.
280fn holds_drawings_of(json: &str, symbol: &str) -> bool {
281    serde_json::from_str::<serde_json::Value>(json).is_ok_and(|document| {
282        document.get("symbol").and_then(serde_json::Value::as_str) == Some(symbol)
283    })
284}
285
286impl Chart {
287    /// Keeps each symbol's drawings in `storage`, in the [`StoreKind::Drawings`] document named
288    /// by the symbol. The drawings of a symbol are read when it is first shown and, when a
289    /// document holds some, replace the chart's drawings and undo history; they are written after
290    /// every drawing edit made through chart history (every user edit, undo and redo) and when
291    /// the symbol changes. Failed writes retain the latest document and retry after one second
292    /// of input time; [`Self::flush_stored_drawings`] retries immediately.
293    /// Drawings that could not all be read are never written over.
294    ///
295    /// Charts that show one symbol keep its drawings in one document. A chart reads them again
296    /// after another chart of the same [`ChartLayout`](super::ChartLayout) wrote them to the same
297    /// `storage`, and after another client changed them if `storage` keeps revisions and reports
298    /// changes through [`Storage::watch`]. It does so once nothing of its own is unsaved and no
299    /// drawing is being drawn, dragged, measured or edited: the stored drawings replace its
300    /// saveable drawings, selection and undo history, each keeping its local id, and a document
301    /// deleted elsewhere leaves none. A write waits for such a read, and one that fails is tried
302    /// again a second of input time later. Drawing sync of a [`ChartLayout`](super::ChartLayout)
303    /// shows drawings of charts outside `storage` on this chart without saving them here. A
304    /// drawing whose study the chart does not show appears in the price pane and is written back
305    /// in its study's pane. If `storage` keeps revisions, a
306    /// write over drawings changed elsewhere since they were read is refused and waits, with the
307    /// chart's drawings unchanged, until [`Self::resolve_storage_conflict`] settles it; without
308    /// revisions, the later write replaces the earlier one, including edits on two charts within
309    /// the time one write takes.
310    ///
311    /// Per-drawing storage notifications are disabled and cleared. To consume them alongside
312    /// automatic storage, call `chart.drawings_mut().set_sync_enabled(true)` after attaching it.
313    pub fn set_storage(&mut self, storage: Arc<dyn Storage>) {
314        self.drawings.set_sync_enabled(false);
315        for (manager, _) in self.symbol_drawings.values_mut() {
316            manager.set_sync_enabled(false);
317        }
318        self.drawing_store = Some(DrawingStore {
319            sync: Self::poll_stored_drawings,
320            save: Self::write_stored_drawings,
321            storage,
322            documents: HashMap::new(),
323            loaded: HashMap::new(),
324            captured: self.history.drawing_edits,
325            pending: HashMap::new(),
326            error: None,
327            ctx: None,
328            changed: HashMap::new(),
329            unshown_panes: HashMap::new(),
330            copies: LocalCopies::default(),
331        });
332    }
333
334    /// Captures current drawing edits and retries failed writes for every symbol immediately.
335    /// Available replies are processed, but asynchronous storage is not awaited. Returns `true`
336    /// once all captured edits are acknowledged. Keep rendering or call again to finish delayed
337    /// reads and writes; inspect [`Self::take_storage_error`] for failures. A write refused
338    /// because the drawings changed elsewhere stays unacknowledged until
339    /// [`Self::resolve_storage_conflict`] settles it.
340    pub fn flush_stored_drawings(&mut self) -> bool {
341        if let Some(sync) = self.drawing_store.as_ref().map(|store| store.sync) {
342            sync(self, None);
343            // An inline load can submit a write whose acknowledgement is already available.
344            sync(self, None);
345        }
346        self.drawing_store.as_ref().is_none_or(|store| {
347            store.pending.is_empty()
348                && store.captured == self.history.drawing_edits
349                && store.documents.values().all(StoredDocument::flushed)
350        })
351    }
352
353    /// The error of a failed read, write or kept-copy operation since the last call, to show
354    /// to the user. A failed write keeps the drawings on the chart and is reported once. A failed read
355    /// of drawings changed elsewhere is reported once and tried again a second of input time
356    /// later until it succeeds. [`StorageError::Read`] tells a document from a newer release,
357    /// which is never written over, from damaged data.
358    pub fn take_storage_error(&mut self) -> Option<StorageError> {
359        self.drawing_store.as_mut()?.error.take()
360    }
361
362    /// The symbol whose drawings `storage` refused to write because another client changed them
363    /// after they were read: the shown symbol first, then the others by name. The chart keeps
364    /// its drawings and writes nothing of that symbol until [`Self::resolve_storage_conflict`]
365    /// settles it; ask the user, for example with `ui::dialogs::StorageConflictDialog`. `None`
366    /// while no refused write waits, including while a kept copy is being written.
367    pub fn storage_conflict(&self) -> Option<&str> {
368        let store = self.drawing_store.as_ref()?;
369        let waiting = |document: &StoredDocument| document.conflicted() && !document.copying();
370        if store.documents.get(&self.symbol).is_some_and(waiting) {
371            return Some(self.symbol.as_str());
372        }
373        store
374            .documents
375            .iter()
376            .filter(|(_, document)| waiting(document))
377            .map(|(name, _)| name.as_str())
378            .min()
379    }
380
381    /// Settles the refused write of `symbol`'s drawings that [`Self::storage_conflict`] reports
382    /// with the user's `choice`: read the stored drawings, keep the unsaved ones as the document
383    /// `<symbol> (local copy)` (or the next free numbered copy) and read the stored ones, or write
384    /// the unsaved ones over the stored ones. The drawings change on the next frame or
385    /// [`Self::flush_stored_drawings`]. Does nothing when no write of `symbol` waits for a choice.
386    /// [`Self::local_copies`] lists the copies kept, to load or discard.
387    pub fn resolve_storage_conflict(&mut self, symbol: &str, choice: ConflictChoice) {
388        if let Some(store) = &mut self.drawing_store
389            && let Some(document) = store.documents.get_mut(symbol)
390        {
391            document.resolve(&*store.storage, choice);
392        }
393    }
394
395    /// Asks storage for the copies of the shown symbol's drawings that
396    /// [`ConflictChoice::KeepLocalCopy`] kept: the [`StoreKind::Drawings`] documents
397    /// `<symbol> (local copy)`, `<symbol> (local copy 2)` and so on. [`Self::local_copies`] has
398    /// them once storage answers, on a later frame or [`Self::flush_stored_drawings`]. Requests
399    /// about copies are sent one at a time, each after any copy of its symbol being written.
400    /// Does nothing without storage or a symbol.
401    pub fn list_local_copies(&mut self) {
402        if let Some(store) = &mut self.drawing_store
403            && !self.symbol.is_empty()
404        {
405            let symbol = self.symbol.clone();
406            store.copies.queued.push_back(CopyRequest::List { symbol });
407        }
408    }
409
410    /// The kept copies of the shown symbol's drawings that the last [`Self::list_local_copies`]
411    /// found, by copy number; `None` until storage answered it for the shown symbol. A failed
412    /// listing, reported through [`Self::take_storage_error`], finds none, and a discarded copy
413    /// leaves the list once storage confirms.
414    pub fn local_copies(&self) -> Option<&[String]> {
415        let (symbol, names) = self.drawing_store.as_ref()?.copies.listed.as_ref()?;
416        (*symbol == self.symbol).then_some(names.as_slice())
417    }
418
419    /// Replaces the shown symbol's drawings with those of its kept copy `name`, one that
420    /// [`Self::local_copies`] lists, as one user action: undo restores the drawings it replaced,
421    /// and the result is written like any other edit. Drawings that are never saved stay, and
422    /// the copy stays stored. The copy is read first and loaded once the symbol's own drawings
423    /// are read and nothing is being drawn, dragged, measured or edited. A copy that no longer
424    /// exists or whose drawings cannot all be read changes nothing, and neither does one that
425    /// arrives after another symbol is shown; [`Self::take_storage_error`] reports each. Does
426    /// nothing for a name that `local_copies` does not list.
427    pub fn load_local_copy(&mut self, name: &str) {
428        self.request_local_copy(name, false);
429    }
430
431    /// Deletes the kept copy `name` of the shown symbol's drawings, one that
432    /// [`Self::local_copies`] lists, which then leaves that list once storage confirms. The copy
433    /// is read first and kept if it does not hold drawings of the shown symbol. Failures are
434    /// reported through [`Self::take_storage_error`]. Does nothing for a name that
435    /// `local_copies` does not list.
436    pub fn discard_local_copy(&mut self, name: &str) {
437        self.request_local_copy(name, true);
438    }
439
440    fn request_local_copy(&mut self, name: &str, discard: bool) {
441        if !self
442            .local_copies()
443            .is_some_and(|names| names.iter().any(|listed| listed == name))
444        {
445            return;
446        }
447        let symbol = self.symbol.clone();
448        let store = self.drawing_store.as_mut().expect("listed copies");
449        let name = name.to_owned();
450        store.copies.queued.push_back(CopyRequest::Read {
451            symbol,
452            name,
453            discard,
454        });
455    }
456
457    /// Handles the requests about kept copies, then loads a copy read for the shown symbol once
458    /// its drawings are read and nothing is being drawn or edited.
459    fn poll_local_copies(&mut self, ctx: Option<&egui::Context>, busy: bool) {
460        let Some(store) = &mut self.drawing_store else {
461            return;
462        };
463        if let Some(ctx) = ctx {
464            let repaint = ctx.clone();
465            store
466                .copies
467                .inbox
468                .set_waker(move || repaint.request_repaint());
469        }
470        store.receive_copies();
471        let ready = !busy
472            && store
473                .documents
474                .get(&self.symbol)
475                .is_some_and(|document| !document.reading());
476        let Some((symbol, name, json)) = store
477            .copies
478            .read
479            .take_if(|(symbol, ..)| *symbol != self.symbol || ready)
480        else {
481            return;
482        };
483        let loaded = if symbol == self.symbol {
484            self.apply_local_copy(&name, &json)
485        } else {
486            Err(StorageError::Storage(message(
487                store.ctx.as_ref(),
488                "{name} was not loaded because {symbol} is no longer shown",
489                &[("name", name.as_str()), ("symbol", symbol.as_str())],
490            )))
491        };
492        if let Err(error) = loaded {
493            self.drawing_store.as_mut().expect("drawing storage").error = Some(error);
494        }
495    }
496
497    /// Replaces the saveable drawings with those of the kept copy `name` read as `json`, as one
498    /// user action that the next write stores.
499    fn apply_local_copy(&mut self, name: &str, json: &str) -> Result<(), StorageError> {
500        let copy = DrawingStorage::from_json(json)
501            .and_then(|(copy, skipped)| {
502                if copy.symbol != self.symbol {
503                    Err(LoadError::OtherSymbol)
504                } else if !skipped.is_empty() {
505                    Err(LoadError::Skipped(skipped))
506                } else {
507                    Ok(copy)
508                }
509            })
510            .map_err(|error| StorageError::Read {
511                symbol: name.into(),
512                error,
513            })?;
514        // Shown in the price pane when this chart does not show their study, written back in it.
515        let mut unshown = HashMap::new();
516        let drawings: Vec<_> = copy
517            .drawings()
518            .iter()
519            .map(|record| {
520                let drawing = self.restored_drawing(record, true);
521                if drawing.pane.is_none()
522                    && let Some(pane) = &record.pane
523                {
524                    unshown.insert(record.id.clone(), (record.created_at, pane.clone()));
525                }
526                drawing
527            })
528            .collect();
529        if !unshown.is_empty() {
530            let store = self.drawing_store.as_mut().expect("drawing storage");
531            let panes = store.unshown_panes.entry(self.symbol.clone()).or_default();
532            panes.extend(unshown);
533        }
534        self.user_action(|chart| chart.drawings.replace_saved_drawings(drawings));
535        Ok(())
536    }
537
538    /// Whether this chart and `other` keep their drawings in the same storage.
539    pub(super) fn shares_stored_drawings(&self, other: &Chart) -> bool {
540        match (&self.drawing_store, &other.drawing_store) {
541            (Some(own), Some(other)) => Arc::ptr_eq(&own.storage, &other.storage),
542            _ => false,
543        }
544    }
545
546    /// Whether this chart keeps its drawings in a storage.
547    pub(super) fn keeps_stored_drawings(&self) -> bool {
548        self.drawing_store.is_some()
549    }
550
551    /// Tells each chart in `charts` which documents the others that keep drawings in the same
552    /// storage wrote or read again since the last call, so that it reads them again. Returns
553    /// whether any chart was told.
554    pub(super) fn share_stored_drawings(charts: &mut [&mut Chart]) -> bool {
555        let mut shared = false;
556        for at in 0..charts.len() {
557            let Some(store) = &mut charts[at].drawing_store else {
558                continue;
559            };
560            if store.changed.is_empty() {
561                continue;
562            }
563            let changed = std::mem::take(&mut store.changed);
564            let storage = Arc::clone(&store.storage);
565            for (other, chart) in charts.iter_mut().enumerate() {
566                let Some(store) = chart
567                    .drawing_store
568                    .as_mut()
569                    .filter(|store| other != at && Arc::ptr_eq(&store.storage, &storage))
570                else {
571                    continue;
572                };
573                for (name, revision) in &changed {
574                    if let Some(document) = store.documents.get_mut(name) {
575                        document.changed_elsewhere(revision.clone());
576                        shared = true;
577                    }
578                }
579            }
580        }
581        shared
582    }
583
584    /// Reads the shown symbol's drawings when it is first shown and saves edited drawings.
585    pub(super) fn sync_stored_drawings(&mut self, ctx: &egui::Context) {
586        if let Some(store) = &self.drawing_store {
587            (store.sync)(self, Some(ctx));
588        }
589    }
590
591    fn poll_stored_drawings(&mut self, ctx: Option<&egui::Context>) {
592        if let Some(ctx) = ctx
593            && let Some(store) = &mut self.drawing_store
594        {
595            store.ctx = Some(ctx.clone());
596        }
597        self.save_stored_drawings();
598        let symbol = self.symbol.clone();
599        // A change made elsewhere is read between gestures, so that it cannot cut one short.
600        let busy = ctx.is_some_and(|ctx| ctx.input(|input| input.pointer.any_down()))
601            || self.drawings.curr_drawing.is_some()
602            || self.drawings.measurement.is_some()
603            || self.drawings.text_editor.is_some();
604        let Some(store) = &mut self.drawing_store else {
605            return;
606        };
607        if !symbol.is_empty() && !store.documents.contains_key(&symbol) {
608            let document = open_drawings(&*store.storage, &symbol);
609            store.documents.insert(symbol.clone(), document);
610        }
611        for (name, document) in &mut store.documents {
612            document.hold(busy && name == &symbol);
613            let mut first = document.reading();
614            let received = match ctx {
615                Some(ctx) => document.poll(&*store.storage, ctx),
616                None => document.flush(&*store.storage),
617            };
618            for received in received {
619                match received {
620                    Received::Loaded(json) => {
621                        let later = !std::mem::replace(&mut first, false);
622                        if later && let Some(revision) = document.revision() {
623                            store.changed.insert(name.clone(), Some(revision.clone()));
624                        }
625                        // Read again and missing, the document was deleted: no drawings remain.
626                        let json = json.or_else(|| {
627                            later
628                                .then(|| DrawingStorage::new(name.clone()).to_json().ok())
629                                .flatten()
630                        });
631                        let read = json.as_deref().map(DrawingStorage::from_json).transpose();
632                        match read {
633                            Ok(Some((drawings, _))) if drawings.symbol != *name => {
634                                document.refuse_writes();
635                                store.error = Some(StorageError::Read {
636                                    symbol: name.clone(),
637                                    error: LoadError::OtherSymbol,
638                                });
639                            }
640                            Ok(Some((_, skipped))) if !skipped.is_empty() => {
641                                document.refuse_writes();
642                                store.error = Some(StorageError::Read {
643                                    symbol: name.clone(),
644                                    error: LoadError::Skipped(skipped),
645                                });
646                            }
647                            Err(error) => {
648                                document.refuse_writes();
649                                store.error = Some(StorageError::Read {
650                                    symbol: name.clone(),
651                                    error,
652                                });
653                            }
654                            Ok(loaded) => {
655                                if let Some((mut pending, merge)) = store.pending.remove(name) {
656                                    let merged = merge
657                                        && loaded.as_ref().is_some_and(|(drawings, _)| {
658                                            !drawings.drawings().is_empty()
659                                        });
660                                    if merge && let Some((mut drawings, _)) = loaded {
661                                        for mut drawing in pending.drawings().iter().cloned() {
662                                            // Edits before the first read are new drawings, even if ids collide.
663                                            while drawings.get_drawing(&drawing.id).is_some() {
664                                                drawing.id.push_str("-local");
665                                            }
666                                            drawings.add_drawing(drawing);
667                                        }
668                                        pending = drawings;
669                                    }
670                                    match pending.to_json() {
671                                        Ok(json) => {
672                                            document.save(&*store.storage, json.clone());
673                                            // Only a real merge needs to replace local drawings and history.
674                                            if merged {
675                                                store.loaded.insert(name.clone(), json);
676                                            }
677                                        }
678                                        Err(error) => {
679                                            store.error = Some(StorageError::Save {
680                                                symbol: name.clone(),
681                                                error,
682                                            })
683                                        }
684                                    }
685                                } else if let Some(json) = json
686                                    && (name == &symbol
687                                        || self.symbol_drawings.contains_key(name)
688                                        || loaded.is_some_and(|(drawings, _)| {
689                                            !drawings.drawings().is_empty()
690                                        }))
691                                {
692                                    store.loaded.insert(name.clone(), json);
693                                }
694                            }
695                        }
696                    }
697                    Received::Failed(error) => store.error = Some(StorageError::Storage(error)),
698                    // `storage_conflict` reports it until `resolve_storage_conflict` settles it.
699                    Received::Conflict => {}
700                    Received::Saved(_) => {
701                        store
702                            .changed
703                            .insert(name.clone(), document.revision().cloned());
704                    }
705                }
706            }
707        }
708        store.documents.retain(|name, document| {
709            name == &symbol
710                || self.symbol_drawings.contains_key(name)
711                || store.loaded.contains_key(name)
712                || store.pending.contains_key(name)
713                || document.saving()
714        });
715        if let Some(json) = store.loaded.remove(&symbol) {
716            self.restore_stored_drawings(&json);
717        }
718        self.poll_local_copies(ctx, busy);
719        self.save_stored_drawings();
720    }
721
722    /// Replaces the drawings with the stored ones in `json`.
723    fn restore_stored_drawings(&mut self, json: &str) {
724        let previous = self.drawings.before_read();
725        let loaded = self.load_drawings_remapped(json, true);
726        if loaded.is_ok() {
727            self.drawings.keep_across_read(previous);
728        }
729        let refused = match loaded {
730            Ok(skipped) if skipped.is_empty() => None,
731            Ok(skipped) => Some(LoadError::Skipped(skipped)),
732            Err(error) => Some(error),
733        };
734        // Drawings whose study this chart does not show, shown in the price pane.
735        let unshown: HashMap<_, _> = DrawingStorage::from_json(json)
736            .map(|(stored, _)| {
737                self.drawings
738                    .drawings
739                    .iter()
740                    .filter(|drawing| drawing.pane.is_none())
741                    .filter_map(|drawing| {
742                        let id = drawing.stored_id.as_ref()?;
743                        let record = stored.get_drawing(id)?;
744                        Some((id.clone(), (record.created_at, record.pane.clone()?)))
745                    })
746                    .collect()
747            })
748            .unwrap_or_default();
749        let store = self.drawing_store.as_mut().expect("drawing storage");
750        if unshown.is_empty() {
751            store.unshown_panes.remove(&self.symbol);
752        } else {
753            store.unshown_panes.insert(self.symbol.clone(), unshown);
754        }
755        if let Some(error) = refused {
756            if let Some(document) = store.documents.get_mut(&self.symbol) {
757                document.refuse_writes();
758            }
759            store.error = Some(StorageError::Read {
760                symbol: self.symbol.clone(),
761                error,
762            });
763        }
764    }
765
766    /// Writes the shown symbol's drawings if they changed since they were last written.
767    pub(super) fn save_stored_drawings(&mut self) {
768        if let Some(store) = &self.drawing_store {
769            (store.save)(self);
770        }
771    }
772
773    fn write_stored_drawings(&mut self) {
774        let edits = self.history.drawing_edits;
775        if self.symbol.is_empty()
776            || self
777                .drawing_store
778                .as_ref()
779                .is_none_or(|store| store.captured == edits)
780        {
781            return;
782        }
783        let mut drawings = self.save_drawings();
784        let store = self.drawing_store.as_mut().expect("drawing storage");
785        // A drawing whose study is not shown here stays in that study's pane for the charts
786        // that show it.
787        if let (Ok(drawings), Some(panes)) = (&mut drawings, store.unshown_panes.get(&self.symbol))
788        {
789            for record in &mut drawings.drawings {
790                if record.pane.is_none()
791                    && let Some((created_at, pane)) = panes.get(&record.id)
792                    && *created_at == record.created_at
793                {
794                    record.pane = Some(pane.clone());
795                }
796            }
797        }
798        let document = store
799            .documents
800            .entry(self.symbol.clone())
801            .or_insert_with(|| open_drawings(&*store.storage, &self.symbol));
802        match drawings {
803            Ok(drawings) if document.reading() => {
804                let merge = store
805                    .pending
806                    .get(&self.symbol)
807                    .is_none_or(|(_, merge)| *merge);
808                store.pending.insert(self.symbol.clone(), (drawings, merge));
809                store.captured = edits;
810            }
811            Ok(drawings) => match drawings.to_json() {
812                Ok(json) => {
813                    document.save(&*store.storage, json);
814                    store.captured = edits;
815                }
816                Err(error) => {
817                    store.error = Some(StorageError::Save {
818                        symbol: self.symbol.clone(),
819                        error,
820                    })
821                }
822            },
823            Err(error) => {
824                store.error = Some(StorageError::Save {
825                    symbol: self.symbol.clone(),
826                    error,
827                })
828            }
829        }
830    }
831
832    #[cfg(feature = "ui")]
833    pub(super) fn save_restored_drawings(&mut self) {
834        self.save_stored_drawings();
835        if let Some(store) = &mut self.drawing_store {
836            store.loaded.remove(&self.symbol);
837            if let Some((_, merge)) = store.pending.get_mut(&self.symbol) {
838                *merge = false;
839            }
840        }
841    }
842}
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847    use crate::data::{FeedError, Reply};
848    use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
849    use crate::model::{Bar, BarData};
850    use crate::widget::{ChartLayout, ChartLayoutMode};
851    use chrono::{TimeZone, Utc};
852    use egui::{Pos2, RawInput, Rect, vec2};
853    use std::sync::Mutex;
854    use std::sync::atomic::{AtomicBool, Ordering};
855
856    #[derive(Default)]
857    struct Host {
858        documents: Mutex<HashMap<String, String>>,
859        fail_writes: AtomicBool,
860        delay_loads: AtomicBool,
861        fail_loads: AtomicBool,
862        loads: Mutex<Vec<(String, Reply<Option<String>>)>>,
863        delay_writes: AtomicBool,
864        writes: Mutex<Vec<Reply<()>>>,
865    }
866
867    impl Storage for Host {
868        fn list(&self, _: StoreKind, reply: Reply<Vec<String>>) {
869            reply.send(Ok(Vec::new()));
870        }
871        fn load(&self, kind: StoreKind, name: &str, reply: Reply<Option<String>>) {
872            assert_eq!(kind, StoreKind::Drawings);
873            if self.fail_loads.load(Ordering::Relaxed) {
874                return reply.send(Err(FeedError::Network("offline".into())));
875            }
876            if self.delay_loads.load(Ordering::Relaxed) {
877                self.loads.lock().unwrap().push((name.into(), reply));
878            } else {
879                reply.send(Ok(self.documents.lock().unwrap().get(name).cloned()));
880            }
881        }
882        fn save(&self, _: StoreKind, name: &str, document: String, reply: Reply<()>) {
883            if self.fail_writes.load(Ordering::Relaxed) {
884                return reply.send(Err(FeedError::NotSupported));
885            }
886            self.documents.lock().unwrap().insert(name.into(), document);
887            if self.delay_writes.load(Ordering::Relaxed) {
888                self.writes.lock().unwrap().push(reply);
889            } else {
890                reply.send(Ok(()));
891            }
892        }
893        fn delete(&self, _: StoreKind, _: &str, reply: Reply<()>) {
894            reply.send(Ok(()));
895        }
896    }
897
898    #[test]
899    fn automatic_storage_does_not_fill_the_notification_queue() {
900        let host = Arc::new(Host::default());
901        let mut chart = Chart::new(BarData::from_bars(vec![Bar::new(
902            Utc.timestamp_opt(0, 0).unwrap(),
903            100.0,
904            101.0,
905            99.0,
906            100.0,
907            1.0,
908        )]));
909        chart.set_symbol("A");
910        chart.set_storage(host.clone());
911        let ctx = egui::Context::default();
912        chart.sync_stored_drawings(&ctx);
913        let mut line = Drawing::new(0, DrawingToolType::HorizontalLine);
914        line.chart_points = vec![ChartPoint::new(0.0, 100.0)];
915        let id = chart.user_action(|chart| chart.drawings_mut().add_drawing(line).unwrap());
916        for i in 0..10_000 {
917            chart.user_action(|chart| {
918                chart.drawings_mut().edit_drawing(id, |drawing| {
919                    drawing.chart_points[0].price = 100.0 + (i % 2) as f64;
920                })
921            });
922            assert!(chart.drawings().pending_sync.lock().unwrap().is_empty());
923            if i % 100 == 0 {
924                chart.sync_stored_drawings(&ctx);
925            }
926        }
927        assert!(chart.flush_stored_drawings());
928        let json = host.documents.lock().unwrap()["A"].clone();
929        assert_eq!(
930            DrawingStorage::from_json(&json).unwrap().0.drawings()[0].points[0].price,
931            101.0
932        );
933        for symbol in ["B", "A"] {
934            chart.set_symbol(symbol);
935            assert!(!chart.drawings.sync_enabled);
936            chart.sync_stored_drawings(&ctx);
937            assert!(
938                !chart.drawings.sync_enabled,
939                "loading keeps notifications disabled"
940            );
941        }
942        chart.drawings_mut().set_sync_enabled(true);
943        for symbol in ["B", "A"] {
944            chart.set_symbol(symbol);
945            assert!(chart.drawings.sync_enabled);
946            chart.sync_stored_drawings(&ctx);
947            assert!(
948                chart.drawings.sync_enabled,
949                "loading preserves the host's opt-in"
950            );
951        }
952    }
953
954    #[test]
955    fn symbol_drawings_come_back_and_storage_failures_lose_nothing() {
956        let ctx = egui::Context::default();
957        let frame = |chart: &mut Chart| {
958            ctx.run_ui(Default::default(), |ui| {
959                chart.show(ui);
960            })
961            .drop_without_applying_deltas();
962        };
963        let new_chart = |host: &Arc<Host>| {
964            let bars = (0..20)
965                .map(|i| {
966                    Bar::new(
967                        Utc.timestamp_opt(i * 60, 0).unwrap(),
968                        10.0,
969                        11.0,
970                        9.0,
971                        10.0,
972                        1.0,
973                    )
974                })
975                .collect();
976            let mut chart = Chart::new(BarData::from_bars(bars));
977            chart.set_symbol("A");
978            chart.set_storage(host.clone());
979            chart
980        };
981        let draw = |chart: &mut Chart| {
982            let mut drawing = Drawing::new(0, DrawingToolType::HorizontalLine);
983            drawing.chart_points = vec![ChartPoint::new(4.0, 10.5)];
984            drawing.completed = true;
985            chart.user_action(|chart| chart.drawings_mut().add_synced_drawing(drawing).unwrap());
986        };
987        let host = Arc::new(Host::default());
988        host.documents
989            .lock()
990            .unwrap()
991            .insert("B".into(), "{\"version\": 99}".into());
992        let mut scanner = new_chart(&host);
993        for i in 0..16 {
994            let symbol = format!("EMPTY-{i}");
995            if i % 2 == 0 {
996                host.documents.lock().unwrap().insert(
997                    symbol.clone(),
998                    DrawingStorage::new(&symbol).to_json().unwrap(),
999                );
1000            }
1001            scanner.set_symbol(symbol);
1002            frame(&mut scanner);
1003        }
1004        assert_eq!(
1005            scanner.drawing_store.as_ref().unwrap().documents.len(),
1006            1,
1007            "empty scans must retain only the active storage reader"
1008        );
1009        let mut chart = new_chart(&host);
1010        frame(&mut chart);
1011        draw(&mut chart);
1012        frame(&mut chart);
1013        assert!(host.documents.lock().unwrap().contains_key("A"));
1014
1015        let mut transient = Drawing::new(0, DrawingToolType::HorizontalLine);
1016        transient.chart_points = vec![ChartPoint::new(4.0, 10.75)];
1017        transient.saveable = false;
1018        chart.user_action(|chart| chart.drawings_mut().add_drawing(transient).unwrap());
1019        for replay in 0..3 {
1020            match replay {
1021                1 => assert!(chart.undo()),
1022                2 => assert!(chart.redo()),
1023                _ => {}
1024            }
1025            frame(&mut chart);
1026            let json = host.documents.lock().unwrap()["A"].clone();
1027            let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1028            assert!(skipped.is_empty());
1029            assert_eq!(saved.drawings().len(), 1, "transient drawing was saved");
1030        }
1031
1032        chart.set_symbol("away");
1033        assert!(chart.forget_symbol("A"));
1034        assert!(
1035            !chart
1036                .drawing_store
1037                .as_ref()
1038                .unwrap()
1039                .documents
1040                .contains_key("A")
1041        );
1042        assert!(host.documents.lock().unwrap().contains_key("A"));
1043        chart.set_symbol("A");
1044        frame(&mut chart);
1045        assert_eq!(
1046            chart.drawings().drawings.len(),
1047            1,
1048            "forgotten state reloads storage"
1049        );
1050
1051        // Empty state must keep its writer until both an edit and its deletion are acknowledged.
1052        chart.set_symbol("SAVING");
1053        frame(&mut chart);
1054        host.delay_writes.store(true, Ordering::Relaxed);
1055        draw(&mut chart);
1056        frame(&mut chart);
1057        chart.user_action(|chart| chart.drawings_mut().clear_all());
1058        chart.set_symbol("away");
1059        assert!(
1060            !chart.flush_stored_drawings(),
1061            "a delayed write is not acknowledged"
1062        );
1063        assert!(!chart.forget_symbol("SAVING"));
1064        for _ in 0..2 {
1065            for reply in std::mem::take(&mut *host.writes.lock().unwrap()) {
1066                reply.send(Ok(()));
1067            }
1068            frame(&mut chart);
1069        }
1070        assert!(chart.flush_stored_drawings());
1071        assert!(chart.forget_symbol("SAVING"));
1072        let (saved, skipped) =
1073            DrawingStorage::from_json(&host.documents.lock().unwrap()["SAVING"]).unwrap();
1074        assert!(saved.drawings().is_empty());
1075        assert!(skipped.is_empty());
1076        host.delay_writes.store(false, Ordering::Relaxed);
1077
1078        let mut restarted = new_chart(&host);
1079        frame(&mut restarted);
1080        let restored = &restarted.drawings().drawings;
1081        assert_eq!(restored.len(), 1);
1082        assert_eq!(restored[0].chart_points[0].bar_idx, 4.0);
1083        assert_eq!(restored[0].chart_points[0].price, 10.5);
1084
1085        // A document this release cannot read is reported and never written over.
1086        restarted.set_symbol("B");
1087        frame(&mut restarted);
1088        assert!(
1089            matches!(restarted.take_storage_error(), Some(StorageError::Read {
1090            symbol, error: LoadError::UnsupportedVersion { found: 99, .. },
1091        }) if symbol == "B")
1092        );
1093        draw(&mut restarted);
1094        frame(&mut restarted);
1095        assert_eq!(host.documents.lock().unwrap()["B"], "{\"version\": 99}");
1096
1097        // Failed writes keep the drawings and are reported once.
1098        host.fail_writes.store(true, Ordering::Relaxed);
1099        restarted.set_symbol("C");
1100        frame(&mut restarted);
1101        for _ in 0..2 {
1102            draw(&mut restarted);
1103            frame(&mut restarted);
1104            frame(&mut restarted);
1105        }
1106        assert!(matches!(
1107            restarted.take_storage_error(),
1108            Some(StorageError::Storage(_))
1109        ));
1110        draw(&mut restarted);
1111        frame(&mut restarted);
1112        frame(&mut restarted);
1113        assert!(restarted.take_storage_error().is_none());
1114        assert_eq!(restarted.drawings().drawings.len(), 3);
1115
1116        // Recovery without another edit must save the latest failed document.
1117        assert!(restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1118        host.fail_writes.store(false, Ordering::Relaxed);
1119        ctx.run_ui(
1120            egui::RawInput {
1121                time: Some(ctx.input(|input| input.time) + 2.0),
1122                ..Default::default()
1123            },
1124            |ui| {
1125                restarted.show(ui);
1126            },
1127        )
1128        .drop_without_applying_deltas();
1129        let json = host.documents.lock().unwrap()["C"].clone();
1130        let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1131        assert!(skipped.is_empty());
1132        assert_eq!(saved.drawings().len(), 3);
1133        assert!(!restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1134
1135        // An explicit flush retries immediately, even without another frame.
1136        host.fail_writes.store(true, Ordering::Relaxed);
1137        draw(&mut restarted);
1138        assert!(!restarted.flush_stored_drawings());
1139        host.fail_writes.store(false, Ordering::Relaxed);
1140        assert!(
1141            !restarted.flush_stored_drawings(),
1142            "unreadable B still prevents a complete flush"
1143        );
1144        assert!(!restarted.drawing_store.as_ref().unwrap().documents["C"].saving());
1145        let json = host.documents.lock().unwrap()["C"].clone();
1146        assert_eq!(
1147            DrawingStorage::from_json(&json).unwrap().0.drawings().len(),
1148            4
1149        );
1150
1151        // A late read must preserve edits, including after switching away from the symbol.
1152        host.fail_writes.store(false, Ordering::Relaxed);
1153        host.delay_loads.store(true, Ordering::Relaxed);
1154        let mut delayed = new_chart(&host);
1155        frame(&mut delayed);
1156        #[cfg(feature = "ui")]
1157        assert!(
1158            delayed.snapshot().is_err(),
1159            "a layout must not capture drawings before their read completes"
1160        );
1161        draw(&mut delayed);
1162        delayed.set_symbol("D");
1163        assert!(
1164            !delayed.forget_symbol("A"),
1165            "pending edits must finish loading"
1166        );
1167        frame(&mut delayed);
1168        for (symbol, reply) in std::mem::take(&mut *host.loads.lock().unwrap()) {
1169            reply.send(Ok(host.documents.lock().unwrap().get(&symbol).cloned()));
1170        }
1171        frame(&mut delayed);
1172        assert!(delayed.drawings().drawings.is_empty());
1173        let json = host.documents.lock().unwrap()["A"].clone();
1174        let (saved, skipped) = DrawingStorage::from_json(&json).unwrap();
1175        assert!(skipped.is_empty());
1176        assert_eq!(saved.drawings().len(), 2);
1177        delayed.set_symbol("A");
1178        frame(&mut delayed);
1179        assert_eq!(delayed.drawings().drawings.len(), 2);
1180        // A different history window still projects the same timestamps and prices.
1181        let bars = delayed.bars(super::super::SeriesId::MAIN)[2..].to_vec();
1182        delayed.set_data(super::super::SeriesId::MAIN, bars);
1183        for drawing in &delayed.drawings().drawings {
1184            assert_eq!(drawing.chart_points[0].bar_idx, 2.0);
1185            assert_eq!(drawing.chart_points[0].price, 10.5);
1186        }
1187    }
1188
1189    fn line_at(bar: f32, price: f64) -> Drawing {
1190        let mut line = Drawing::new(0, DrawingToolType::HorizontalLine);
1191        line.chart_points = vec![ChartPoint::new(bar, price)];
1192        line.completed = true;
1193        line
1194    }
1195
1196    fn draw_line(chart: &mut Chart, line: Drawing) -> usize {
1197        chart.user_action(|chart| chart.drawings_mut().add_drawing(line).unwrap())
1198    }
1199
1200    fn sorted(mut prices: Vec<f64>) -> Vec<f64> {
1201        prices.sort_by(f64::total_cmp);
1202        prices
1203    }
1204
1205    fn shown_prices(chart: &Chart) -> Vec<f64> {
1206        let drawings = &chart.drawings().drawings;
1207        sorted(drawings.iter().map(|d| d.chart_points[0].price).collect())
1208    }
1209
1210    /// A chart of symbol A on ten one-minute bars, keeping its drawings in `storage` if given.
1211    fn chart_on(storage: Option<Arc<dyn Storage>>) -> Chart {
1212        let bars = (0..10)
1213            .map(|i| {
1214                Bar::new(
1215                    Utc.timestamp_opt(i * 60, 0).unwrap(),
1216                    10.0,
1217                    11.0,
1218                    9.0,
1219                    10.0,
1220                    1.0,
1221                )
1222            })
1223            .collect();
1224        let mut chart = Chart::new(BarData::from_bars(bars));
1225        chart.set_symbol("A");
1226        if let Some(storage) = storage {
1227            chart.set_storage(storage);
1228        }
1229        chart
1230    }
1231
1232    /// Shows `charts` in `layout` for three frames over `seconds` of input time: a write, the
1233    /// read it causes, and a frame of rest.
1234    fn show_frames(
1235        ctx: &egui::Context,
1236        layout: &mut ChartLayout,
1237        charts: &mut [&mut Chart],
1238        seconds: f64,
1239    ) {
1240        for _ in 0..3 {
1241            let input = RawInput {
1242                screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(900.0, 600.0))),
1243                time: Some(ctx.input(|input| input.time) + seconds / 3.0),
1244                ..Default::default()
1245            };
1246            ctx.run_ui(input, |ui| {
1247                layout.show(ui, charts);
1248            })
1249            .drop_without_applying_deltas();
1250        }
1251    }
1252
1253    /// The prices of the drawings stored as A.
1254    fn stored_prices(host: &Host) -> Vec<f64> {
1255        prices_in(&host.documents.lock().unwrap()["A"])
1256    }
1257
1258    /// The prices of the drawings in a stored `document`.
1259    fn prices_in(document: &str) -> Vec<f64> {
1260        let (stored, _) = DrawingStorage::from_json(document).unwrap();
1261        sorted(
1262            stored
1263                .drawings()
1264                .iter()
1265                .map(|d| d.points[0].price)
1266                .collect(),
1267        )
1268    }
1269
1270    /// Guards #1463 and #1604: two clients on a storage that keeps revisions, where the later
1271    /// write is refused and the user's choice settles it, and the copy kept is offered to load
1272    /// onto the chart or to discard.
1273    #[test]
1274    fn a_refused_drawings_write_waits_for_the_users_choice() {
1275        use crate::services::document::tests::Revised;
1276        let stored = |host: &Revised, name: &str| {
1277            host.get(name).map(|json| {
1278                let (stored, _) = DrawingStorage::from_json(&json).unwrap();
1279                sorted(
1280                    stored
1281                        .drawings()
1282                        .iter()
1283                        .map(|d| d.points[0].price)
1284                        .collect(),
1285                )
1286            })
1287        };
1288        let new_chart = |host: &Arc<Revised>| {
1289            let bar = Bar::new(Utc.timestamp_opt(0, 0).unwrap(), 10.0, 11.0, 9.0, 10.0, 1.0);
1290            let mut chart = Chart::new(BarData::from_bars(vec![bar]));
1291            chart.set_symbol("A");
1292            chart.set_storage(host.clone());
1293            assert!(chart.flush_stored_drawings());
1294            chart
1295        };
1296        // The choice, whether the other client deletes the document instead of adding to it, the
1297        // prices then stored, those of the kept copy, and those the chart shows.
1298        for (choice, deleted, expected, copy, shown) in [
1299            (
1300                ConflictChoice::ReloadRemote,
1301                false,
1302                Some(vec![10.0, 10.5]),
1303                None,
1304                vec![10.0, 10.5],
1305            ),
1306            (
1307                ConflictChoice::KeepLocalCopy,
1308                false,
1309                Some(vec![10.0, 10.5]),
1310                Some(vec![10.0, 11.0]),
1311                vec![10.0, 10.5],
1312            ),
1313            (
1314                ConflictChoice::Retry,
1315                false,
1316                Some(vec![10.0, 11.0]),
1317                None,
1318                vec![10.0, 11.0],
1319            ),
1320            (ConflictChoice::ReloadRemote, true, None, None, vec![]),
1321        ] {
1322            let host = Arc::new(Revised::default());
1323            let mut theirs = new_chart(&host);
1324            draw_line(&mut theirs, line_at(0.0, 10.0));
1325            assert!(theirs.flush_stored_drawings());
1326            let mut mine = new_chart(&host);
1327            assert_eq!(shown_prices(&mine), [10.0]);
1328            draw_line(&mut mine, line_at(0.0, 11.0));
1329            if deleted {
1330                host.remove("A");
1331            } else {
1332                draw_line(&mut theirs, line_at(0.0, 10.5));
1333                assert!(theirs.flush_stored_drawings());
1334            }
1335            assert!(!mine.flush_stored_drawings(), "{choice:?}");
1336            assert_eq!(mine.storage_conflict(), Some("A"));
1337            assert_eq!(shown_prices(&mine), [10.0, 11.0], "kept until the choice");
1338            mine.resolve_storage_conflict("A", choice);
1339            assert!(mine.flush_stored_drawings(), "{choice:?}");
1340            assert_eq!(mine.storage_conflict(), None);
1341            assert_eq!(stored(&host, "A"), expected, "{choice:?}");
1342            assert_eq!(stored(&host, "A (local copy)"), copy, "{choice:?}");
1343            assert_eq!(shown_prices(&mine), shown, "{choice:?}");
1344            // A change from elsewhere waits for the drawing in progress.
1345            if choice == ConflictChoice::Retry {
1346                theirs.drawings_mut().curr_drawing = Some(line_at(0.0, 12.0));
1347                theirs.flush_stored_drawings();
1348                assert_eq!(shown_prices(&theirs), [10.0, 10.5]);
1349                theirs.drawings_mut().curr_drawing = None;
1350                theirs.flush_stored_drawings();
1351                assert_eq!(shown_prices(&theirs), [10.0, 11.0]);
1352            }
1353            if choice == ConflictChoice::KeepLocalCopy {
1354                // Numbered copies are offered; names that are not copies of `A` are not.
1355                host.put("A (local copy 2)", &host.get("A").unwrap());
1356                let other = DrawingStorage::new("B").to_json().unwrap();
1357                for name in [
1358                    "A (local copy 3)",
1359                    "A (local copy 02)",
1360                    "AB (local copy)",
1361                    "A (local copy) (local copy)",
1362                ] {
1363                    host.put(name, &other);
1364                }
1365                assert_eq!(mine.local_copies(), None);
1366                mine.list_local_copies();
1367                assert!(mine.flush_stored_drawings());
1368                let listed = |chart: &Chart| chart.local_copies().map(<[String]>::to_vec);
1369                assert_eq!(
1370                    listed(&mine),
1371                    Some(
1372                        ["A (local copy)", "A (local copy 2)", "A (local copy 3)"]
1373                            .map(String::from)
1374                            .to_vec()
1375                    )
1376                );
1377                assert!(mine.take_storage_error().is_none());
1378                // Another symbol's drawings under a copy's name are neither loaded nor deleted.
1379                mine.load_local_copy("A (local copy 3)");
1380                mine.discard_local_copy("A (local copy 3)");
1381                assert!(mine.flush_stored_drawings());
1382                assert!(mine.take_storage_error().is_some());
1383                assert!(host.get("A (local copy 3)").is_some());
1384                assert_eq!(shown_prices(&mine), [10.0, 10.5]);
1385                // Loading replaces the saveable drawings in one undo step, written like any edit.
1386                let mut overlay = line_at(0.0, 9.0);
1387                overlay.saveable = false;
1388                draw_line(&mut mine, overlay);
1389                mine.load_local_copy("A (local copy)");
1390                assert!(mine.flush_stored_drawings());
1391                assert_eq!(shown_prices(&mine), [9.0, 10.0, 11.0]);
1392                assert_eq!(stored(&host, "A"), Some(vec![10.0, 11.0]));
1393                assert!(mine.undo());
1394                assert!(mine.flush_stored_drawings());
1395                assert_eq!(shown_prices(&mine), [9.0, 10.0, 10.5]);
1396                assert_eq!(stored(&host, "A"), Some(vec![10.0, 10.5]));
1397                // Only a listed copy is discarded.
1398                mine.discard_local_copy("A (local copy 02)");
1399                mine.discard_local_copy("A (local copy)");
1400                assert!(mine.flush_stored_drawings());
1401                assert_eq!(host.get("A (local copy)"), None);
1402                assert!(host.get("A (local copy 02)").is_some());
1403                assert_eq!(
1404                    listed(&mine),
1405                    Some(
1406                        ["A (local copy 2)", "A (local copy 3)"]
1407                            .map(String::from)
1408                            .to_vec()
1409                    )
1410                );
1411            }
1412        }
1413    }
1414
1415    /// Guards #1463: two charts of one layout on one storage edit one symbol with drawing sync
1416    /// on, whether the host keeps revisions or not, without a refusal or a lost drawing; a
1417    /// drawing keeps its study's pane, and a reread keeps what an open dialog refers to.
1418    #[cfg(feature = "studies")]
1419    #[test]
1420    fn charts_of_one_layout_follow_each_others_stored_drawings() {
1421        use crate::services::document::tests::Revised;
1422        use crate::studies::{CustomIndicator, IndicatorValue};
1423
1424        let unwatched = Revised::default();
1425        unwatched.2.store(true, Ordering::Relaxed);
1426        let hosts: [Arc<dyn Storage>; 2] = [Arc::new(Host::default()), Arc::new(unwatched)];
1427        for host in hosts {
1428            let new_chart = || {
1429                let bars = (0..10)
1430                    .map(|i| {
1431                        let time = Utc.timestamp_opt(i * 60, 0).unwrap();
1432                        Bar::new(time, 10.0, 11.0, 9.0, 10.0, 1.0)
1433                    })
1434                    .collect();
1435                let mut chart = Chart::new(BarData::from_bars(bars));
1436                chart.set_symbol("A");
1437                chart.set_storage(host.clone());
1438                chart
1439            };
1440            let (mut a, mut b) = (new_chart(), new_chart());
1441            let gamma = a.add_indicator(Box::new(
1442                CustomIndicator::new("Gamma", |bars: &[Bar]| {
1443                    vec![IndicatorValue::Single(40.0); bars.len()]
1444                })
1445                .with_overlay(false),
1446            ));
1447            let mut layout = ChartLayout::default();
1448            layout.mode = ChartLayoutMode::TwoHorizontal;
1449            layout.sync.sync_drawings = true;
1450            let ctx = egui::Context::default();
1451            // A write, the reread it causes, and a frame of rest.
1452            let mut frames = |a: &mut Chart, b: &mut Chart| {
1453                for _ in 0..3 {
1454                    let input = RawInput {
1455                        screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
1456                        ..Default::default()
1457                    };
1458                    ctx.run_ui(input, |ui| {
1459                        layout.show(ui, &mut [&mut *a, &mut *b]);
1460                    })
1461                    .drop_without_applying_deltas();
1462                }
1463            };
1464            let find = |chart: &Chart, price: f64| {
1465                let drawings = &chart.drawings().drawings;
1466                drawings
1467                    .iter()
1468                    .find(|d| d.chart_points[0].price == price)
1469                    .cloned()
1470                    .unwrap()
1471            };
1472            frames(&mut a, &mut b);
1473            let mut study_line = line_at(4.0, 40.0);
1474            study_line.pane = Some(gamma.0);
1475            draw_line(&mut a, study_line);
1476            frames(&mut a, &mut b);
1477            assert_eq!(shown_prices(&a), [40.0]);
1478            assert_eq!(shown_prices(&b), [40.0]);
1479
1480            draw_line(&mut b, line_at(4.0, 10.5));
1481            frames(&mut a, &mut b);
1482            assert_eq!(shown_prices(&a), [10.5, 40.0]);
1483            assert_eq!(shown_prices(&b), [10.5, 40.0]);
1484            assert_eq!(find(&a, 40.0).pane, Some(gamma.0));
1485            assert_eq!(find(&a, 10.5).pane, None);
1486
1487            // Properties opened before a reread that removes one drawing and adds another.
1488            #[cfg(feature = "ui")]
1489            {
1490                use crate::ui::dialogs::{DrawingId, DrawingProps};
1491                let open = |chart: &Chart, price: f64| {
1492                    let drawing = find(chart, price);
1493                    let props = DrawingProps {
1494                        line_width: 3.5,
1495                        ..DrawingProps::from_drawing(&drawing, chart)
1496                    };
1497                    (DrawingId(drawing.id), props)
1498                };
1499                let (removed_id, removed) = open(&a, 40.0);
1500                let (kept_id, kept) = open(&a, 10.5);
1501                let gone = find(&b, 40.0).id;
1502                b.user_action(|chart| {
1503                    chart.drawings_mut().delete_drawing(gone);
1504                });
1505                draw_line(&mut b, line_at(4.0, 9.5));
1506                frames(&mut a, &mut b);
1507                assert_eq!(shown_prices(&a), [9.5, 10.5]);
1508                assert_eq!(find(&a, 10.5).id, kept_id.0);
1509                assert!(removed.apply_to_chart(removed_id, &mut a).is_err());
1510                let width = find(&a, 9.5).stroke_width;
1511                assert_eq!(find(&a, 10.5).stroke_width, width);
1512                kept.apply_to_chart(kept_id, &mut a).unwrap();
1513                assert_eq!(find(&a, 10.5).stroke_width, 3.5);
1514                assert_eq!(find(&a, 9.5).stroke_width, width);
1515            }
1516
1517            // A drawing no document holds stays through a reread and reaches the other chart by
1518            // drawing sync.
1519            let mut volatile = line_at(4.0, 77.0);
1520            volatile.saveable = false;
1521            a.drawings_mut().add_synced_drawing(volatile).unwrap();
1522            frames(&mut a, &mut b);
1523            draw_line(&mut b, line_at(4.0, 8.5));
1524            frames(&mut a, &mut b);
1525            let count = |chart: &Chart, price: f64| {
1526                shown_prices(chart).iter().filter(|p| **p == price).count()
1527            };
1528            for chart in [&a, &b] {
1529                assert_eq!(count(chart, 8.5), 1);
1530                assert_eq!(count(chart, 77.0), 1);
1531            }
1532        }
1533    }
1534
1535    /// Guards #1614: a drawing read again never takes the local id of a drawing that is not
1536    /// saved, so removing that one by its id leaves the read drawing on both charts and stored.
1537    #[test]
1538    fn a_drawing_read_again_never_takes_the_id_of_one_that_is_not_saved() {
1539        let host = Arc::new(Host::default());
1540        let storage: Arc<dyn Storage> = host.clone();
1541        let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1542        let mut layout = ChartLayout::default();
1543        layout.mode = ChartLayoutMode::TwoHorizontal;
1544        let ctx = egui::Context::default();
1545        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1546        // The host marks a level on B that no document holds, and gets id 0.
1547        let mut marker = line_at(4.0, 77.0);
1548        marker.saveable = false;
1549        let marker = b.drawings_mut().add_drawing(marker).unwrap();
1550        // A's first drawing is stored as "0", and B reads it.
1551        draw_line(&mut a, line_at(4.0, 10.5));
1552        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1553        assert_eq!(shown_prices(&b), [10.5, 77.0]);
1554        b.drawings_mut().delete_drawing(marker);
1555        assert_eq!(shown_prices(&b), [10.5]);
1556        draw_line(&mut b, line_at(4.0, 9.0));
1557        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1558        assert_eq!(
1559            [shown_prices(&a), shown_prices(&b)],
1560            [[9.0, 10.5], [9.0, 10.5]]
1561        );
1562        assert_eq!(stored_prices(&host), [9.0, 10.5]);
1563    }
1564
1565    /// Guards #1615: an edit made while the chart reads its drawings again is not written before
1566    /// the read is answered, and the read answered after it does not replace the edit.
1567    #[test]
1568    fn an_edit_made_while_the_drawings_are_read_again_waits_for_the_read() {
1569        let host = Arc::new(Host::default());
1570        let storage: Arc<dyn Storage> = host.clone();
1571        let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1572        let mut layout = ChartLayout::default();
1573        layout.mode = ChartLayoutMode::TwoHorizontal;
1574        let ctx = egui::Context::default();
1575        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1576        draw_line(&mut a, line_at(4.0, 10.0));
1577        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1578        assert_eq!(shown_prices(&b), [10.0]);
1579        // A draws a line and undoes it, so A writes and B reads the drawings again; the host
1580        // has not answered that read yet.
1581        host.delay_loads.store(true, Ordering::Relaxed);
1582        draw_line(&mut a, line_at(4.0, 12.0));
1583        assert!(a.undo());
1584        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1585        let (_, read) = host.loads.lock().unwrap().pop().expect("B reads again");
1586        let before = host.documents.lock().unwrap()["A"].clone();
1587        draw_line(&mut b, line_at(4.0, 11.0));
1588        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1589        assert_eq!(stored_prices(&host), [10.0], "written during the read");
1590        host.delay_loads.store(false, Ordering::Relaxed);
1591        read.send(Ok(Some(before)));
1592        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1593        assert_eq!(shown_prices(&b), [10.0, 11.0]);
1594        assert_eq!(stored_prices(&host), [10.0, 11.0]);
1595        assert!(b.can_undo(), "B read its own write again");
1596    }
1597
1598    /// Guards #1617: a failed read of drawings another chart wrote is reported once and tried
1599    /// again each second, so the chart's next edit keeps them.
1600    #[test]
1601    fn a_failed_read_of_drawings_another_chart_wrote_is_reported_once_and_tried_again() {
1602        let host = Arc::new(Host::default());
1603        let storage: Arc<dyn Storage> = host.clone();
1604        let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1605        let mut layout = ChartLayout::default();
1606        layout.mode = ChartLayoutMode::TwoHorizontal;
1607        let ctx = egui::Context::default();
1608        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1609        host.fail_loads.store(true, Ordering::Relaxed);
1610        draw_line(&mut a, line_at(4.0, 10.0));
1611        // A writes; B's read of it fails, and fails again a second later.
1612        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1613        assert_eq!(
1614            b.take_storage_error()
1615                .map(|error| error.to_string())
1616                .as_deref(),
1617            Some("Could not read A: network error: offline")
1618        );
1619        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1620        assert!(
1621            b.take_storage_error().is_none(),
1622            "a run of failed reads is reported once"
1623        );
1624        host.fail_loads.store(false, Ordering::Relaxed);
1625        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 3.0);
1626        assert_eq!(shown_prices(&b), [10.0]);
1627        draw_line(&mut b, line_at(4.0, 11.0));
1628        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1629        assert_eq!(stored_prices(&host), [10.0, 11.0]);
1630        // A write of B ends a run of failed reads, so the next failure is reported again.
1631        host.fail_loads.store(true, Ordering::Relaxed);
1632        draw_line(&mut a, line_at(4.0, 12.0));
1633        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1634        assert!(b.take_storage_error().is_some());
1635        draw_line(&mut b, line_at(4.0, 13.0));
1636        draw_line(&mut a, line_at(4.0, 14.0));
1637        show_frames(&ctx, &mut layout, &mut [&mut a, &mut b], 0.1);
1638        assert!(b.take_storage_error().is_some(), "a new run is reported");
1639    }
1640
1641    /// Guards #1619 and #1650: with drawing sync on, a chart outside the charts' shared storage
1642    /// shows each of their drawings once, and its own drawing, mirrored onto them but not
1643    /// stored, stays as it is while they read each other's writes. A storage that reports
1644    /// changes has a chart read another's write in the frame that write is made.
1645    #[test]
1646    fn a_chart_outside_the_storage_shows_each_drawing_once_and_keeps_its_own() {
1647        use crate::services::document::tests::Revised;
1648
1649        for revised in [false, true] {
1650            let host = Arc::new(Host::default());
1651            let watched = Arc::new(Revised::default());
1652            let storage: Arc<dyn Storage> = if revised {
1653                watched.clone()
1654            } else {
1655                host.clone()
1656            };
1657            let (mut a, mut b) = (chart_on(Some(storage.clone())), chart_on(Some(storage)));
1658            let mut c = chart_on(None);
1659            let mut layout = ChartLayout::default();
1660            layout.mode = ChartLayoutMode::Three;
1661            layout.sync.sync_drawings = true;
1662            let ctx = egui::Context::default();
1663            let mut show = |a: &mut Chart, b: &mut Chart, c: &mut Chart| {
1664                show_frames(&ctx, &mut layout, &mut [a, b, c], 0.1);
1665            };
1666            show(&mut a, &mut b, &mut c);
1667            draw_line(&mut a, line_at(4.0, 10.5));
1668            show(&mut a, &mut b, &mut c);
1669            assert_eq!([shown_prices(&b), shown_prices(&c)], [[10.5], [10.5]]);
1670            let own = draw_line(&mut c, line_at(4.0, 9.0));
1671            show(&mut a, &mut b, &mut c);
1672            draw_line(&mut a, line_at(4.0, 8.0));
1673            show(&mut a, &mut b, &mut c);
1674            for chart in [&a, &b, &c] {
1675                assert_eq!(shown_prices(chart), [8.0, 9.0, 10.5]);
1676            }
1677            let own = c.drawings().drawings.iter().find(|d| d.id == own).unwrap();
1678            assert_eq!(own.chart_points[0].price, 9.0);
1679            assert_eq!(
1680                if revised {
1681                    prices_in(&watched.get("A").unwrap())
1682                } else {
1683                    stored_prices(&host)
1684                },
1685                [8.0, 10.5],
1686                "a mirrored drawing is not stored"
1687            );
1688        }
1689    }
1690}