1use 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 pub(super) edits: u64,
22 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 removed_series: Vec<HeldSeries>,
34}
35
36#[derive(Default)]
37struct Action {
38 symbol: Option<String>,
39 series: Option<SeriesSet>,
40 config: Option<ChartConfig>,
41 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 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
82struct 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
113struct HeldSeries {
116 series: PriceSeries,
117 formatter: Option<std::sync::Arc<dyn crate::scales::PriceFormatter>>,
118 bar_limit: Option<std::num::NonZeroUsize>,
119}
120
121#[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 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 pub(super) fn recording(&self) -> bool {
165 self.pending.is_some()
166 }
167
168 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 && (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 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 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 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 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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 pub fn can_undo(&self) -> bool {
658 !self.history.undo.is_empty()
659 }
660
661 pub fn can_redo(&self) -> bool {
663 !self.history.redo.is_empty()
664 }
665
666 pub fn undo(&mut self) -> bool {
669 self.step_chart_history(false)
670 }
671
672 pub fn redo(&mut self) -> bool {
675 self.step_chart_history(true)
676 }
677
678 pub fn take_history_changed(&mut self) -> bool {
684 std::mem::take(&mut self.history.changed)
685 }
686
687 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 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 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 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 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 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 #[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 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 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 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 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 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 #[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 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 #[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}