1use super::keyboard::ShortcutRequest;
4use super::{Chart, VisibleTimeRange};
5use crate::config::KeyboardAction;
6use crate::localization as l10n;
7use crate::model::Timeframe;
8use crate::theme::ThemeExt;
9use egui::{Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2};
10
11#[cfg(feature = "drawings")]
12mod drawings;
13#[cfg(feature = "drawings")]
14use drawings::DrawingLinks;
15
16#[derive(
20 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
21)]
22pub enum ChartLayoutMode {
23 #[default]
25 Single,
26 TwoHorizontal,
28 TwoVertical,
30 Three,
32 ThreeHorizontal,
34 ThreeVertical,
36 ThreeLargeLeft,
38 ThreeLargeRight,
40 ThreeLargeTop,
43 Four,
45 FourHorizontal,
47 FourVertical,
49 FourLargeLeft,
51 FourLargeRight,
53 FourLargeTop,
55 FourLargeBottom,
57 Five,
59 FiveHorizontal,
61 FiveVertical,
63 FiveLargeLeft,
65 FiveLargeRight,
67 FiveLargeTop,
69 FiveLargeBottom,
71 Six,
73 SixHorizontal,
75 SixVertical,
77 SixLargeLeft,
79 SixLargeRight,
81 SixLargeTop,
83 SixLargeBottom,
85 Seven,
87 Eight,
89 Nine,
91 Ten,
93 Eleven,
95 Twelve,
97 Thirteen,
99 Fourteen,
101 Fifteen,
103 Sixteen,
105 Tabs,
107}
108
109impl ChartLayoutMode {
110 pub fn all() -> &'static [Self] {
112 &[
113 Self::Single,
114 Self::TwoHorizontal,
115 Self::TwoVertical,
116 Self::Three,
117 Self::ThreeHorizontal,
118 Self::ThreeVertical,
119 Self::ThreeLargeLeft,
120 Self::ThreeLargeRight,
121 Self::ThreeLargeTop,
122 Self::Four,
123 Self::FourHorizontal,
124 Self::FourVertical,
125 Self::FourLargeLeft,
126 Self::FourLargeRight,
127 Self::FourLargeTop,
128 Self::FourLargeBottom,
129 Self::Five,
130 Self::FiveHorizontal,
131 Self::FiveVertical,
132 Self::FiveLargeLeft,
133 Self::FiveLargeRight,
134 Self::FiveLargeTop,
135 Self::FiveLargeBottom,
136 Self::Six,
137 Self::SixHorizontal,
138 Self::SixVertical,
139 Self::SixLargeLeft,
140 Self::SixLargeRight,
141 Self::SixLargeTop,
142 Self::SixLargeBottom,
143 Self::Seven,
144 Self::Eight,
145 Self::Nine,
146 Self::Ten,
147 Self::Eleven,
148 Self::Twelve,
149 Self::Thirteen,
150 Self::Fourteen,
151 Self::Fifteen,
152 Self::Sixteen,
153 Self::Tabs,
154 ]
155 }
156
157 pub fn label(&self) -> &'static str {
159 match self {
160 Self::Single => "Single",
161 Self::TwoHorizontal => "2 Horizontal",
162 Self::TwoVertical => "2 Vertical",
163 Self::Three => "2 Top, 1 Bottom",
164 Self::ThreeHorizontal => "3 Horizontal",
165 Self::ThreeVertical => "3 Vertical",
166 Self::ThreeLargeLeft => "1 Left, 2 Right",
167 Self::ThreeLargeRight => "2 Left, 1 Right",
168 Self::ThreeLargeTop => "1 Top, 2 Bottom",
169 Self::Four => "2x2 Grid",
170 Self::FourHorizontal => "4 Horizontal",
171 Self::FourVertical => "4 Vertical",
172 Self::FourLargeLeft => "1 Left, 3 Right",
173 Self::FourLargeRight => "3 Left, 1 Right",
174 Self::FourLargeTop => "1 Top, 3 Bottom",
175 Self::FourLargeBottom => "3 Top, 1 Bottom",
176 Self::Five => "3 Top, 2 Bottom",
177 Self::FiveHorizontal => "5 Horizontal",
178 Self::FiveVertical => "5 Vertical",
179 Self::FiveLargeLeft => "1 Left, 4 Right",
180 Self::FiveLargeRight => "4 Left, 1 Right",
181 Self::FiveLargeTop => "1 Top, 4 Bottom",
182 Self::FiveLargeBottom => "4 Top, 1 Bottom",
183 Self::Six => "3x2 Grid",
184 Self::SixHorizontal => "6 Horizontal",
185 Self::SixVertical => "6 Vertical",
186 Self::SixLargeLeft => "1 Left, 5 Right",
187 Self::SixLargeRight => "5 Left, 1 Right",
188 Self::SixLargeTop => "1 Top, 5 Bottom",
189 Self::SixLargeBottom => "5 Top, 1 Bottom",
190 Self::Seven => "7 Charts",
191 Self::Eight => "4x2 Grid",
192 Self::Nine => "3x3 Grid",
193 Self::Ten => "10 Charts",
194 Self::Eleven => "11 Charts",
195 Self::Twelve => "4x3 Grid",
196 Self::Thirteen => "13 Charts",
197 Self::Fourteen => "14 Charts",
198 Self::Fifteen => "15 Charts",
199 Self::Sixteen => "4x4 Grid",
200 Self::Tabs => "Tabs",
201 }
202 }
203
204 pub fn chart_count(&self) -> usize {
206 if *self == Self::Tabs {
207 return 16;
208 }
209 let (Tracks::Rows(cells) | Tracks::Columns(cells)) = self.tracks();
210 cells.iter().sum()
211 }
212
213 pub fn grid_size(&self) -> (usize, usize) {
216 match self.tracks() {
217 Tracks::Rows(cells) => (cells.iter().copied().max().unwrap_or(1), cells.len()),
218 Tracks::Columns(cells) => (cells.len(), cells.iter().copied().max().unwrap_or(1)),
219 }
220 }
221
222 fn tracks(&self) -> Tracks {
224 use Tracks::{Columns, Rows};
225 match self {
226 Self::Single | Self::Tabs => Rows(&[1]),
227 Self::TwoHorizontal => Rows(&[2]),
228 Self::TwoVertical => Rows(&[1, 1]),
229 Self::Three => Rows(&[2, 1]),
230 Self::ThreeHorizontal => Rows(&[3]),
231 Self::ThreeVertical => Rows(&[1, 1, 1]),
232 Self::ThreeLargeLeft => Columns(&[1, 2]),
233 Self::ThreeLargeRight => Columns(&[2, 1]),
234 Self::ThreeLargeTop => Rows(&[1, 2]),
235 Self::Four => Rows(&[2, 2]),
236 Self::FourHorizontal => Rows(&[4]),
237 Self::FourVertical => Rows(&[1, 1, 1, 1]),
238 Self::FourLargeLeft => Columns(&[1, 3]),
239 Self::FourLargeRight => Columns(&[3, 1]),
240 Self::FourLargeTop => Rows(&[1, 3]),
241 Self::FourLargeBottom => Rows(&[3, 1]),
242 Self::Five => Rows(&[3, 2]),
243 Self::FiveHorizontal => Rows(&[5]),
244 Self::FiveVertical => Rows(&[1, 1, 1, 1, 1]),
245 Self::FiveLargeLeft => Columns(&[1, 4]),
246 Self::FiveLargeRight => Columns(&[4, 1]),
247 Self::FiveLargeTop => Rows(&[1, 4]),
248 Self::FiveLargeBottom => Rows(&[4, 1]),
249 Self::Six => Rows(&[3, 3]),
250 Self::SixHorizontal => Rows(&[6]),
251 Self::SixVertical => Rows(&[1, 1, 1, 1, 1, 1]),
252 Self::SixLargeLeft => Columns(&[1, 5]),
253 Self::SixLargeRight => Columns(&[5, 1]),
254 Self::SixLargeTop => Rows(&[1, 5]),
255 Self::SixLargeBottom => Rows(&[5, 1]),
256 Self::Seven => Rows(&[4, 3]),
257 Self::Eight => Rows(&[4, 4]),
258 Self::Nine => Rows(&[3, 3, 3]),
259 Self::Ten => Rows(&[4, 4, 2]),
260 Self::Eleven => Rows(&[4, 4, 3]),
261 Self::Twelve => Rows(&[4, 4, 4]),
262 Self::Thirteen => Rows(&[4, 4, 4, 1]),
263 Self::Fourteen => Rows(&[4, 4, 4, 2]),
264 Self::Fifteen => Rows(&[4, 4, 4, 3]),
265 Self::Sixteen => Rows(&[4, 4, 4, 4]),
266 }
267 }
268}
269
270impl std::fmt::Display for ChartLayoutMode {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 f.write_str(self.label())
273 }
274}
275
276enum Tracks {
279 Rows(&'static [usize]),
280 Columns(&'static [usize]),
281}
282
283#[derive(Debug, Clone, PartialEq)]
285pub struct ChartSyncOptions {
286 pub sync_time_axis: bool,
288 pub sync_crosshair: bool,
290 pub sync_drawings: bool,
296 pub sync_symbol: bool,
298 pub sync_timeframe: bool,
300 pub sync_date_range: bool,
302}
303
304impl Default for ChartSyncOptions {
305 fn default() -> Self {
306 Self {
307 sync_time_axis: true,
308 sync_crosshair: true,
309 sync_drawings: false,
310 sync_symbol: false,
311 sync_timeframe: false,
312 sync_date_range: false,
313 }
314 }
315}
316
317#[derive(Clone, Default)]
337pub struct ChartLayout {
338 pub mode: ChartLayoutMode,
340 pub sync: ChartSyncOptions,
342 active: usize,
343 maximized: Option<usize>,
345 focus_active: bool,
346 groups: [Option<String>; 16],
347 detached: [bool; 16],
348 previous: [Option<(String, Option<Timeframe>)>; 16],
349 previous_ranges: [Option<VisibleTimeRange>; 16],
350 previous_bars: [BarsKey; 16],
351 pending_ranges: [Option<PendingRange>; 16],
352 #[cfg(feature = "drawings")]
353 drawings: DrawingLinks,
354 previous_preset: Option<(ChartLayoutMode, usize)>,
355}
356
357type BarsKey = (u64, Option<chrono::DateTime<chrono::Utc>>);
359
360fn bars_key(chart: &Chart) -> BarsKey {
361 (
362 chart.state.revision(),
363 chart.state.shown().first().map(|bar| bar.time),
364 )
365}
366
367#[derive(Clone, Copy)]
369struct PendingRange {
370 range: VisibleTimeRange,
371 source: Option<(Timeframe, f64)>,
373 bars: BarsKey,
374}
375
376impl ChartLayout {
377 pub fn active(&self) -> usize {
379 self.active
380 }
381
382 pub fn set_active(&mut self, index: usize) {
384 if index < 16 {
385 self.active = index;
386 self.focus_active = true;
387 }
388 }
389
390 pub fn set_sync_group(&mut self, index: usize, group: Option<String>) {
392 if let Some(slot) = self.groups.get_mut(index) {
393 *slot = group;
394 }
395 }
396
397 pub fn sync_group(&self, index: usize) -> Option<&str> {
399 self.groups.get(index).and_then(|g| g.as_deref())
400 }
401
402 pub fn set_detached(&mut self, index: usize, detached: bool) {
405 if let Some(slot) = self.detached.get_mut(index) {
406 *slot = detached;
407 }
408 }
409
410 pub fn is_detached(&self, index: usize) -> bool {
412 self.detached.get(index).copied().unwrap_or(false)
413 }
414
415 pub fn apply_date_range(&self, charts: &mut [&mut Chart], range: super::VisibleTimeRange) {
418 let Some(active) = charts.get_mut(self.active) else {
419 return;
420 };
421 let source = active
422 .fit_time_range(range, f64::INFINITY)
423 .then(|| active.sync_source())
424 .flatten();
425 for (index, chart) in charts.iter_mut().take(16).enumerate() {
426 if index != self.active
427 && self.sync.sync_date_range
428 && self.same_group(index, self.active)
429 {
430 chart.apply_synced_time_range_from(range, source);
431 }
432 }
433 }
434
435 fn same_group(&self, a: usize, b: usize) -> bool {
436 self.groups[a] == self.groups[b]
437 }
438
439 pub fn show(&mut self, ui: &mut Ui, charts: &mut [&mut Chart]) -> Response {
445 let total = charts.len().min(16);
446 let charts = &mut charts[..total];
447 let count = total.min(self.mode.chart_count());
448 if count == 0 {
449 return ui.allocate_response(Vec2::ZERO, Sense::hover());
450 }
451 self.active = self.active.min(count - 1);
452 if self.maximized.is_some_and(|index| index >= count) {
453 self.maximized = None;
454 }
455 if let Some(index) = self.maximized {
456 self.active = index;
457 }
458 if self.mode == ChartLayoutMode::Tabs {
459 egui::ScrollArea::horizontal().show(ui, |ui| {
460 ui.horizontal(|ui| {
461 for (index, chart) in charts.iter().take(count).enumerate() {
462 if ui
463 .selectable_label(index == self.active, chart.symbol())
464 .clicked()
465 {
466 self.active = index;
467 }
468 }
469 });
470 });
471 }
472 let (rect, response) = ui.allocate_exact_size(ui.available_size(), Sense::hover());
473 let rects = self.rects(rect, count);
474 ui.painter()
475 .rect_filled(rect, 0.0, ui.active_theme().ui.border);
476 let wheel = super::pan_zoom::raw_wheel_delta(ui) != Vec2::ZERO;
478 if self.mode != ChartLayoutMode::Tabs
479 && let Some(index) = rects.iter().position(|r| {
480 ui.rect_contains_pointer(*r) && ui.input(|i| i.pointer.any_pressed() || wheel)
481 })
482 {
483 self.active = index;
484 }
485 self.sync_state(charts, total);
486 for (index, chart) in charts.iter().enumerate() {
487 if !self.sync.sync_time_axis || total == 1 {
488 self.pending_ranges[index] = None;
489 } else if self.previous[index]
490 .as_ref()
491 .is_some_and(|p| p.1 != chart.timeframe())
492 {
493 let own = self.previous_ranges[index].map(|range| PendingRange {
494 range,
495 source: None,
496 bars: self.previous_bars[index],
497 });
498 self.pending_ranges[index] = self.pending_ranges[index].or(own);
499 }
500 }
501 for chart in charts.iter_mut() {
502 chart.last_hover_bar_idx = None;
503 }
504 let mut pointer_down = ui.input(|i| i.pointer.any_down());
505 let before_range = charts[self.active].visible_time_range();
506 let before_sync: Vec<_> = charts
507 .iter()
508 .map(|c| (c.synced_crosshair_bar_idx, c.get_time_scale_state()))
509 .collect();
510 for index in 0..count {
511 if self.mode == ChartLayoutMode::Tabs && index != self.active && !self.detached[index] {
512 continue;
513 }
514 if self.maximized.is_some_and(|slot| slot != index) && !self.detached[index] {
515 continue;
516 }
517 charts[index].layout_inactive =
518 count > 1 && index != self.active && !self.detached[index];
519 let focus = self.focus_active && index == self.active;
520 if self.detached[index] {
521 let id = egui::ViewportId::from_hash_of((ui.scope_id(), "detached_chart", index));
522 let chart = &mut charts[index];
523 let mut close = false;
524 let mut activate = false;
525 ui.ctx().show_viewport_immediate(
526 id,
527 egui::ViewportBuilder::default()
528 .with_title(chart.symbol())
529 .with_inner_size([900.0, 600.0]),
530 |ui, _| {
531 pointer_down |= ui.input(|i| i.pointer.any_down());
532 close |= ui.input(|i| i.viewport().close_requested());
533 if ui
534 .button(l10n::text(ui.ctx(), "Return to layout"))
535 .clicked()
536 {
537 close = true;
538 }
539 egui::CentralPanel::default().show(ui, |ui| {
540 let response = chart.show(ui);
541 if focus {
542 response.request_focus();
543 }
544 activate |= response.is_pointer_button_down_on()
545 || response.clicked()
546 || (response.hovered()
547 && super::pan_zoom::raw_wheel_delta(ui) != Vec2::ZERO);
548 });
549 },
550 );
551 self.restore_time_range(index, chart);
552 self.detached[index] = !close;
553 if activate {
554 self.active = index;
555 }
556 if (self.mode != ChartLayoutMode::Tabs || index == self.active)
557 && self.maximized.is_none_or(|slot| slot == index)
558 {
559 ui.scope_builder(egui::UiBuilder::new().max_rect(rects[index]), |ui| {
560 if ui
561 .button(l10n::text(ui.ctx(), "Return chart to layout"))
562 .clicked()
563 {
564 self.detached[index] = false;
565 }
566 });
567 }
568 continue;
569 }
570 let rect = rects[index];
571 ui.painter()
572 .rect_filled(rect, 0.0, charts[index].config().background_color);
573 ui.scope_builder(
574 egui::UiBuilder::new()
575 .max_rect(rect)
576 .id_salt(("chart_slot", index)),
577 |ui| {
578 ui.shrink_clip_rect(rect);
579 let response = charts[index].show(ui);
580 charts[index].layout_inactive = false;
581 if focus {
582 response.request_focus();
583 }
584 if index == self.active
585 && (ui.rect_contains_pointer(rect) || egui::Popup::is_any_open(ui.ctx()))
586 && charts[index]
587 .pane_layout
588 .maximized
589 .is_none_or(|pane| pane == super::PaneId::MAIN)
590 {
591 let button = ui.put(
592 Rect::from_min_size(
593 rect.min
594 + Vec2::new(
595 (rect.width()
596 - if rect.width() < 600.0 { 22.0 } else { 100.0 })
597 .max(0.0),
598 2.0,
599 ),
600 Vec2::splat(20.0),
601 ),
602 egui::Button::new("…").frame(false),
603 );
604 egui::Popup::menu(&button)
605 .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
606 .show(|ui| {
607 if ui.button(l10n::text(ui.ctx(), "Detach chart")).clicked() {
608 self.detached[index] = true;
609 ui.close();
610 }
611 ui.label(l10n::text(ui.ctx(), "Sync group"));
612 let mut group = self.groups[index].clone().unwrap_or_default();
613 if ui.text_edit_singleline(&mut group).changed() {
614 self.groups[index] = (!group.is_empty()).then_some(group);
615 }
616 });
617 }
618 },
619 );
620 self.restore_time_range(index, charts[index]);
621 if count > 1 && self.maximized.is_none() && index == self.active {
622 ui.painter().rect_stroke(
623 rect,
624 0.0,
625 Stroke::new(2.0, ui.active_theme().ui.accent),
626 StrokeKind::Inside,
627 );
628 }
629 }
630 self.focus_active = false;
631 if self.mode != ChartLayoutMode::Tabs && count > 1 {
633 for (index, chart) in charts.iter_mut().take(count).enumerate() {
634 let requests = &mut chart.keyboard.requests;
635 let before = requests.len();
636 requests.retain(|request| {
637 *request != ShortcutRequest::Action(KeyboardAction::MaximizeChart)
638 });
639 if requests.len() != before && !ui.ctx().will_discard() {
641 self.maximized = if self.maximized == Some(index) {
642 None
643 } else {
644 Some(index)
645 };
646 self.set_active(index);
647 ui.ctx().request_repaint();
648 }
649 }
650 }
651 let sync_range = charts[self.active].visible_time_range() != before_range
653 || self.previous_preset != Some((self.mode, count));
654 self.sync_view(charts, total, sync_range);
655 self.previous_preset = Some((self.mode, count));
656 if charts
657 .iter()
658 .zip(before_sync)
659 .any(|(chart, (crosshair, (spacing, offset)))| {
660 chart.synced_crosshair_bar_idx != crosshair
661 || (chart.get_time_scale_state().0 - spacing).abs() > 0.001
662 || (chart.get_time_scale_state().1 - offset).abs() > 0.001
663 })
664 {
665 ui.ctx().request_repaint();
666 }
667 #[cfg(feature = "drawings")]
668 if self.sync.sync_drawings
669 && self
670 .drawings
671 .sync(charts, total, &self.groups, pointer_down)
672 {
673 ui.ctx().request_repaint();
674 }
675 #[cfg(feature = "drawings")]
676 if Chart::share_stored_drawings(charts) {
677 ui.ctx().request_repaint();
678 }
679 for (index, chart) in charts.iter().enumerate() {
680 self.previous[index] = Some((chart.symbol().to_owned(), chart.timeframe()));
681 self.previous_bars[index] = bars_key(chart);
682 if self.pending_ranges[index].is_none()
683 && let Some(range) = chart.visible_time_range()
684 {
685 self.previous_ranges[index] = Some(range);
686 }
687 }
688 response
689 }
690
691 fn restore_time_range(&mut self, index: usize, chart: &mut Chart) {
692 if let Some(pending) = self.pending_ranges[index]
695 && chart.visible_time_range().is_some()
696 && bars_key(chart) != pending.bars
697 {
698 chart.apply_synced_time_range_from(pending.range, pending.source);
700 self.pending_ranges[index] = None;
701 }
702 }
703
704 fn sync_state(&mut self, charts: &mut [&mut Chart], count: usize) {
705 let source = &charts[self.active];
706 let symbol = source.symbol().to_owned();
707 let timeframe = source.timeframe();
708 let previous = self.previous[self.active].as_ref();
709 let symbol_changed = previous.is_some_and(|p| p.0 != symbol);
710 let timeframe_changed = previous.is_some_and(|p| p.1 != timeframe);
711 for (index, chart) in charts.iter_mut().take(count).enumerate() {
712 if index == self.active || !self.same_group(index, self.active) {
713 continue;
714 }
715 if self.sync.sync_symbol && symbol_changed {
716 chart.set_symbol(&symbol);
717 }
718 if self.sync.sync_timeframe
719 && timeframe_changed
720 && let Some(tf) = timeframe
721 {
722 chart.set_timeframe(tf);
723 }
724 }
725 }
726
727 fn sync_view(&mut self, charts: &mut [&mut Chart], count: usize, sync_range: bool) {
728 let hovered = charts
729 .iter()
730 .take(count)
731 .position(|c| c.hover_time().is_some());
732 let period = hovered.and_then(|source| charts[source].hover_period());
733 let price = hovered.and_then(|source| charts[source].last_crosshair?.1);
735 let range = charts[self.active].visible_time_range();
736 let source = charts[self.active].sync_source();
737 for (index, chart) in charts.iter_mut().take(count).enumerate() {
738 chart.set_synced_crosshair(None, None);
739 if self.sync.sync_crosshair
740 && let Some(source) = hovered
741 && index != source
742 && self.same_group(index, source)
743 {
744 chart.set_synced_crosshair(period, price);
745 }
746 if index != self.active
747 && self.same_group(index, self.active)
748 && self.sync.sync_time_axis
749 && self.pending_ranges[self.active].is_none()
750 && sync_range
751 && let Some(range) = range
752 {
753 match &mut self.pending_ranges[index] {
755 Some(pending) => {
756 pending.range = range;
757 pending.source = source;
758 }
759 None => {
760 chart.apply_synced_time_range_from(range, source);
761 }
762 }
763 }
764 }
765 }
766
767 pub fn visible_chart_rects(&self, rect: Rect, count: usize) -> Vec<(usize, Rect)> {
771 let count = count.min(16).min(self.mode.chart_count());
772 self.rects(rect, count)
773 .into_iter()
774 .enumerate()
775 .filter(|(slot, rect)| {
776 !self.is_detached(*slot)
777 && rect.is_positive()
778 && (self.mode != ChartLayoutMode::Tabs
779 || *slot == self.active.min(count.saturating_sub(1)))
780 })
781 .collect()
782 }
783
784 fn rects(&self, rect: Rect, count: usize) -> Vec<Rect> {
785 if self.mode == ChartLayoutMode::Tabs {
786 return vec![rect; count];
787 }
788 if let Some(slot) = self.maximized {
789 return (0..count)
790 .map(|index| if index == slot { rect } else { Rect::NOTHING })
791 .collect();
792 }
793 let (by_rows, tracks) = match self.mode.tracks() {
794 Tracks::Rows(tracks) => (true, tracks),
795 Tracks::Columns(tracks) => (false, tracks),
796 };
797 let tracks: Vec<usize> = tracks
799 .iter()
800 .scan(count, |left, &cells| {
801 let cells = cells.min(*left);
802 *left -= cells;
803 (cells > 0).then_some(cells)
804 })
805 .collect();
806 let mut rects = Vec::with_capacity(count);
807 for (track, &cells) in tracks.iter().enumerate() {
808 for cell in 0..cells {
809 let (column, columns, row, rows) = if by_rows {
810 (cell, cells, track, tracks.len())
811 } else {
812 (track, tracks.len(), cell, cells)
813 };
814 let width = rect.width() / columns as f32;
815 let height = ((rect.height() - 2.0 * (rows - 1) as f32) / rows as f32).max(0.0);
816 rects.push(Rect::from_min_size(
817 rect.min + Vec2::new(column as f32 * width, row as f32 * (height + 2.0)),
818 Vec2::new(width, height),
819 ));
820 }
821 }
822 rects
823 }
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829 use crate::model::{Bar, BarData};
830 use chrono::{DateTime, Duration};
831 use egui::{Event, PointerButton, Pos2, RawInput, pos2, vec2};
832
833 fn chart(step: i64) -> Chart {
834 let start = DateTime::from_timestamp(1_700_000_000, 0).unwrap();
835 let mut chart = Chart::new(BarData::from_bars(
836 (0..100)
837 .map(|i| {
838 Bar::new(
839 start + Duration::seconds(i * step),
840 10.0,
841 11.0,
842 9.0,
843 10.0,
844 1.0,
845 )
846 })
847 .collect(),
848 ));
849 chart.set_symbol("SAMPLE");
850 chart.set_timeframe(Timeframe::Min1);
851 chart
852 }
853
854 #[cfg(feature = "icons")]
856 #[test]
857 fn scale_settings_follow_layout_activity_and_hover() {
858 for (mode, maximized, hovered, expected) in [
859 (ChartLayoutMode::Single, None, false, 1),
860 (ChartLayoutMode::Four, None, false, 1),
861 (ChartLayoutMode::Four, None, true, 2),
862 (ChartLayoutMode::Tabs, None, false, 1),
863 (ChartLayoutMode::Four, Some(2), false, 1),
864 ] {
865 let ctx = egui::Context::default();
866 ctx.enable_accesskit();
867 let mut charts = [chart(60), chart(60), chart(60), chart(60)];
868 let mut layout = ChartLayout {
869 mode,
870 maximized,
871 ..Default::default()
872 };
873 for _ in 0..2 {
874 let mut output = ctx.run_ui(
875 RawInput {
876 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 800.0))),
877 events: if hovered {
878 vec![Event::PointerMoved(pos2(900.0, 200.0))]
879 } else {
880 vec![Event::PointerGone]
881 },
882 ..Default::default()
883 },
884 |ui| {
885 layout.show(ui, &mut charts.iter_mut().collect::<Vec<_>>());
886 },
887 );
888 output.textures_delta.clear();
889 let gears = output
890 .platform_output
891 .accesskit_update
892 .as_ref()
893 .unwrap()
894 .nodes
895 .iter()
896 .filter(|(_, node)| node.label() == Some("Scale settings"))
897 .count();
898 assert_eq!(
899 gears, expected,
900 "{mode:?}, maximized={maximized:?}, hovered={hovered}"
901 );
902 output.drop_without_applying_deltas();
903 }
904 }
905 }
906
907 #[test]
908 fn default_syncs_time_axis_and_crosshair_but_not_drawings() {
909 let options = ChartSyncOptions::default();
910 assert!(options.sync_time_axis);
911 assert!(options.sync_crosshair);
912 assert!(!options.sync_drawings);
913 }
914
915 #[test]
916 fn presets_preserve_chart_state_and_route_input_to_visible_slots() {
917 let rect = Rect::from_min_size(pos2(54.0, 70.0), vec2(1386.0, 716.0));
918 let mut labels = std::collections::HashSet::new();
919 for &mode in ChartLayoutMode::all() {
920 assert!(labels.insert(mode.label()), "{mode:?}");
921 let layout = ChartLayout {
922 mode,
923 ..Default::default()
924 };
925 let rects = layout.rects(rect, mode.chart_count());
926 assert_eq!(rects.len(), mode.chart_count());
927 if mode == ChartLayoutMode::Tabs {
928 continue;
929 }
930 let area: f32 = rects.iter().map(Rect::area).sum();
931 let gaps: f32 = rects
933 .iter()
934 .filter(|cell| cell.bottom() < rect.bottom() - 1.0)
935 .map(|cell| 2.0 * cell.width())
936 .sum();
937 assert!((area + gaps - rect.area()).abs() < 1.0, "{mode:?}");
938 for (index, cell) in rects.iter().enumerate() {
939 assert!(rect.contains_rect(*cell));
940 assert!(
941 rects[..index]
942 .iter()
943 .all(|other| other.intersect(*cell).area() == 0.0)
944 );
945 }
946 if mode == ChartLayoutMode::Four {
947 assert_eq!(rects[0], Rect::from_min_size(rect.min, vec2(693.0, 357.0)));
948 assert_eq!(rects[2].top(), 429.0);
949 assert_eq!(rects[1].left(), 747.0);
950 }
951 if mode == ChartLayoutMode::ThreeLargeLeft {
952 assert_eq!(
953 rects,
954 [
955 Rect::from_min_size(rect.min, vec2(693.0, 716.0)),
956 Rect::from_min_size(pos2(747.0, 70.0), vec2(693.0, 357.0)),
957 Rect::from_min_size(pos2(747.0, 429.0), vec2(693.0, 357.0)),
958 ]
959 );
960 }
961 }
962 let sixteen = ChartLayout {
964 mode: ChartLayoutMode::Sixteen,
965 ..Default::default()
966 };
967 assert_eq!(
968 sixteen.rects(rect, 6)[5],
969 Rect::from_min_size(pos2(747.0, 429.0), vec2(693.0, 357.0))
970 );
971 let ctx = egui::Context::default();
972 for width in [80.0, 240.0] {
973 let clip = Rect::from_min_size(Pos2::ZERO, vec2(width, 600.0));
974 let output = ctx.run_ui(RawInput::default(), |ui| {
975 ui.set_clip_rect(clip);
976 ChartLayout::default().show(ui, &mut [&mut chart(60)]);
977 });
978 let clipped = output.shapes.iter().all(|shape| {
979 !matches!(shape.shape, egui::Shape::Text(_)) || clip.contains_rect(shape.clip_rect)
980 });
981 output.drop_without_applying_deltas();
982 assert!(clipped, "a chart must respect its host's clip");
983 }
984 let mut charts = [chart(60), chart(120)];
985 for chart in &mut charts {
986 chart.set_symbol("LONG_SYMBOL_".repeat(12));
987 }
988 let mut layout = ChartLayout {
989 mode: ChartLayoutMode::Tabs,
990 ..Default::default()
991 };
992 layout.set_active(1);
993 let frame = |layout: &mut ChartLayout, charts: &mut [Chart; 2], events| {
994 let output = ctx.run_ui(
995 RawInput {
996 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(640.0, 900.0))),
997 events,
998 ..Default::default()
999 },
1000 |ui| {
1001 layout.show(ui, &mut charts.iter_mut().collect::<Vec<_>>());
1002 assert!(
1003 ui.min_rect().right() <= 640.0,
1004 "long chart tabs must scroll inside the container"
1005 );
1006 },
1007 );
1008 let group = output.shapes.iter().find_map(|shape| match &shape.shape {
1009 egui::Shape::Text(text) if text.galley.text() == "Sync group" => {
1010 Some(text.pos + text.galley.size() / 2.0)
1011 }
1012 _ => None,
1013 });
1014 output.drop_without_applying_deltas();
1015 group
1016 };
1017 frame(&mut layout, &mut charts, vec![]);
1018 let point = charts[1].get_rendered_price_rect().center();
1019 frame(
1020 &mut layout,
1021 &mut charts,
1022 vec![
1023 Event::PointerMoved(point),
1024 Event::PointerButton {
1025 pos: point,
1026 button: PointerButton::Primary,
1027 pressed: true,
1028 modifiers: Default::default(),
1029 },
1030 ],
1031 );
1032 assert_eq!(
1033 layout.active(),
1034 1,
1035 "clicking the tab's plot must not activate slot zero"
1036 );
1037 frame(
1038 &mut layout,
1039 &mut charts,
1040 vec![Event::PointerButton {
1041 pos: point,
1042 button: PointerButton::Primary,
1043 pressed: false,
1044 modifiers: Default::default(),
1045 }],
1046 );
1047 layout.mode = ChartLayoutMode::TwoHorizontal;
1048 frame(&mut layout, &mut charts, vec![]);
1049 let point = charts[0].get_rendered_price_rect().center();
1050 frame(
1051 &mut layout,
1052 &mut charts,
1053 vec![
1054 Event::PointerMoved(point),
1055 Event::PointerButton {
1056 pos: point,
1057 button: PointerButton::Primary,
1058 pressed: true,
1059 modifiers: Default::default(),
1060 },
1061 ],
1062 );
1063 assert_eq!(layout.active(), 0);
1064 frame(
1065 &mut layout,
1066 &mut charts,
1067 vec![Event::PointerButton {
1068 pos: point,
1069 button: PointerButton::Primary,
1070 pressed: false,
1071 modifiers: Default::default(),
1072 }],
1073 );
1074 let click = |pos, pressed| Event::PointerButton {
1076 pos,
1077 button: PointerButton::Primary,
1078 pressed,
1079 modifiers: Default::default(),
1080 };
1081 let corner = pos2(308.0, 12.0);
1082 frame(&mut layout, &mut charts, vec![Event::PointerMoved(corner)]);
1083 frame(&mut layout, &mut charts, vec![click(corner, true)]);
1084 frame(&mut layout, &mut charts, vec![click(corner, false)]);
1085 frame(&mut layout, &mut charts, vec![]);
1086 let input =
1087 frame(&mut layout, &mut charts, vec![]).expect("group editor") + vec2(100.0, 18.0);
1088 assert!(input.x > 320.0);
1089 frame(&mut layout, &mut charts, vec![Event::PointerMoved(input)]);
1090 frame(&mut layout, &mut charts, vec![click(input, true)]);
1091 frame(&mut layout, &mut charts, vec![click(input, false)]);
1092 frame(&mut layout, &mut charts, vec![Event::Text("Desk".into())]);
1093 assert_eq!(layout.sync_group(0), Some("Desk"));
1094 assert_eq!(layout.active(), 0);
1095 let escape = Event::Key {
1096 key: egui::Key::Escape,
1097 physical_key: None,
1098 pressed: true,
1099 repeat: false,
1100 modifiers: Default::default(),
1101 };
1102 frame(&mut layout, &mut charts, vec![escape]);
1103 layout.set_sync_group(0, None);
1104 frame(&mut layout, &mut charts, vec![Event::PointerMoved(point)]);
1105 frame(&mut layout, &mut charts, vec![click(point, true)]);
1106 frame(&mut layout, &mut charts, vec![click(point, false)]);
1107 layout.sync.sync_time_axis = false;
1108 for chart in &mut charts {
1109 chart.apply_synced_time_scale(3.0, 0.0);
1110 }
1111 #[cfg(feature = "drawings")]
1112 {
1113 for chart in &mut charts {
1115 let mut drawing = crate::drawings::Drawing::new(
1116 0,
1117 crate::drawings::DrawingToolType::HorizontalLine,
1118 );
1119 drawing.completed = true;
1120 drawing
1121 .chart_points
1122 .push(crate::drawings::ChartPoint::new(10.0, 10.0));
1123 chart.drawings_mut().add_synced_drawing(drawing).unwrap();
1124 let id = chart.drawings().drawings[0].id;
1125 chart.drawings_mut().select(id);
1126 }
1127 frame(
1128 &mut layout,
1129 &mut charts,
1130 vec![Event::Key {
1131 key: egui::Key::Delete,
1132 physical_key: None,
1133 pressed: true,
1134 repeat: false,
1135 modifiers: Default::default(),
1136 }],
1137 );
1138 assert!(charts[0].drawings().drawings.is_empty());
1139 assert_eq!(charts[1].drawings().drawings.len(), 1);
1140 let id = charts[1].drawings().drawings[0].id;
1141 charts[1].drawings_mut().delete_drawing(id);
1142 }
1143
1144 charts[0].last_hover_bar_idx = Some(10.0);
1146 layout.mode = ChartLayoutMode::Tabs;
1147 layout.set_active(1);
1148 frame(&mut layout, &mut charts, vec![Event::PointerGone]);
1149 assert_eq!(charts[0].hover_time(), None);
1150 assert_eq!(charts[1].synced_crosshair_bar_idx, None);
1151
1152 #[cfg(feature = "drawings")]
1153 {
1154 layout.mode = ChartLayoutMode::TwoHorizontal;
1156 layout.sync.sync_drawings = true;
1157 let mut drawing =
1158 crate::drawings::Drawing::new(0, crate::drawings::DrawingToolType::HorizontalLine);
1159 drawing.completed = true;
1160 drawing
1161 .chart_points
1162 .push(crate::drawings::ChartPoint::new(10.0, 10.0));
1163 charts[0]
1164 .drawings_mut()
1165 .add_synced_drawing(drawing)
1166 .unwrap();
1167 frame(&mut layout, &mut charts, vec![]);
1168 assert_eq!(charts[1].drawings().drawings.len(), 1);
1169 layout.mode = ChartLayoutMode::Single;
1170 frame(&mut layout, &mut charts, vec![]);
1171 layout.mode = ChartLayoutMode::TwoHorizontal;
1172 frame(&mut layout, &mut charts, vec![]);
1173 assert_eq!(charts[0].drawings().drawings.len(), 1);
1174 assert_eq!(charts[1].drawings().drawings.len(), 1);
1175 layout.mode = ChartLayoutMode::Single;
1176 let id = charts[0].drawings().drawings[0].id;
1177 charts[0].drawings_mut().delete_drawing(id);
1178 frame(&mut layout, &mut charts, vec![]);
1179 layout.mode = ChartLayoutMode::TwoHorizontal;
1180 frame(&mut layout, &mut charts, vec![]);
1181 assert!(charts[1].drawings().drawings.is_empty());
1182 }
1183
1184 layout.set_detached(1, true);
1185 frame(&mut layout, &mut charts, vec![]);
1186 assert_eq!(charts[1].bars(super::super::SeriesId::MAIN).len(), 100);
1187 layout.set_detached(1, false);
1188 frame(&mut layout, &mut charts, vec![]);
1189 assert!(!layout.is_detached(1));
1190 }
1191
1192 #[test]
1193 fn sync_switches_and_groups_isolate_changes() {
1194 let mut layout = ChartLayout {
1195 mode: ChartLayoutMode::Four,
1196 ..Default::default()
1197 };
1198 let mut charts = [chart(60), chart(120), chart(60), chart(60)];
1199 let mut refs: Vec<_> = charts.iter_mut().collect();
1200 layout.set_sync_group(2, Some("B".into()));
1201 layout.previous[0] = Some(("SAMPLE".into(), Some(Timeframe::Min1)));
1202 refs[0].set_symbol("OTHER");
1203 refs[0].set_timeframe(Timeframe::Hour1);
1204 layout.sync_state(&mut refs, 4);
1205 assert_eq!(refs[1].symbol(), "SAMPLE");
1206 layout.sync.sync_symbol = true;
1207 layout.sync.sync_timeframe = true;
1208 layout.sync_state(&mut refs, 4);
1209 assert_eq!(
1210 (refs[1].symbol(), refs[1].timeframe()),
1211 ("OTHER", Some(Timeframe::Hour1))
1212 );
1213 assert_eq!(
1214 (refs[2].symbol(), refs[2].timeframe()),
1215 ("SAMPLE", Some(Timeframe::Min1))
1216 );
1217 refs[1].last_hover_bar_idx = Some(2.0);
1219 let time = refs[1].hover_time().unwrap();
1220 refs[1].last_crosshair = Some((time, Some(10.5)));
1221 layout.sync_view(&mut refs, 4, false);
1222 assert_eq!(refs[0].synced_crosshair_bar_idx, Some(4.0));
1223 assert_eq!(refs[0].synced_crosshair_price, Some(10.5));
1224 assert_eq!(refs[2].synced_crosshair_bar_idx, None);
1225 refs[1].last_hover_bar_idx = None;
1226 layout.sync_view(&mut refs, 4, false);
1227 assert_eq!(refs[0].synced_crosshair_bar_idx, None);
1228 assert_eq!(refs[0].synced_crosshair_price, None);
1229 refs[1].last_hover_bar_idx = Some(2.0);
1230 layout.sync.sync_crosshair = false;
1231 layout.sync_view(&mut refs, 4, false);
1232 assert_eq!(refs[0].synced_crosshair_bar_idx, None);
1233 let range = super::super::VisibleTimeRange {
1234 from: refs[0].state.shown()[10].time,
1235 to: refs[0].state.shown()[30].time,
1236 };
1237 let before = refs[1].get_time_scale_state();
1238 layout.apply_date_range(&mut refs, range);
1239 assert_eq!(refs[1].get_time_scale_state(), before);
1240 layout.sync.sync_date_range = true;
1241 let isolated = refs[2].get_time_scale_state();
1242 layout.apply_date_range(&mut refs, range);
1243 assert_ne!(refs[1].get_time_scale_state(), before);
1244 assert_eq!(refs[2].get_time_scale_state(), isolated);
1245
1246 let ctx = egui::Context::default();
1248 for mode in [ChartLayoutMode::Four, ChartLayoutMode::Tabs] {
1249 let mut layout = ChartLayout {
1250 mode,
1251 ..Default::default()
1252 };
1253 let mut charts = [chart(60), chart(60), chart(60), chart(60)];
1254 let frame = |layout: &mut ChartLayout, charts: &mut [Chart; 4]| {
1255 ctx.run_ui(
1256 RawInput {
1257 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1440.0, 900.0))),
1258 ..Default::default()
1259 },
1260 |ui| {
1261 layout.show(ui, &mut charts.iter_mut().collect::<Vec<_>>());
1262 },
1263 )
1264 .drop_without_applying_deltas();
1265 };
1266 frame(&mut layout, &mut charts);
1267 for active in [0, 1] {
1268 layout.set_active(active);
1269 frame(&mut layout, &mut charts);
1270 charts[active].apply_synced_time_scale(0.5, 0.0);
1271 frame(&mut layout, &mut charts);
1272 let remembered = charts[active].visible_time_range().unwrap();
1273 charts[active].set_timeframe(Timeframe::Hour1);
1274 charts[active].set_data(super::super::SeriesId::MAIN, vec![]);
1275 frame(&mut layout, &mut charts);
1276 let others: Vec<_> = charts.iter().map(Chart::get_time_scale_state).collect();
1277 frame(&mut layout, &mut charts);
1278 for (index, chart) in charts.iter().enumerate() {
1279 if index != active {
1280 assert_eq!(chart.get_time_scale_state(), others[index]);
1281 }
1282 }
1283 charts[active].set_data(
1284 super::super::SeriesId::MAIN,
1285 chart(3600).state.shown().to_vec(),
1286 );
1287 frame(&mut layout, &mut charts);
1288 assert!((charts[active].get_time_scale_state().0 - 30.0).abs() < 0.01);
1290 let actual = charts[active].visible_time_range().unwrap();
1291 assert!((actual.from - remembered.from).num_milliseconds().abs() < 1);
1292 assert!((actual.to - remembered.to).num_milliseconds().abs() < 1);
1293 }
1294 layout.set_active(0);
1295 charts[0].set_visible_bars(65);
1296 frame(&mut layout, &mut charts);
1297 for chart in &charts {
1298 let scale = chart.state.time_scale();
1299 let range = scale.visible_logical_range();
1300 assert!(scale.bar_spacing().is_finite() && scale.bar_spacing() >= 0.5);
1301 assert!(range.left.ceil().max(0.0) <= range.right.floor().min(99.0));
1302 }
1303 layout.set_active(2);
1305 frame(&mut layout, &mut charts);
1306 charts[2].set_timeframe(Timeframe::Day1);
1307 charts[2].set_data(super::super::SeriesId::MAIN, vec![]);
1308 frame(&mut layout, &mut charts);
1309 let mut later = chart(86_400).state.shown().to_vec();
1310 later
1311 .iter_mut()
1312 .for_each(|bar| bar.time += Duration::days(3650));
1313 charts[2].set_data(super::super::SeriesId::MAIN, later);
1314 frame(&mut layout, &mut charts);
1315 assert!(layout.pending_ranges.iter().all(Option::is_none));
1316
1317 for emptied in [true, false] {
1321 let mut layout = ChartLayout {
1322 mode,
1323 ..Default::default()
1324 };
1325 layout.sync.sync_timeframe = true;
1326 let mut charts = [chart(60), chart(60), chart(60), chart(60)];
1327 frame(&mut layout, &mut charts);
1328 charts[0].set_timeframe(Timeframe::Hour1);
1329 for chart in charts.iter_mut().filter(|_| emptied) {
1330 chart.set_data(super::super::SeriesId::MAIN, vec![]);
1331 }
1332 frame(&mut layout, &mut charts);
1333 let hourly = chart(3600).state.shown().to_vec();
1334 charts[0].set_data(super::super::SeriesId::MAIN, hourly.clone());
1335 frame(&mut layout, &mut charts);
1336 charts[0].set_visible_bars(8);
1337 frame(&mut layout, &mut charts);
1338 assert!(charts[0].get_time_scale_state().0 > 50.0);
1339 let latest = charts[0].visible_time_range().unwrap();
1340 charts[1].set_data(super::super::SeriesId::MAIN, hourly);
1341 layout.set_active(1);
1342 frame(&mut layout, &mut charts);
1343 for chart in &charts[..2] {
1344 let actual = chart.visible_time_range().unwrap();
1345 assert!((actual.from - latest.from).num_milliseconds().abs() < 1);
1346 assert!((actual.to - latest.to).num_milliseconds().abs() < 1);
1347 }
1348 }
1349
1350 let mut layout = ChartLayout {
1352 mode,
1353 ..Default::default()
1354 };
1355 let mut charts = [chart(60), chart(60), chart(60), chart(60)];
1356 frame(&mut layout, &mut charts);
1357 charts[0].set_visible_bars(8);
1358 frame(&mut layout, &mut charts);
1359 assert!(charts[0].get_time_scale_state().0 > 72.0);
1360 let source = charts[0].visible_time_range().unwrap();
1361 for chart in &charts[1..] {
1362 let actual = chart.visible_time_range().unwrap();
1363 assert!((actual.from - source.from).num_milliseconds().abs() < 1);
1364 assert!((actual.to - source.to).num_milliseconds().abs() < 1);
1365 }
1366 }
1367 }
1368
1369 #[test]
1372 fn alt_enter_maximizes_and_restores_the_active_tile() {
1373 let ctx = egui::Context::default();
1374 let mut layout = ChartLayout {
1375 mode: ChartLayoutMode::Four,
1376 ..Default::default()
1377 };
1378 let mut charts = [chart(60), chart(120), chart(60), chart(60)];
1379 layout.set_sync_group(2, Some("B".into()));
1380 layout.set_active(2);
1381 let mut time = 0.0;
1382 let mut frame = |layout: &mut ChartLayout, charts: &mut [Chart; 4], pressed: bool| {
1383 time += 0.1;
1384 let events = if pressed {
1385 vec![Event::Key {
1386 key: egui::Key::Enter,
1387 physical_key: None,
1388 pressed: true,
1389 repeat: false,
1390 modifiers: egui::Modifiers::ALT,
1391 }]
1392 } else {
1393 vec![]
1394 };
1395 ctx.run_ui(
1396 RawInput {
1397 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
1398 time: Some(time),
1399 events,
1400 ..Default::default()
1401 },
1402 |ui| {
1403 layout.show(ui, &mut charts.iter_mut().collect::<Vec<_>>());
1404 },
1405 )
1406 .drop_without_applying_deltas();
1407 };
1408 for _ in 0..3 {
1409 frame(&mut layout, &mut charts, false);
1410 }
1411 let tile = charts[2].get_rendered_price_rect();
1412 let viewport = charts[2].get_time_scale_state();
1413 frame(&mut layout, &mut charts, true);
1414 frame(&mut layout, &mut charts, false);
1416 frame(&mut layout, &mut charts, false);
1417 assert_eq!((layout.maximized, layout.active()), (Some(2), 2));
1418 assert!(charts[2].get_rendered_price_rect().width() > tile.width() * 1.8);
1419 assert_eq!(layout.sync_group(2), Some("B"));
1420 assert!(charts[2].take_shortcut_requests().is_empty());
1421 frame(&mut layout, &mut charts, true);
1422 frame(&mut layout, &mut charts, false);
1423 frame(&mut layout, &mut charts, false);
1424 assert_eq!((layout.maximized, layout.active()), (None, 2));
1425 assert_eq!(charts[2].get_rendered_price_rect(), tile);
1426 assert_eq!(charts[2].get_time_scale_state(), viewport);
1427 }
1428}