Skip to main content

egui_charts/widget/
history.rs

1//! Chart history records UI actions, not ordinary host API calls.
2
3use super::series::{PriceSeries, SeriesOptions};
4use super::{Chart, ChartElementId, ChartEvent, SeriesId};
5use crate::config::{ChartConfig, ChartOptions};
6#[cfg(feature = "drawings")]
7use crate::drawings::DrawingManager;
8#[cfg(feature = "drawings")]
9use crate::drawings::services::HistoryService;
10use crate::model::{ChartType, ChartTypeParams, SeriesAlerts, SeriesScale, Timeframe};
11use crate::scales::PriceScale;
12use crate::studies::{Indicator, StudySettingsSnapshot};
13
14#[derive(Default)]
15pub(super) struct ChartHistory {
16    undo: Vec<Action>,
17    redo: Vec<Action>,
18    pending: Option<Pending>,
19    changed: bool,
20    /// User edits recorded, undone or redone, kept when the history is cleared.
21    pub(super) edits: u64,
22    /// The edits among them that changed drawings.
23    pub(super) drawing_edits: u64,
24}
25
26struct Pending {
27    depth: usize,
28    before: Action,
29    #[cfg(feature = "drawings")]
30    standalone_drawings: HistoryService,
31    study_order: Vec<SeriesId>,
32    /// Added series removed during the action, with their bars, price formatters and bar limits.
33    removed_series: Vec<HeldSeries>,
34}
35
36#[derive(Default)]
37struct Action {
38    symbol: Option<String>,
39    series: Option<SeriesSet>,
40    config: Option<ChartConfig>,
41    /// The main series of the other state, when the action removed it.
42    main_series: Option<MainSeries>,
43    main_series_visible: Option<bool>,
44    price_targets_visible: Option<bool>,
45    options: Option<ChartOptions>,
46    view: Option<(f32, f32)>,
47    scale: Option<PriceScale>,
48    /// Scales that differ from the chart's, by assignment; `None` where the chart has none.
49    series_scales: std::collections::HashMap<crate::model::SeriesScale, Option<PriceScale>>,
50    panes: Option<super::pane::PaneLayout>,
51    chart_type: Option<(ChartType, ChartTypeParams)>,
52    interval: Option<Option<Timeframe>>,
53    studies: Vec<StudyChange>,
54    #[cfg(feature = "drawings")]
55    drawings: HistoryService,
56}
57
58impl Action {
59    fn is_empty(&self) -> bool {
60        #[cfg(feature = "drawings")]
61        let drawings_changed = self.drawings.can_undo();
62        #[cfg(not(feature = "drawings"))]
63        let drawings_changed = false;
64        self.symbol.is_none()
65            && self.series.is_none()
66            && self.view.is_none()
67            && self.config.is_none()
68            && self.main_series.is_none()
69            && self.main_series_visible.is_none()
70            && self.price_targets_visible.is_none()
71            && self.options.is_none()
72            && self.scale.is_none()
73            && self.series_scales.is_empty()
74            && self.panes.is_none()
75            && self.chart_type.is_none()
76            && self.interval.is_none()
77            && self.studies.is_empty()
78            && !drawings_changed
79    }
80}
81
82/// The added series of one state of the chart: their draw order, display settings and
83/// visibility, the scale of the main series, and the series the other state lacks, whole, with
84/// their price formatters and bar limits.
85struct SeriesSet {
86    props: Vec<(SeriesId, SeriesOptions, bool)>,
87    main_scale: SeriesScale,
88    main_alerts: SeriesAlerts,
89    held: Vec<HeldSeries>,
90}
91
92impl SeriesSet {
93    fn capture(chart: &Chart) -> Self {
94        Self {
95            props: chart
96                .price_series
97                .iter()
98                .map(|item| (item.id, item.options.clone(), item.visible))
99                .collect(),
100            main_scale: chart.main_series_scale,
101            main_alerts: chart.main_series_style.alerts,
102            held: Vec::new(),
103        }
104    }
105
106    fn same(&self, other: &Self) -> bool {
107        self.main_scale == other.main_scale
108            && self.main_alerts == other.main_alerts
109            && self.props == other.props
110    }
111}
112
113/// An added series that one state of the chart lacks, with all its bars, whatever the replay
114/// cutoff, and the price formatter and bar limit that the chart keeps apart from it.
115struct HeldSeries {
116    series: PriceSeries,
117    formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
118    bar_limit: Option<std::num::NonZeroUsize>,
119}
120
121/// Whether the main series is present in one state of the chart, with the price lines, price
122/// formatter and bar limit that the state without it lacks. Its bars and markers stay in the
123/// chart, which reads them only while the series is present; the bars also give the remaining
124/// series their time axis while it is absent.
125#[derive(Default)]
126struct MainSeries {
127    present: bool,
128    price_lines: Vec<(super::PriceLineId, crate::model::PriceLine)>,
129    formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
130    bar_limit: Option<std::num::NonZeroUsize>,
131    /// The time axis the absent state kept, for the redo of a removal whose undo reloads the feed.
132    bars: Vec<crate::model::Bar>,
133}
134
135struct StudyChange {
136    id: SeriesId,
137    before: Option<Study>,
138}
139
140struct Study {
141    index: usize,
142    settings: StudySettingsSnapshot,
143    indicator: Option<Box<dyn Indicator>>,
144    inputs: String,
145    autoscale: bool,
146}
147
148impl ChartHistory {
149    #[cfg(feature = "drawings")]
150    pub(super) fn visit_chart_points(
151        &mut self,
152        visit: &mut impl FnMut(&mut crate::drawings::ChartPoint),
153    ) {
154        for action in self.undo.iter_mut().chain(&mut self.redo) {
155            action.drawings.visit_chart_points(visit);
156        }
157        if let Some(pending) = &mut self.pending {
158            pending.standalone_drawings.visit_chart_points(visit);
159            pending.before.drawings.visit_chart_points(visit);
160        }
161    }
162
163    /// Whether a user action is in progress.
164    pub(super) fn recording(&self) -> bool {
165        self.pending.is_some()
166    }
167
168    /// Forgets every action, keeping the edit counts.
169    pub(super) fn clear(&mut self) {
170        *self = Self {
171            edits: self.edits,
172            drawing_edits: self.drawing_edits,
173            ..Self::default()
174        };
175    }
176
177    fn push(&mut self, action: Action) {
178        self.edits += 1;
179        #[cfg(feature = "drawings")]
180        if action.drawings.can_undo() {
181            self.drawing_edits += 1;
182        }
183        self.redo.clear();
184        if self.undo.len() == 100 {
185            self.undo.remove(0);
186        }
187        self.undo.push(action);
188    }
189
190    #[cfg(feature = "drawings")]
191    pub(super) fn start_drawing_action(
192        &self,
193        drawings: &mut DrawingManager,
194    ) -> Option<HistoryService> {
195        if self.pending.is_some() {
196            return None;
197        }
198        let mut history = HistoryService::with_max_size(0);
199        drawings.swap_history(&mut history);
200        Some(history)
201    }
202
203    #[cfg(feature = "drawings")]
204    pub(super) fn finish_drawing_action(
205        &mut self,
206        drawings: &mut DrawingManager,
207        history: Option<HistoryService>,
208    ) {
209        let placement = drawings.finished_text_placement.take();
210        if let Some(mut history) = history {
211            drawings.swap_history(&mut history);
212            if let Some(id) = placement
213                && let Some(index) = self.undo.iter().rposition(|action| action.drawings.can_undo())
214                && let drawing = drawings.drawings.iter().find(|drawing| drawing.id == id)
215                // Cancelling empty text must not undo unrelated chart settings first.
216                && (drawing.is_none() || index + 1 == self.undo.len())
217                && self.undo[index].drawings.merge_text_placement(id, &mut history, drawing)
218            {
219                if self.undo[index].is_empty() {
220                    self.undo.remove(index);
221                }
222                self.edits += 1;
223                self.drawing_edits += 1;
224                self.redo.clear();
225            } else if history.can_undo() {
226                self.push(Action {
227                    drawings: history,
228                    ..Action::default()
229                });
230            }
231        }
232    }
233}
234
235impl Study {
236    fn capture(chart: &Chart, id: SeriesId) -> Option<Self> {
237        let index = chart
238            .study_ids
239            .iter()
240            .position(|candidate| *candidate == id)?;
241        Some(Self {
242            index,
243            settings: chart.studies.indicators()[index].snapshot_settings(),
244            indicator: None,
245            inputs: chart.study_inputs[index].clone(),
246            autoscale: chart.study_autoscale[index],
247        })
248    }
249
250    fn same(&self, other: &Self) -> bool {
251        self.index == other.index
252            && self.inputs == other.inputs
253            && self.autoscale == other.autoscale
254            && self.settings == other.settings
255    }
256}
257
258impl Chart {
259    /// Apply one user action from a host toolbar, menu, dialog or intent as one undo step.
260    /// Nested actions belong to the outer action. Unchanged chart/study settings add no history.
261    ///
262    /// Chart-owned drawing interactions and shortcuts record themselves. Ordinary host calls
263    /// (including data/feed updates, setters and `drawings_mut`) do not enter chart history;
264    /// wrap only edits originating from a user action in this method. Drawing commands and
265    /// bulk snapshots are transferred from the drawing manager, retaining its sync events.
266    /// History restores the edited study or settings snapshot, not market data or host state.
267    /// Hosts without a chart-owned feed should use [`Self::take_history_changed`] to reload
268    /// bars and synchronize their interval/style/settings controls after undo or redo.
269    pub fn user_action<R>(&mut self, edit: impl FnOnce(&mut Self) -> R) -> R {
270        self.begin_user_action();
271        let result = edit(self);
272        self.end_user_action();
273        result
274    }
275
276    /// Begin a user action spanning a host handler that also borrows non-chart state.
277    /// Pair with [`Self::end_user_action`]; prefer [`Self::user_action`] when possible.
278    pub fn begin_user_action(&mut self) {
279        self.apply_config_scale_mode();
280        if let Some(pending) = &mut self.history.pending {
281            pending.depth += 1;
282            return;
283        }
284        #[cfg(feature = "drawings")]
285        let mut standalone_drawings = HistoryService::with_max_size(0);
286        self.sync_panes();
287        #[cfg(feature = "drawings")]
288        self.drawings.swap_history(&mut standalone_drawings);
289        self.history.pending = Some(Pending {
290            depth: 1,
291            before: Action {
292                symbol: Some(self.symbol.clone()),
293                series: Some(SeriesSet::capture(self)),
294                config: Some(self.config.clone()),
295                main_series_visible: Some(self.main_series_visible),
296                price_targets_visible: Some(self.show_price_targets),
297                options: Some(self.chart_options.clone()),
298                view: Some(self.get_time_scale_state()),
299                scale: Some(self.state.price_scale().clone()),
300                series_scales: self
301                    .series_scales
302                    .iter()
303                    .map(|(id, scale)| (*id, Some(scale.clone())))
304                    .collect(),
305                panes: Some(self.pane_layout.clone()),
306                chart_type: Some((self.chart_type, self.chart_type_params.clone())),
307                interval: Some(self.timeframe),
308                ..Action::default()
309            },
310            #[cfg(feature = "drawings")]
311            standalone_drawings,
312            study_order: self.study_ids.clone(),
313            removed_series: Vec::new(),
314        });
315    }
316
317    /// Finish the matching [`Self::begin_user_action`], recording only changed components.
318    pub fn end_user_action(&mut self) {
319        #[cfg(feature = "studies")]
320        if !self.config.show_volume
321            && let Some(id) = self.volume_study
322        {
323            // Capture the removed study before the volume setting's action closes.
324            self.remove_series(id);
325        }
326        let Some(mut pending) = self.history.pending.take() else {
327            return;
328        };
329        if pending.depth > 1 {
330            pending.depth -= 1;
331            self.history.pending = Some(pending);
332            return;
333        }
334        self.apply_config_scale_mode();
335        #[cfg(feature = "drawings")]
336        self.drawings.swap_history(&mut pending.standalone_drawings);
337        self.sync_panes();
338        let mut action = pending.before;
339        #[cfg(feature = "drawings")]
340        {
341            action.drawings = pending.standalone_drawings;
342        }
343        if action.symbol.as_ref() == Some(&self.symbol) {
344            action.symbol = None;
345        }
346        if let Some(before) = &mut action.series {
347            if before.same(&SeriesSet::capture(self)) {
348                action.series = None;
349            } else {
350                before.held = pending
351                    .removed_series
352                    .into_iter()
353                    .filter(|item| before.props.iter().any(|(id, ..)| *id == item.series.id))
354                    .collect();
355            }
356        }
357        if action.view == Some(self.get_time_scale_state()) {
358            action.view = None;
359        }
360        if action.config.as_ref() == Some(&self.config) {
361            action.config = None;
362        }
363        if action.price_targets_visible == Some(self.show_price_targets) {
364            action.price_targets_visible = None;
365        }
366        if action.main_series_visible == Some(self.main_series_visible) {
367            action.main_series_visible = None;
368        }
369        if action.options.as_ref() == Some(&self.chart_options) {
370            action.options = None;
371        }
372        if action.chart_type.as_ref() == Some(&(self.chart_type, self.chart_type_params.clone())) {
373            action.chart_type = None;
374        }
375        if action.interval == Some(self.timeframe) {
376            action.interval = None;
377        }
378        if let Some(scale) = &action.scale {
379            let current = self.state.price_scale();
380            if scale.options() == current.options()
381                && scale.is_manual() == current.is_manual()
382                && (!scale.is_manual() || scale.range() == current.range())
383            {
384                action.scale = None;
385            }
386        }
387        let known: Vec<_> = action.series_scales.keys().copied().collect();
388        action.series_scales.retain(|id, scale| {
389            let (Some(scale), Some(current)) = (scale, self.series_scales.get(id)) else {
390                return true;
391            };
392            scale.options() != current.options()
393                || scale.is_manual() != current.is_manual()
394                || (scale.is_manual() && scale.range() != current.range())
395        });
396        for id in self.series_scales.keys().filter(|id| !known.contains(id)) {
397            action.series_scales.insert(*id, None);
398        }
399        if let Some(panes) = &action.panes
400            && panes.maximized == self.pane_layout.maximized
401            && panes.main == self.pane_layout.main
402            && panes.panes.len() == self.pane_layout.panes.len()
403            && panes
404                .panes
405                .iter()
406                .zip(&self.pane_layout.panes)
407                .all(|(a, b)| {
408                    a.id == b.id
409                        && a.members == b.members
410                        && a.stretch == b.stretch
411                        && a.height == b.height
412                        && a.preserve == b.preserve
413                        && a.collapsed == b.collapsed
414                        && a.scale.options() == b.scale.options()
415                        && a.scale.is_manual() == b.scale.is_manual()
416                        && (!a.scale.is_manual() || a.scale.range() == b.scale.range())
417                })
418        {
419            action.panes = None;
420        }
421        action.studies.retain(
422            |change| match (&change.before, Study::capture(self, change.id)) {
423                (None, None) => false,
424                (Some(before), Some(after)) => !before.same(&after),
425                _ => true,
426            },
427        );
428        if action.is_empty() {
429            return;
430        }
431        // Edits after a symbol change belong to the new symbol, so they are undone first.
432        #[cfg(feature = "drawings")]
433        let later = action
434            .symbol
435            .is_some()
436            .then(|| Action {
437                drawings: std::mem::take(&mut action.drawings),
438                ..Action::default()
439            })
440            .filter(|later| later.drawings.can_undo());
441        self.history.push(action);
442        #[cfg(feature = "drawings")]
443        if let Some(later) = later {
444            self.history.push(later);
445        }
446    }
447
448    /// Ends the drawing record of the pending action before another symbol's drawings take
449    /// over. What it recorded belongs to the old symbol, so it becomes an action of its own.
450    #[cfg(feature = "drawings")]
451    pub(super) fn detach_pending_drawings(&mut self) {
452        let Some(pending) = &mut self.history.pending else {
453            return;
454        };
455        self.drawings.swap_history(&mut pending.standalone_drawings);
456        let recorded = std::mem::replace(
457            &mut pending.standalone_drawings,
458            HistoryService::with_max_size(0),
459        );
460        if recorded.can_undo() {
461            self.history.push(Action {
462                drawings: recorded,
463                ..Action::default()
464            });
465        }
466    }
467
468    /// Starts recording the pending action's drawing edits on the new symbol's drawings.
469    #[cfg(feature = "drawings")]
470    pub(super) fn attach_pending_drawings(&mut self) {
471        if let Some(pending) = &mut self.history.pending {
472            let mut recording = HistoryService::with_max_size(0);
473            self.drawings.swap_history(&mut recording);
474            pending.standalone_drawings = recording;
475        }
476    }
477
478    /// Keeps a bar series removed by the pending action, bars, price formatter and bar limit
479    /// included, so undo restores it. Outside an action they are dropped.
480    pub(super) fn retain_removed_series(&mut self, series: PriceSeries) {
481        let held = self.hold_series(series);
482        if let Some(pending) = &mut self.history.pending {
483            pending.removed_series.push(held);
484        }
485    }
486
487    /// Takes `series`, which has left the chart, with all its bars, its price formatter and its
488    /// bar limit.
489    fn hold_series(&mut self, mut series: PriceSeries) -> HeldSeries {
490        self.replay_release(&mut series);
491        let mut held = HeldSeries {
492            series,
493            formatter: None,
494            bar_limit: None,
495        };
496        self.swap_series_settings(held.series.id, &mut held.formatter, &mut held.bar_limit);
497        held
498    }
499
500    /// Gives a held series its price formatter and bar limit back as it enters the chart, under
501    /// the cutoff of a running replay.
502    fn restore_series(&mut self, mut held: HeldSeries) -> PriceSeries {
503        self.swap_series_settings(held.series.id, &mut held.formatter, &mut held.bar_limit);
504        self.replay_admit(&mut held.series);
505        held.series
506    }
507
508    /// Takes the main series out of the chart and pauses its feed. The pending action keeps its
509    /// price lines, formatter and bar limit, so undo restores them.
510    pub(super) fn remove_main_series(&mut self) {
511        let mut removed = MainSeries::default();
512        self.swap_main_series(&mut removed);
513        if let Some(pending) = &mut self.history.pending {
514            pending.before.main_series = Some(removed);
515        }
516    }
517
518    /// Exchanges the main series' presence, price lines, formatter and bar limit with `main`,
519    /// so the exchange reverses itself, and pauses or resumes the feed to match. Removing the
520    /// series again before the resumed feed sent its bars, or after it failed to, restores the
521    /// bars the series left behind, so the remaining series keep their time axis.
522    fn swap_main_series(&mut self, main: &mut MainSeries) {
523        std::mem::swap(&mut self.main_series_present, &mut main.present);
524        std::mem::swap(&mut self.price_lines, &mut main.price_lines);
525        self.swap_series_settings(SeriesId::MAIN, &mut main.formatter, &mut main.bar_limit);
526        let paused = self.feed_link.as_ref().is_some_and(|link| link.host_paused);
527        self.set_feed_paused(paused);
528        if self.main_series_present {
529            // The resumed feed's reload empties the bars until its page arrives.
530            if self.feed_link.as_ref().is_some_and(|link| !link.paused) {
531                main.bars = self.state.data().bars.clone();
532            }
533        } else {
534            let kept = std::mem::take(&mut main.bars);
535            if self.state.data().bars.is_empty() && !kept.is_empty() {
536                self.replace_main_data(kept);
537            }
538        }
539    }
540
541    /// Exchanges the price formatter and bar limit of the bar series `id` with `formatter` and
542    /// `bar_limit`, so the exchange reverses itself.
543    fn swap_series_settings(
544        &mut self,
545        id: SeriesId,
546        formatter: &mut Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
547        bar_limit: &mut Option<std::num::NonZeroUsize>,
548    ) {
549        *formatter = match formatter.take() {
550            Some(formatter) => self.series_formatters.insert(id, formatter),
551            None => self.series_formatters.remove(&id),
552        };
553        *bar_limit = match bar_limit.take() {
554            Some(limit) => self.bar_limits.insert(id, limit),
555            None => self.bar_limits.remove(&id),
556        };
557    }
558
559    pub(super) fn record_study(&mut self, id: SeriesId) {
560        self.element_studies.dirty = true;
561        if self
562            .history
563            .pending
564            .as_ref()
565            .is_some_and(|pending| !pending.before.studies.iter().any(|change| change.id == id))
566        {
567            let mut before = Study::capture(self, id);
568            if let Some(study) = &mut before {
569                study.index = self
570                    .history
571                    .pending
572                    .as_ref()
573                    .unwrap()
574                    .study_order
575                    .iter()
576                    .position(|candidate| *candidate == id)
577                    .unwrap_or(study.index);
578            }
579            self.history
580                .pending
581                .as_mut()
582                .unwrap()
583                .before
584                .studies
585                .push(StudyChange { id, before });
586        }
587    }
588
589    pub(super) fn retain_removed_study(&mut self, id: SeriesId, indicator: Box<dyn Indicator>) {
590        let change = if let Some(pending) = &mut self.history.pending {
591            pending
592                .before
593                .studies
594                .iter_mut()
595                .find(|change| change.id == id)
596        } else {
597            // Host removal does not record an action, but existing edits must remain replayable.
598            self.history
599                .undo
600                .iter_mut()
601                .rev()
602                .chain(self.history.redo.iter_mut().rev())
603                .flat_map(|action| &mut action.studies)
604                .find(|change| change.id == id && change.before.is_some())
605        };
606        if let Some(study) = change.and_then(|change| change.before.as_mut()) {
607            study.indicator = Some(indicator);
608        }
609    }
610
611    /// Exchanges the added series with those of `set`. A series the chart lacks comes from the
612    /// set whole, and one the set lacks goes into it, so the exchange reverses itself.
613    fn swap_series(&mut self, set: &mut SeriesSet) {
614        let now = SeriesSet::capture(self);
615        let mut pool = std::mem::take(&mut self.price_series);
616        let mut held = std::mem::take(&mut set.held);
617        for (id, options, visible) in set.props.drain(..) {
618            let mut item = if let Some(at) = pool.iter().position(|item| item.id == id) {
619                pool.swap_remove(at)
620            } else if let Some(at) = held.iter().position(|item| item.series.id == id) {
621                self.restore_series(held.swap_remove(at))
622            } else {
623                continue;
624            };
625            item.options = options;
626            item.visible = visible;
627            item.revision = crate::model::next_revision();
628            self.price_series.push(item);
629        }
630        // The series the set lacks leave the chart with their formatters and limits.
631        for item in pool {
632            held.push(self.hold_series(item));
633        }
634        self.main_series_scale = set.main_scale;
635        self.main_series_style.alerts = set.main_alerts;
636        *set = SeriesSet { held, ..now };
637        // `set.props` now lists the series shown before the exchange, in draw order.
638        for (id, ..) in &set.props {
639            if self.price_series.iter().all(|item| item.id != *id) {
640                self.host_events.push(ChartEvent::SeriesRemoved(*id));
641            }
642        }
643        for item in &self.price_series {
644            if set.props.iter().all(|(id, ..)| *id != item.id) {
645                self.host_events.push(ChartEvent::SeriesAdded(item.id));
646            }
647        }
648        if let Some(ChartElementId::Series(id)) = self.selection.selected_id()
649            && self.price_series.iter().all(|item| item.id != id)
650        {
651            self.selection.deselect();
652        }
653        self.fill_fed_series();
654    }
655
656    /// Whether a chart user action can be undone (also drives toolbar availability).
657    pub fn can_undo(&self) -> bool {
658        !self.history.undo.is_empty()
659    }
660
661    /// Whether a chart user action can be redone (also drives toolbar availability).
662    pub fn can_redo(&self) -> bool {
663        !self.history.redo.is_empty()
664    }
665
666    /// Undo the most recent chart user action, across drawings, studies and settings.
667    /// Returns false while a user action is in progress; finish that action first.
668    pub fn undo(&mut self) -> bool {
669        self.step_chart_history(false)
670    }
671
672    /// Redo the most recently undone chart user action.
673    /// Returns false while a user action is in progress; finish that action first.
674    pub fn redo(&mut self) -> bool {
675        self.step_chart_history(true)
676    }
677
678    /// Drain a notification that undo/redo changed chart state. Hosts that own the data must
679    /// reload it if [`Self::timeframe`] or [`Self::bar_clock`] changed, and synchronize their UI.
680    /// Added series that the step removed or brought back are reported by
681    /// [`drain_events`](Self::drain_events) as [`ChartEvent::SeriesRemoved`] and
682    /// [`ChartEvent::SeriesAdded`].
683    pub fn take_history_changed(&mut self) -> bool {
684        std::mem::take(&mut self.history.changed)
685    }
686
687    /// A count that changes with every user edit of the chart, undo and redo included, and with
688    /// every change of symbol. Compare it with the count at the last save to tell whether the
689    /// chart changed since, for example to save a layout automatically.
690    pub fn edit_count(&self) -> u64 {
691        self.history.edits
692    }
693
694    fn step_chart_history(&mut self, redo: bool) -> bool {
695        if self.history.pending.is_some() {
696            return false;
697        }
698        let stack = if redo {
699            &mut self.history.redo
700        } else {
701            &mut self.history.undo
702        };
703        let Some(mut action) = stack.pop() else {
704            return false;
705        };
706        // Validate on temporary calculations before changing chart state. A host study may
707        // reject a previously accepted input; failed replay leaves both history stacks intact.
708        let restore: Option<Vec<_>> = action
709            .studies
710            .iter()
711            .filter_map(|change| change.before.as_ref().map(|study| (change.id, study)))
712            .map(|(id, study)| {
713                let original = self
714                    .study_ids
715                    .iter()
716                    .position(|candidate| *candidate == id)
717                    .map(|at| self.studies.indicators()[at].as_ref())
718                    .or(study.indicator.as_deref())
719                    .or_else(|| {
720                        self.history
721                            .undo
722                            .iter()
723                            .chain(&self.history.redo)
724                            .flat_map(|action| &action.studies)
725                            .filter(|change| change.id == id)
726                            .find_map(|change| change.before.as_ref()?.indicator.as_deref())
727                    })?;
728                let mut indicator = original.clone_box();
729                indicator.restore_settings(&study.settings).ok()?;
730                Some((
731                    id,
732                    study.index,
733                    study.inputs.clone(),
734                    study.autoscale,
735                    indicator,
736                ))
737            })
738            .collect();
739        let Some(mut restore) = restore else {
740            if redo {
741                self.history.redo.push(action);
742            } else {
743                self.history.undo.push(action);
744            }
745            return false;
746        };
747        let current_view = self.get_time_scale_state();
748        let mut current: Vec<_> = action
749            .studies
750            .iter()
751            .map(|change| Study::capture(self, change.id))
752            .collect();
753        if let Some(symbol) = &mut action.symbol {
754            let target = std::mem::replace(symbol, self.symbol.clone());
755            self.change_symbol(target, true);
756        }
757        if let Some((kind, params)) = &mut action.chart_type {
758            let current = (self.chart_type, self.chart_type_params.clone());
759            self.set_chart_type(*kind);
760            self.set_chart_type_params(params.clone());
761            (*kind, *params) = current;
762        }
763        if let Some(interval) = &mut action.interval {
764            let current = self.timeframe;
765            if let Some(timeframe) = *interval {
766                self.set_timeframe(timeframe);
767            } else {
768                self.timeframe = None;
769                #[cfg(feature = "drawings")]
770                self.drawings.curr_timeframe.clear();
771            }
772            *interval = current;
773        }
774        if let Some(config) = &mut action.config {
775            std::mem::swap(&mut self.config, config);
776        }
777        if let Some(visible) = &mut action.price_targets_visible {
778            std::mem::swap(&mut self.show_price_targets, visible);
779        }
780        // Before the visibility, which an absent main series ignores.
781        if let Some(main) = &mut action.main_series {
782            self.swap_main_series(main);
783        }
784        if let Some(visible) = &mut action.main_series_visible {
785            let current = self.main_series_visible;
786            self.set_main_series_visible(*visible);
787            *visible = current;
788        }
789        if let Some(options) = &mut action.options {
790            std::mem::swap(&mut self.chart_options, options);
791            let (spacing, offset) = self.get_time_scale_state();
792            let opt = &self.chart_options.time_scale;
793            self.state.time_scale_mut().apply_options(
794                spacing,
795                opt.min_bar_spacing,
796                opt.max_bar_spacing,
797                opt.fix_left_edge,
798                opt.fix_right_edge,
799                offset,
800                None,
801            );
802        }
803        if let Some(view) = &mut action.view {
804            self.apply_synced_time_scale(view.0, view.1);
805            *view = current_view;
806            self.pending_scroll_time = None;
807            self.tracking_mode_active = false;
808        }
809        if let Some(scale) = &mut action.scale {
810            std::mem::swap(self.state.price_scale_mut(), scale);
811        }
812        for (id, scale) in &mut action.series_scales {
813            let current = self.series_scales.remove(id);
814            if let Some(scale) = scale.take() {
815                self.series_scales.insert(*id, scale);
816            }
817            *scale = current;
818        }
819        if let Some(set) = &mut action.series {
820            self.swap_series(set);
821        }
822        self.element_studies.dirty |= !action.studies.is_empty();
823        // Restore by original index after removing every touched study, so bulk removal and
824        // templates retain order and stable ids while unrelated host studies stay present.
825        let show_volume = self.config.show_volume;
826        for (change, current) in action.studies.iter().zip(&mut current) {
827            let indicator = self.take_study(change.id);
828            if change.before.is_none()
829                && let Some(current) = current
830            {
831                current.indicator = indicator;
832            }
833        }
834        restore.sort_by_key(|(_, index, ..)| *index);
835        for (id, index, inputs, autoscale, mut indicator) in restore {
836            if let Some(timeframe) = self.timeframe {
837                indicator.set_timeframe(timeframe);
838            }
839            indicator.set_instrument(self.instrument());
840            indicator.set_bar_clock(&self.bar_clock());
841            let at = index.min(self.study_ids.len());
842            // The registry exposes a slice; append then rotate to insert at the saved position.
843            self.studies.add(indicator);
844            self.studies.indicators_mut()[at..].rotate_right(1);
845            self.study_ids.insert(at, id);
846            self.study_inputs.insert(at, inputs);
847            self.study_autoscale.insert(at, autoscale);
848            self.pending_from.insert(at, None);
849            self.next_refresh.insert(at, f64::NEG_INFINITY);
850        }
851        self.recalculate_studies();
852        // Replacing a study during history replay is not a volume-switch edit.
853        self.config.show_volume = show_volume;
854        for (change, before) in action.studies.iter_mut().zip(current) {
855            change.before = before;
856        }
857        if let Some(panes) = &mut action.panes {
858            std::mem::swap(&mut self.pane_layout, panes);
859        }
860        self.sync_panes();
861        if let Some(ChartElementId::Indicator(id)) = self.selection.selected_id()
862            && !self.study_ids.contains(&id)
863        {
864            self.selection.deselect();
865        }
866        #[cfg(feature = "drawings")]
867        {
868            self.drawings.swap_history(&mut action.drawings);
869            if redo {
870                while self.drawings.redo() {}
871            } else {
872                while self.drawings.undo() {}
873            }
874            self.drawings.swap_history(&mut action.drawings);
875        }
876        self.history.edits += 1;
877        #[cfg(feature = "drawings")]
878        if action.drawings.can_undo() || action.drawings.can_redo() {
879            self.history.drawing_edits += 1;
880        }
881        if redo {
882            self.history.undo.push(action);
883        } else {
884            self.history.redo.push(action);
885        }
886        self.history.changed = true;
887        true
888    }
889}
890
891#[cfg(all(test, feature = "studies"))]
892mod tests {
893    use super::*;
894    #[cfg(feature = "drawings")]
895    use crate::drawings::manager::DrawingSyncAction;
896    #[cfg(feature = "drawings")]
897    use crate::drawings::{ChartPoint, Drawing, DrawingToolType};
898    use crate::model::BarData;
899    use crate::studies::{InputValue, RSI, SMA};
900    use crate::widget::test_frames::chart;
901    use egui::Color32;
902    #[cfg(feature = "drawings")]
903    use egui::{Context, Event, Key, Modifiers, RawInput};
904
905    /// A valid host trend line for history actions that do not test its geometry.
906    #[cfg(feature = "drawings")]
907    fn host_line() -> Drawing {
908        let mut drawing = Drawing::new(0, DrawingToolType::TrendLine);
909        drawing.chart_points = vec![ChartPoint::new(1.0, 15.0), ChartPoint::new(2.0, 16.0)];
910        drawing
911    }
912
913    #[test]
914    fn study_history_records_settings_without_cloning_calculations() {
915        use std::sync::{
916            Arc,
917            atomic::{AtomicUsize, Ordering},
918        };
919
920        #[derive(Clone)]
921        struct Counted(SMA, Arc<AtomicUsize>);
922        impl Indicator for Counted {
923            fn name(&self) -> &str {
924                self.0.name()
925            }
926            fn calculate(&mut self, bars: &[crate::model::Bar]) {
927                self.0.calculate(bars);
928            }
929            fn values(&self) -> &crate::studies::IndicatorValues {
930                self.0.values()
931            }
932            fn colors(&self) -> Vec<Color32> {
933                self.0.colors()
934            }
935            fn set_colors(&mut self, colors: Vec<Color32>) {
936                self.0.set_colors(colors);
937            }
938            fn set_visible(&mut self, visible: bool) {
939                self.0.set_visible(visible);
940            }
941            fn inputs(&self) -> Vec<crate::studies::StudyInput> {
942                self.0.inputs()
943            }
944            fn set_input(
945                &mut self,
946                id: &str,
947                value: InputValue,
948            ) -> Result<(), crate::studies::StudyError> {
949                self.0.set_input(id, value)
950            }
951            fn clone_box(&self) -> Box<dyn Indicator> {
952                self.1.fetch_add(1, Ordering::Relaxed);
953                Box::new(self.clone())
954            }
955        }
956
957        let clones = Arc::new(AtomicUsize::new(0));
958        let mut chart = chart(&[10.0; 30], &[20.0; 30]);
959        let id = chart.add_indicator(Box::new(Counted(SMA::new(2), clones.clone())));
960        let original = chart.indicators().next().unwrap().1.colors();
961        for red in 1..=30 {
962            chart.user_action(|chart| {
963                chart
964                    .indicator_mut(id)
965                    .unwrap()
966                    .set_colors(vec![Color32::from_rgb(red, 0, 0)])
967            });
968        }
969        assert_eq!(
970            clones.load(Ordering::Relaxed),
971            0,
972            "recording settings must not copy calculated histories"
973        );
974        // History must recalculate from today's closes, not restore the old output column.
975        let mut bars = chart.bars(SeriesId::MAIN)[..4].to_vec();
976        for (bar, close) in bars.iter_mut().zip([10.0, 14.0, 18.0, 22.0]) {
977            bar.close = close;
978            bar.high = close + 1.0;
979            bar.low = close - 1.0;
980            bar.open = close;
981        }
982        chart.set_data(SeriesId::MAIN, bars);
983        for _ in 0..30 {
984            assert!(chart.undo());
985            assert_eq!(
986                chart.indicators().next().unwrap().1.values().value(0, 3),
987                Some(20.0)
988            );
989        }
990        assert_eq!(chart.indicators().next().unwrap().1.colors(), original);
991        for red in 1..=30 {
992            assert!(chart.redo());
993            assert_eq!(
994                chart.indicators().next().unwrap().1.colors(),
995                [Color32::from_rgb(red, 0, 0)]
996            );
997            assert_eq!(
998                chart.indicators().next().unwrap().1.values().value(0, 3),
999                Some(20.0)
1000            );
1001        }
1002        let before_removal = clones.load(Ordering::Relaxed);
1003        chart.user_action(|chart| {
1004            chart
1005                .indicator_mut(id)
1006                .unwrap()
1007                .set_input("period", InputValue::Length(3))
1008                .unwrap();
1009            chart.remove_series(id);
1010        });
1011        assert_eq!(
1012            clones.load(Ordering::Relaxed),
1013            before_removal,
1014            "removal moves the original implementation into history"
1015        );
1016        assert!(chart.undo());
1017        assert_eq!(chart.study_ids, [id]);
1018        assert_eq!(
1019            chart.indicators().next().unwrap().1.values().value(0, 3),
1020            Some(20.0)
1021        );
1022        assert!(chart.redo());
1023        assert!(chart.indicators().next().is_none());
1024        assert!(chart.undo());
1025        // An ordinary host removal must not make earlier appearance edits unreplayable.
1026        assert!(chart.remove_series(id));
1027        assert!(chart.undo());
1028        assert_eq!(chart.study_ids, [id]);
1029        assert_eq!(
1030            chart.indicators().next().unwrap().1.colors(),
1031            [Color32::from_rgb(29, 0, 0)]
1032        );
1033        assert!(chart.redo());
1034        assert!(chart.indicators().next().is_none());
1035    }
1036
1037    #[test]
1038    fn study_history_restores_visibility_before_an_interval_is_set() {
1039        let mut chart = chart(&[10.0; 4], &[20.0; 4]);
1040        let id = chart.add_indicator(Box::new(SMA::new(2)));
1041        let study = chart.indicator_mut(id).unwrap();
1042        let mut settings = study.settings();
1043        settings.hidden_intervals = vec![Timeframe::Min1];
1044        study.apply_settings(&[], settings).unwrap();
1045        assert!(chart.indicators().next().unwrap().1.is_visible());
1046        chart.user_action(|chart| {
1047            chart
1048                .indicator_mut(id)
1049                .unwrap()
1050                .set_colors(vec![Color32::RED]);
1051            chart.set_timeframe(Timeframe::Min1);
1052        });
1053        for redo in [false, true, false] {
1054            assert!(if redo { chart.redo() } else { chart.undo() });
1055            assert_eq!(chart.timeframe(), redo.then_some(Timeframe::Min1));
1056            assert_eq!(chart.indicators().next().unwrap().1.is_visible(), !redo);
1057        }
1058    }
1059
1060    #[test]
1061    fn study_history_restores_inputs_with_dependent_limits() {
1062        use crate::studies::{AverageLine, AverageType, MACombination};
1063
1064        let mut chart = chart(&[10.0, 14.0, 18.0, 22.0], &[10.0, 14.0, 18.0, 22.0]);
1065        let id = chart.add_indicator(Box::new(
1066            MACombination::new(vec![
1067                AverageLine::new(AverageType::Simple, 1, Color32::RED),
1068                AverageLine::new(AverageType::Simple, 2, Color32::BLUE),
1069            ])
1070            .unwrap(),
1071        ));
1072        chart.user_action(|chart| {
1073            chart
1074                .indicator_mut(id)
1075                .unwrap()
1076                .set_input("kind_0", InputValue::Choice(5))
1077                .unwrap();
1078        });
1079        for redo in [false, true, false, true] {
1080            assert!(if redo { chart.redo() } else { chart.undo() });
1081            let study = chart.indicators().next().unwrap().1;
1082            let input = |id: &str| {
1083                study
1084                    .inputs()
1085                    .into_iter()
1086                    .find(|input| input.id == id)
1087                    .unwrap()
1088                    .value
1089            };
1090            assert_eq!(
1091                input("period_0"),
1092                InputValue::Length(if redo { 2 } else { 1 })
1093            );
1094            assert_eq!(
1095                input("kind_0"),
1096                InputValue::Choice(if redo { 5 } else { 0 })
1097            );
1098            assert_eq!(
1099                study.values().value(0, 3),
1100                Some(if redo { 70.0 / 3.0 } else { 22.0 })
1101            );
1102        }
1103    }
1104
1105    #[test]
1106    fn study_edits_and_bulk_removal_restore_ids_order_and_settings() {
1107        let mut chart = chart(&[10.0; 30], &[20.0; 30]);
1108        chart.set_timeframe(Timeframe::Min1);
1109        let rsi = chart.user_action(|chart| {
1110            let id = chart.add_indicator(Box::new(RSI::new(14)));
1111            chart.set_indicator_inputs(id, "14");
1112            id
1113        });
1114        let sma = chart.user_action(|chart| chart.add_indicator(Box::new(SMA::new(3))));
1115        chart.user_action(|chart| {
1116            let study = chart.indicator_mut(rsi).unwrap();
1117            let mut settings = study.settings();
1118            settings.plots[0].color = Color32::RED;
1119            settings.plots[0].width = 4.0;
1120            settings.hidden_intervals = vec![Timeframe::Min1];
1121            study
1122                .apply_settings(&[("period".into(), InputValue::Length(5))], settings)
1123                .unwrap();
1124            chart.set_indicator_inputs(rsi, "5");
1125            chart.set_indicator_autoscale(rsi, false);
1126        });
1127        assert!(chart.undo());
1128        assert_eq!(chart.indicator_inputs(rsi), Some("14"));
1129        assert_eq!(
1130            chart.indicators().next().unwrap().1.inputs()[0].value,
1131            InputValue::Length(14)
1132        );
1133        assert!(chart.study_autoscale[0]);
1134        assert!(chart.indicators().next().unwrap().1.is_visible());
1135        assert!(chart.redo());
1136        assert_eq!(chart.indicator_inputs(rsi), Some("5"));
1137        assert_eq!(
1138            chart.indicators().next().unwrap().1.settings().plots[0].color,
1139            Color32::RED
1140        );
1141        assert!(!chart.study_autoscale[0]);
1142        assert!(!chart.indicators().next().unwrap().1.is_visible());
1143        chart.set_timeframe(Timeframe::Min15);
1144        assert!(
1145            chart.indicators().next().unwrap().1.is_visible(),
1146            "interval filtering must not overwrite the global visibility toggle"
1147        );
1148        chart.user_action(|chart| assert!(chart.move_indicator_to_pane(sma, rsi)));
1149        assert_eq!(chart.pane_layout.panes[1].members, [rsi, sma]);
1150        assert!(chart.undo());
1151        assert!(
1152            chart
1153                .indicators()
1154                .find(|(id, _)| *id == sma)
1155                .unwrap()
1156                .1
1157                .is_overlay()
1158        );
1159        assert_eq!(chart.pane_layout.panes[1].members, [rsi]);
1160        assert!(chart.redo());
1161        assert_eq!(chart.pane_layout.panes[1].members, [rsi, sma]);
1162        chart.user_action(|chart| {
1163            chart.remove_series(rsi);
1164            chart.remove_series(sma);
1165        });
1166        // A host study added after the action is independent of both sides of its history.
1167        let host = chart.add_indicator(Box::new(SMA::new(7)));
1168        let bars = chart.bars(SeriesId::MAIN)[..17].to_vec();
1169        chart.set_data(SeriesId::MAIN, bars);
1170        for _ in 0..2 {
1171            assert!(chart.undo());
1172            assert_eq!(chart.study_ids, [rsi, sma, host]);
1173            assert_eq!(chart.pane_layout.panes[1].members, [rsi, sma]);
1174            assert_eq!(chart.indicator_inputs(rsi), Some("5"));
1175            assert_eq!(chart.indicators().next().unwrap().1.values().len(), 17);
1176            assert!(chart.redo());
1177            assert_eq!(chart.study_ids, [host]);
1178        }
1179    }
1180
1181    #[test]
1182    fn interleaved_interval_type_and_settings_follow_action_order() {
1183        let mut chart = Chart::new(BarData::new());
1184        chart.set_timeframe(Timeframe::Min1);
1185        let original = chart.config.clone();
1186        chart.user_action(|chart| chart.set_timeframe(Timeframe::Min15));
1187        chart.user_action(|chart| {
1188            chart.config_mut().background_color = Color32::RED;
1189            chart.price_scale_mut().options_mut().invert_scale = true;
1190            chart.chart_options.time_scale.timezone =
1191                crate::scales::TimezoneMode::Timezone("+05:45".parse().unwrap());
1192        });
1193        chart.user_action(|chart| chart.set_timeframe(Timeframe::Hour1));
1194        chart.user_action(|chart| chart.set_chart_type(ChartType::Line));
1195        chart.user_action(|chart| chart.config_mut().show_volume = !original.show_volume);
1196        for redo in [false, true, false] {
1197            let expected = [
1198                (
1199                    Timeframe::Hour1,
1200                    ChartType::Line,
1201                    true,
1202                    original.show_volume,
1203                ),
1204                (
1205                    Timeframe::Hour1,
1206                    ChartType::Candles,
1207                    true,
1208                    original.show_volume,
1209                ),
1210                (
1211                    Timeframe::Min15,
1212                    ChartType::Candles,
1213                    true,
1214                    original.show_volume,
1215                ),
1216                (
1217                    Timeframe::Min15,
1218                    ChartType::Candles,
1219                    false,
1220                    original.show_volume,
1221                ),
1222                (
1223                    Timeframe::Min1,
1224                    ChartType::Candles,
1225                    false,
1226                    original.show_volume,
1227                ),
1228            ];
1229            if redo {
1230                for (tf, kind, red, volume) in [
1231                    (
1232                        Timeframe::Min15,
1233                        ChartType::Candles,
1234                        false,
1235                        original.show_volume,
1236                    ),
1237                    (
1238                        Timeframe::Min15,
1239                        ChartType::Candles,
1240                        true,
1241                        original.show_volume,
1242                    ),
1243                    (
1244                        Timeframe::Hour1,
1245                        ChartType::Candles,
1246                        true,
1247                        original.show_volume,
1248                    ),
1249                    (
1250                        Timeframe::Hour1,
1251                        ChartType::Line,
1252                        true,
1253                        original.show_volume,
1254                    ),
1255                    (
1256                        Timeframe::Hour1,
1257                        ChartType::Line,
1258                        true,
1259                        !original.show_volume,
1260                    ),
1261                ] {
1262                    assert!(chart.redo());
1263                    assert_eq!(chart.timeframe(), Some(tf));
1264                    assert_eq!(chart.chart_type(), kind);
1265                    assert_eq!(chart.config.background_color == Color32::RED, red);
1266                    assert_eq!(chart.config.show_volume, volume);
1267                }
1268            } else {
1269                for (tf, kind, red, volume) in expected {
1270                    assert!(chart.undo());
1271                    assert_eq!(chart.timeframe(), Some(tf));
1272                    assert_eq!(chart.chart_type(), kind);
1273                    assert_eq!(chart.config.background_color == Color32::RED, red);
1274                    assert_eq!(chart.config.show_volume, volume);
1275                    assert_eq!(chart.state.price_scale().options().invert_scale, red);
1276                    assert_eq!(
1277                        chart.chart_options.time_scale.timezone
1278                            == crate::scales::TimezoneMode::Timezone("+05:45".parse().unwrap()),
1279                        red
1280                    );
1281                }
1282                assert!(!chart.can_undo());
1283            }
1284        }
1285    }
1286
1287    #[test]
1288    fn display_options_restore_applied_spacing_but_keep_pan_for_timezone_edits() {
1289        let mut chart = chart(&[10.0], &[20.0]);
1290        chart.apply_synced_time_scale(9.0, -7.0);
1291        let edit_options = |chart: &mut Chart, spacing: Option<f32>| {
1292            let mut options = chart.chart_options().clone();
1293            if let Some(spacing) = spacing {
1294                options.time_scale.bar_spacing = spacing;
1295            }
1296            options.time_scale.timezone =
1297                crate::scales::TimezoneMode::Timezone("+05:45".parse().unwrap());
1298            let before = std::mem::replace(chart, Chart::new(BarData::new()));
1299            *chart = before.with_chart_options(options);
1300        };
1301        chart.user_action(|chart| edit_options(chart, Some(12.0)));
1302        assert_eq!(chart.get_time_scale_state().0, 12.0);
1303        assert!(chart.undo());
1304        assert_eq!(chart.get_time_scale_state().0, 9.0);
1305        assert!(chart.redo());
1306        assert_eq!(chart.get_time_scale_state().0, 12.0);
1307        // A display-only timezone edit uses the same builder but preserves the live view.
1308        chart.chart_options.time_scale.timezone = crate::scales::TimezoneMode::Utc;
1309        chart.user_action(|chart| edit_options(chart, None));
1310        chart.apply_synced_time_scale(15.0, -11.0);
1311        assert!(chart.undo());
1312        assert_eq!(chart.get_time_scale_state(), (15.0, -11.0));
1313        assert!(chart.redo());
1314        assert_eq!(chart.get_time_scale_state(), (15.0, -11.0));
1315    }
1316
1317    #[test]
1318    #[cfg(feature = "drawings")]
1319    fn host_updates_and_unchanged_actions_do_not_enter_or_clear_history() {
1320        let mut chart = Chart::new(BarData::new());
1321        chart.set_timeframe(Timeframe::Min1);
1322        chart.config_mut().show_grid = false;
1323        let host = chart.add_indicator(Box::new(SMA::new(3)));
1324        chart
1325            .drawings_mut()
1326            .add_synced_drawing(host_line())
1327            .unwrap();
1328        assert!(!chart.can_undo());
1329        chart.user_action(|chart| {
1330            chart.user_action(|chart| chart.set_timeframe(Timeframe::Min15));
1331            chart.add_indicator(Box::new(RSI::new(14)));
1332        });
1333        assert!(chart.undo());
1334        assert!(!chart.can_undo(), "nested edits are one user action");
1335        assert_eq!(chart.study_ids, [host]);
1336        assert!(!chart.config.show_grid);
1337        chart.user_action(|chart| {
1338            assert!(
1339                !chart.undo(),
1340                "an open action cannot consume completed history"
1341            );
1342            chart.set_timeframe(Timeframe::Min1);
1343            chart.indicator_mut(host).unwrap().set_overlay(true);
1344            chart.set_indicator_scale(host, crate::studies::StudyScale::Main);
1345            chart
1346                .set_indicator_source(
1347                    host,
1348                    crate::studies::StudySource::Price(crate::model::PriceSource::Close),
1349                )
1350                .unwrap();
1351        });
1352        chart.set_data(SeriesId::MAIN, vec![]);
1353        chart.set_chart_type(ChartType::Area);
1354        assert!(!chart.can_undo());
1355        assert!(chart.can_redo(), "host and unchanged edits retain redo");
1356        assert!(chart.redo());
1357        assert_eq!(chart.chart_type(), ChartType::Area);
1358        assert_eq!(chart.timeframe(), Some(Timeframe::Min15));
1359        assert!(chart.undo());
1360        chart.user_action(|chart| chart.config_mut().show_volume = false);
1361        assert!(
1362            !chart.can_redo(),
1363            "a new user edit clears cross-domain redo"
1364        );
1365    }
1366
1367    #[test]
1368    #[cfg(feature = "drawings")]
1369    fn drawing_snapshots_keep_host_loads_sync_events_and_prepended_coordinates() {
1370        let mut chart = chart(&[10.0, 11.0], &[20.0, 21.0]);
1371        chart.user_action(|chart| {
1372            let mut drawing = Drawing::new(0, DrawingToolType::HorizontalLine);
1373            drawing.chart_points = vec![ChartPoint::new(1.0, 15.0)];
1374            chart.drawings_mut().add_synced_drawing(drawing).unwrap();
1375        });
1376        chart.user_action(|chart| chart.drawings_mut().clear_all());
1377        let mut host = host_line();
1378        host.stored_id = Some("host".into());
1379        chart.drawings_mut().add_synced_drawing(host).unwrap();
1380        chart.drawings.pending_sync.lock().unwrap().clear();
1381        // Prepending real bars remaps commands on both stacks, as it does live drawings.
1382        let prepend = |chart: &mut Chart, count| {
1383            let mut bars = chart.bars(SeriesId::MAIN).to_vec();
1384            let first = bars[0].clone();
1385            for days in 1..=count {
1386                let mut older = first.clone();
1387                older.time -= chrono::Duration::days(days);
1388                bars.push(older);
1389            }
1390            chart.set_data(SeriesId::MAIN, bars);
1391        };
1392        prepend(&mut chart, 3);
1393        assert!(chart.undo());
1394        assert_eq!(chart.drawings.drawings.len(), 2);
1395        assert_eq!(chart.drawings.drawings[0].chart_points[0].bar_idx, 4.0);
1396        assert!(matches!(
1397            chart.drawings.pending_sync.lock().unwrap().as_slice(),
1398            [DrawingSyncAction::SaveDrawing(_)]
1399        ));
1400        assert!(chart.undo());
1401        assert_eq!(chart.drawings.drawings.len(), 1);
1402        assert_eq!(
1403            chart.drawings.drawings[0].stored_id.as_deref(),
1404            Some("host")
1405        );
1406        prepend(&mut chart, 2);
1407        assert!(chart.redo());
1408        assert_eq!(
1409            chart.drawings.drawings.last().unwrap().chart_points[0].bar_idx,
1410            6.0
1411        );
1412        assert!(chart.redo());
1413        assert_eq!(chart.drawings.drawings.len(), 1);
1414        assert_eq!(
1415            chart.drawings.drawings[0].stored_id.as_deref(),
1416            Some("host")
1417        );
1418    }
1419
1420    #[test]
1421    #[cfg(feature = "drawings")]
1422    fn command_z_y_and_shift_z_share_the_chart_history_on_both_platforms() {
1423        for mac in [false, true] {
1424            let ctx = Context::default();
1425            ctx.set_os(if mac {
1426                egui::os::OperatingSystem::Mac
1427            } else {
1428                egui::os::OperatingSystem::Nix
1429            });
1430            let command = Modifiers {
1431                command: true,
1432                mac_cmd: mac,
1433                ctrl: !mac,
1434                ..Modifiers::NONE
1435            };
1436            let mut chart = Chart::new(BarData::new());
1437            chart.user_action(|chart| {
1438                chart
1439                    .drawings_mut()
1440                    .add_synced_drawing(host_line())
1441                    .unwrap()
1442            });
1443            let id = chart.user_action(|chart| chart.add_indicator(Box::new(RSI::new(14))));
1444            for (key, shift, studies, drawings) in [
1445                (Key::Z, false, 0, 1),
1446                (Key::Y, false, 1, 1),
1447                (Key::Z, false, 0, 1),
1448                (Key::Z, true, 1, 1),
1449                (Key::Z, false, 0, 1),
1450                (Key::Z, false, 0, 0),
1451                (Key::Y, false, 0, 1),
1452                (Key::Y, false, 1, 1),
1453            ] {
1454                let modifiers = Modifiers { shift, ..command };
1455                ctx.run_ui(
1456                    RawInput {
1457                        events: vec![
1458                            Event::ModifiersChanged(modifiers),
1459                            Event::Key {
1460                                key,
1461                                physical_key: None,
1462                                pressed: true,
1463                                repeat: false,
1464                                modifiers,
1465                            },
1466                        ],
1467                        ..RawInput::default()
1468                    },
1469                    |ui| chart.dispatch_shortcuts(ui.ctx()),
1470                )
1471                .drop_without_applying_deltas();
1472                assert_eq!(
1473                    chart.indicators().count(),
1474                    studies,
1475                    "mac {mac}, {key:?}, shift {shift}"
1476                );
1477                assert_eq!(chart.drawings().drawings.len(), drawings);
1478                if studies == 1 {
1479                    assert_eq!(chart.indicators().next().unwrap().0, id);
1480                }
1481            }
1482        }
1483    }
1484
1485    /// A user's symbol navigation and comparison edits undo and redo in order, each step finding
1486    /// the drawings, series, scales and symbol it was recorded against, and a removed comparison
1487    /// coming back with its price formatter and bar limit.
1488    #[test]
1489    #[cfg(feature = "drawings")]
1490    fn symbol_navigation_and_comparison_edits_undo_and_redo_in_order() {
1491        use crate::model::SeriesScale;
1492        use crate::scales::PriceScaleMode;
1493        let mut chart = chart(&[10.0, 11.0], &[20.0, 21.0]);
1494        chart.set_symbol("AAA");
1495        let draw = |chart: &mut Chart| {
1496            chart.user_action(|chart| {
1497                chart
1498                    .drawings_mut()
1499                    .add_synced_drawing(host_line())
1500                    .unwrap()
1501            })
1502        };
1503        draw(&mut chart);
1504        chart.user_action(|chart| chart.set_symbol("BBB"));
1505        assert!(chart.drawings().drawings.is_empty());
1506        let bars = chart.bars(SeriesId::MAIN).to_vec();
1507        let mode = chart.state.price_scale().options().mode;
1508        let limit = std::num::NonZeroUsize::new(1000);
1509        let comparison = chart.user_action(|chart| {
1510            let id = chart.add_comparison("CCC", Color32::GOLD);
1511            chart.set_data(id, bars.clone());
1512            chart.set_bar_limit(id, limit);
1513            assert!(chart.set_series_formatter(
1514                id,
1515                Some(std::sync::Arc::new(crate::scales::CurrencyFormatter::usd())),
1516            ));
1517            id
1518        });
1519        chart.user_action(|chart| {
1520            chart.set_series_visible(comparison, false);
1521            chart.set_series_scale(comparison, SeriesScale::Left);
1522        });
1523        chart.user_action(|chart| {
1524            chart.remove_series(comparison);
1525        });
1526        draw(&mut chart);
1527
1528        let series_events = |chart: &mut Chart| {
1529            chart
1530                .drain_events()
1531                .filter(|event| {
1532                    matches!(
1533                        event,
1534                        ChartEvent::SeriesAdded(_) | ChartEvent::SeriesRemoved(_)
1535                    )
1536                })
1537                .collect::<Vec<_>>()
1538        };
1539        // Undo reverses removal then addition; redo follows the original edits.
1540        let added_then_removed = [
1541            ChartEvent::SeriesAdded(comparison),
1542            ChartEvent::SeriesRemoved(comparison),
1543        ];
1544        assert_eq!(series_events(&mut chart), added_then_removed);
1545
1546        let comparison_state = |chart: &Chart| {
1547            (
1548                chart.series_scale(comparison),
1549                chart.is_series_visible(comparison),
1550                chart.bars(comparison).len(),
1551                chart.bar_limit(comparison),
1552                chart
1553                    .series_label_formatter(comparison, PriceScaleMode::Normal)
1554                    .is_some(),
1555            )
1556        };
1557        let absent = (None, false, 0, None, false);
1558        let undo = |chart: &mut Chart| assert!(chart.undo());
1559        undo(&mut chart);
1560        assert_eq!(
1561            (chart.symbol(), chart.drawings().drawings.len()),
1562            ("BBB", 0)
1563        );
1564        undo(&mut chart);
1565        assert_eq!(
1566            comparison_state(&chart),
1567            (Some(SeriesScale::Left), false, bars.len(), limit, true)
1568        );
1569        undo(&mut chart);
1570        assert_eq!(
1571            comparison_state(&chart),
1572            (Some(SeriesScale::Right), true, bars.len(), limit, true)
1573        );
1574        assert_eq!(
1575            chart.state.price_scale().options().mode,
1576            PriceScaleMode::Percentage
1577        );
1578        undo(&mut chart);
1579        assert_eq!(chart.series().count(), 0);
1580        assert_eq!(comparison_state(&chart), absent);
1581        assert_eq!(chart.state.price_scale().options().mode, mode);
1582        undo(&mut chart);
1583        assert_eq!(
1584            (chart.symbol(), chart.drawings().drawings.len()),
1585            ("AAA", 1)
1586        );
1587        undo(&mut chart);
1588        assert!(chart.drawings().drawings.is_empty());
1589        assert!(!chart.can_undo());
1590
1591        assert_eq!(series_events(&mut chart), added_then_removed);
1592        assert!(chart.redo());
1593        assert_eq!(
1594            (chart.symbol(), chart.drawings().drawings.len()),
1595            ("AAA", 1)
1596        );
1597        assert!(chart.redo());
1598        assert_eq!(
1599            (chart.symbol(), chart.drawings().drawings.len()),
1600            ("BBB", 0)
1601        );
1602        assert!(chart.redo());
1603        assert_eq!(
1604            comparison_state(&chart),
1605            (Some(SeriesScale::Right), true, bars.len(), limit, true)
1606        );
1607        assert!(chart.redo());
1608        assert_eq!(
1609            comparison_state(&chart),
1610            (Some(SeriesScale::Left), false, bars.len(), limit, true)
1611        );
1612        assert!(chart.redo());
1613        assert_eq!(chart.series().count(), 0);
1614        assert_eq!(comparison_state(&chart), absent);
1615        assert!(chart.redo());
1616        assert_eq!(
1617            (chart.symbol(), chart.drawings().drawings.len()),
1618            ("BBB", 1)
1619        );
1620        assert!(!chart.can_redo());
1621        assert_eq!(series_events(&mut chart), added_then_removed);
1622    }
1623
1624    #[test]
1625    fn series_alert_styles_stay_distinct_through_undo_and_snapshot() {
1626        let mut chart = chart(&[1.0], &[2.0]);
1627        let other = chart.add_series(crate::widget::series::SeriesOptions::default());
1628        let main_alerts = SeriesAlerts {
1629            visible: false,
1630            labels: false,
1631            color: Color32::RED,
1632        };
1633        let other_alerts = SeriesAlerts {
1634            color: Color32::BLUE,
1635            ..SeriesAlerts::default()
1636        };
1637        chart.user_action(|chart| {
1638            let mut settings = chart.series_settings();
1639            settings.alerts = main_alerts;
1640            chart.apply_series_settings(&settings);
1641            chart.series_style_mut(other).unwrap().alerts = other_alerts;
1642        });
1643        assert_eq!(chart.series_settings().alerts, main_alerts);
1644        assert_eq!(chart.series_style(other).unwrap().alerts, other_alerts);
1645        #[cfg(feature = "ui")]
1646        {
1647            let mut restored = super::super::Chart::new(BarData::default());
1648            restored
1649                .restore(&chart.snapshot().unwrap())
1650                .expect("snapshot restores");
1651            assert_eq!(restored.series_settings().alerts, main_alerts);
1652        }
1653        assert!(chart.undo());
1654        assert_eq!(chart.series_settings().alerts, SeriesAlerts::default());
1655        assert_eq!(
1656            chart.series_style(other).unwrap().alerts,
1657            SeriesAlerts::default()
1658        );
1659        assert!(chart.redo());
1660        assert_eq!(chart.series_settings().alerts, main_alerts);
1661        assert_eq!(chart.series_style(other).unwrap().alerts, other_alerts);
1662    }
1663
1664    /// Undo of the main series' removal, as its legend row's Remove control records it, brings
1665    /// back its 100 bars, its visibility, its marker, its price line, its formatter and its bar
1666    /// limit, and the feed follows the host's pause again: running when the host left it
1667    /// running, also after a replay that began while the series was removed, and paused when
1668    /// the host paused it. Redo removes the series again, and a second undo restores it again.
1669    #[test]
1670    fn undoing_the_main_series_removal_restores_it_and_its_feed() {
1671        struct Feed;
1672        impl crate::data::Datafeed for Feed {
1673            fn history(
1674                &self,
1675                _: crate::data::HistoryRequest,
1676                _: crate::data::Reply<crate::data::HistoryPage>,
1677            ) {
1678            }
1679        }
1680        for (host_paused, replay) in [(false, false), (true, false), (false, true)] {
1681            let mut chart = chart(&[10.0], &[20.0]);
1682            chart.attach_feed(std::sync::Arc::new(Feed), 1);
1683            chart.set_feed_paused(host_paused);
1684            let first = chart.bars(SeriesId::MAIN)[0].time;
1685            let cutoff = chart.bars(SeriesId::MAIN)[50].time;
1686            chart.set_markers(SeriesId::MAIN, vec![crate::model::Marker::new(first)]);
1687            let line = chart
1688                .add_price_line(
1689                    SeriesId::MAIN,
1690                    crate::model::PriceLine::new(15.0, Color32::RED),
1691                )
1692                .unwrap();
1693            let limit = std::num::NonZeroUsize::new(1000);
1694            chart.set_bar_limit(SeriesId::MAIN, limit);
1695            assert!(chart.set_series_formatter(
1696                SeriesId::MAIN,
1697                Some(std::sync::Arc::new(crate::scales::CurrencyFormatter::usd())),
1698            ));
1699            let state = |chart: &Chart| {
1700                (
1701                    chart.bars(SeriesId::MAIN).len(),
1702                    chart.is_main_series_visible(),
1703                    chart.markers(SeriesId::MAIN).len(),
1704                    chart.price_line(line).is_some(),
1705                    chart.bar_limit(SeriesId::MAIN),
1706                    chart
1707                        .series_label_formatter(
1708                            SeriesId::MAIN,
1709                            crate::scales::PriceScaleMode::Normal,
1710                        )
1711                        .is_some(),
1712                    chart.feed_link.as_ref().unwrap().paused,
1713                )
1714            };
1715            let restored = (100, true, 1, true, limit, true, host_paused);
1716            let removed = (0, false, 0, false, None, false, true);
1717            let case = format!("host paused {host_paused}, replay {replay}");
1718            assert_eq!(state(&chart), restored, "{case}: before");
1719            assert!(chart.user_action(|chart| chart.remove_series(SeriesId::MAIN)));
1720            assert_eq!(state(&chart), removed, "{case}: removed");
1721            if replay {
1722                chart.set_replay_time(Some(cutoff));
1723            }
1724            assert!(chart.undo());
1725            if replay {
1726                assert!(
1727                    chart.feed_link.as_ref().unwrap().paused,
1728                    "{case}: in replay"
1729                );
1730                chart.set_replay_time(None);
1731            }
1732            assert_eq!(state(&chart), restored, "{case}: undone");
1733            assert!(chart.redo());
1734            assert_eq!(state(&chart), removed, "{case}: redone");
1735            assert!(chart.undo());
1736            assert_eq!(state(&chart), restored, "{case}: undone again");
1737        }
1738    }
1739}