Skip to main content

egui_charts/ui/replay/
mod.rs

1//! Historical bar replay controls, session state and cursor management.
2//!
3//! Use [`ReplayController`] to drive playback over host-supplied bars and apply its
4//! cutoff to the chart with [`Chart::set_replay_time`](crate::Chart::set_replay_time).
5//! [`ReplayControls`] emits playback and seek actions; the host retains or persists
6//! the session and supplies any additional source history.
7//!
8//! # Example
9//!
10//! ```rust
11//! # let bars: Vec<egui_charts::model::Bar> = (0..100).map(|i| { let p = 100.0 + (i as f64 * 0.2).sin() * 5.0; egui_charts::model::Bar::new(chrono::DateTime::from_timestamp(1_700_000_000 + i * 60, 0).unwrap(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
12//! use egui_charts::ui::replay::{ReplayController, ReplayControls};
13//!
14//! // Keep the full history in the chart and drive its window with the source cursor.
15//! use egui_charts::{Chart, model::{BarData, BarClock, Timeframe}};
16//! let mut chart = Chart::new(BarData::from_bars(bars.clone()));
17//! chart.set_timeframe(Timeframe::Min1);
18//! // Create replay controller with data
19//! # let ts = bars.iter().map(|bar| bar.time).collect();
20//! let mut controller = ReplayController::new(bars, ts);
21//!
22//! // In your UI loop:
23//! # egui::__run_test_ui(|ui| {
24//! egui::Panel::bottom("replay").show(ui, |ui| {
25//!     let controls = ReplayControls::new(&controller.config);
26//!     let action = controls.show(ui, &mut controller.state);
27//!     controller.handle_action(action);
28//! });
29//! # });
30//!
31//! chart.set_replay_time(controller.cutoff(&BarClock::new(Timeframe::Min1)));
32//! // Supply finer history with chart.set_replay_source when it is available.
33//! // One-trade bars from data::aggregate_trades advance it trade by trade.
34//! // Store controller.session() in host storage and restore it after fetching source history.
35//! ```
36
37mod actions;
38mod config;
39mod controls;
40pub mod floating;
41pub(super) mod model;
42mod session;
43mod start;
44mod state;
45pub mod status;
46mod toolbar;
47
48pub use crate::model::ReplaySession;
49pub use actions::{ReplayAction, ReplaySpeed};
50pub use config::{ReplayBehavior, ReplayColors, ReplayConfig, ReplayLayout};
51pub use controls::{CompactReplayControls, ReplayControls, ReplayKeyboardHandler};
52pub use start::{ReplayStartAction, ReplayStartPicker};
53pub use state::{PlaybackState, ReplayMarker, ReplayState};
54pub use toolbar::{ReplayToolbar, ReplayToolbarAction, ReplayToolbarConfig, ReplayToolbarState};
55
56use crate::model::Bar;
57use chrono::{DateTime, Utc};
58
59/// Replay controller
60///
61/// Orchestrates the replay system, managing state, data feeding, and action handling.
62/// This is the main entry point for integrating replay functionality into your application.
63#[derive(Default)]
64pub struct ReplayController {
65    /// Replay state
66    pub state: ReplayState,
67    /// Replay configuration
68    pub config: ReplayConfig,
69    /// Historical bar data
70    bars: Vec<Bar>,
71    /// Bar ts
72    ts: Vec<DateTime<Utc>>,
73}
74
75impl ReplayController {
76    /// Create a new replay controller with bar data. Timestamps are read from the bars;
77    /// the separate timestamp argument is retained for source compatibility.
78    pub fn new(bars: Vec<Bar>, _ts: Vec<DateTime<Utc>>) -> Self {
79        let ts: Vec<_> = bars.iter().map(|bar| bar.time).collect();
80        let mut state = ReplayState::new();
81
82        // Initialize state if we have data
83        if !bars.is_empty() && !ts.is_empty() {
84            let start = ts.first().copied().unwrap_or_else(Utc::now);
85            let end = ts.last().copied().unwrap_or_else(Utc::now);
86            state.init(bars.len(), start, end, String::new());
87        }
88
89        Self {
90            state,
91            config: ReplayConfig::default(),
92            bars,
93            ts,
94        }
95    }
96
97    /// Create with configuration
98    pub fn with_config(mut self, config: ReplayConfig) -> Self {
99        self.config = config;
100        self
101    }
102
103    /// Set the symbol name
104    pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
105        self.state.symbol = Some(symbol.into());
106        self
107    }
108
109    /// Load new bar data
110    ///
111    /// During an active replay the position moves to the last bar that opens at or before the
112    /// current replay time and playback carries on, so a change of interval continues the replay
113    /// where it was. On a coarser interval the bar that contains the replay time is shown whole; a
114    /// host that wants it partial passes that bar built from the finer bars. Otherwise the replay
115    /// starts paused on the first bar.
116    pub fn load_data(&mut self, bars: Vec<Bar>, _ts: Vec<DateTime<Utc>>, symbol: String) {
117        let resume = self.state.is_active().then(|| {
118            (
119                self.ts.get(self.state.curr_bar).copied(),
120                self.state.is_playing(),
121            )
122        });
123        self.bars = bars;
124        self.ts = self.bars.iter().map(|bar| bar.time).collect();
125
126        if !self.bars.is_empty() && !self.ts.is_empty() {
127            let start = self.ts.first().copied().unwrap_or_else(Utc::now);
128            let end = self.ts.last().copied().unwrap_or_else(Utc::now);
129            self.state.init(self.bars.len(), start, end, symbol);
130            if let Some((Some(time), playing)) = resume {
131                let bar = self.ts.partition_point(|&t| t <= time).saturating_sub(1);
132                self.state.jump_to_bar(bar);
133                self.update_curr_date();
134                if playing {
135                    self.state.play();
136                }
137            }
138        } else {
139            self.state.stop();
140            self.state.total_bars = 0;
141            self.state.curr_date = None;
142            self.state.start_date = None;
143            self.state.end_date = None;
144        }
145    }
146
147    /// Unload current data
148    pub fn unload_data(&mut self) {
149        self.bars.clear();
150        self.ts.clear();
151        self.state.stop();
152        self.state.total_bars = 0;
153    }
154
155    /// Check if data is loaded
156    pub fn has_data(&self) -> bool {
157        !self.bars.is_empty()
158    }
159
160    /// Get the current bar
161    pub fn curr_bar(&self) -> Option<&Bar> {
162        self.bars.get(self.state.curr_bar)
163    }
164
165    /// Get the current ts
166    pub fn curr_ts(&self) -> Option<&DateTime<Utc>> {
167        self.ts.get(self.state.curr_bar)
168    }
169
170    /// Get visible bars up to current position
171    pub fn visible_bars(&self) -> &[Bar] {
172        let end = (self.state.curr_bar + 1).min(self.bars.len());
173        &self.bars[..end]
174    }
175
176    /// Get visible ts up to current position
177    pub fn visible_ts(&self) -> &[DateTime<Utc>] {
178        let end = (self.state.curr_bar + 1).min(self.ts.len());
179        &self.ts[..end]
180    }
181
182    /// Get bars in a window around current position
183    pub fn bars_in_window(&self, window_size: usize) -> &[Bar] {
184        let end = (self.state.curr_bar + 1).min(self.bars.len());
185        let start = end.saturating_sub(window_size);
186        &self.bars[start..end]
187    }
188
189    /// Handle a replay action
190    pub fn handle_action(&mut self, action: ReplayAction) {
191        match action {
192            ReplayAction::None => {}
193
194            // Playback controls
195            ReplayAction::Play => self.state.play(),
196            ReplayAction::Pause => self.state.pause(),
197            ReplayAction::TogglePlayPause => self.state.toggle_play_pause(),
198            ReplayAction::Stop => self.state.stop(),
199            ReplayAction::Reset => self.state.reset(),
200
201            // Navigation
202            ReplayAction::StepForward => {
203                self.state.pause();
204                self.state.step_forward();
205                self.update_curr_date();
206            }
207            ReplayAction::StepBackward => {
208                self.state.pause();
209                self.state.step_backward();
210                self.update_curr_date();
211            }
212            ReplayAction::StepForwardN(n) => {
213                self.state.pause();
214                for _ in 0..n {
215                    if !self.state.at_end() {
216                        self.state.step_forward();
217                    }
218                }
219                self.update_curr_date();
220            }
221            ReplayAction::StepBackwardN(n) => {
222                self.state.pause();
223                for _ in 0..n {
224                    if !self.state.at_start() {
225                        self.state.step_backward();
226                    }
227                }
228                self.update_curr_date();
229            }
230            ReplayAction::JumpToBar(bar) => {
231                self.state.jump_to_bar(bar);
232                self.update_curr_date();
233            }
234            ReplayAction::JumpToDate(date) => {
235                // Find the nearest bar to the given date
236                if let Some(idx) = self.find_bar_at_date(&date) {
237                    self.state.jump_to_bar(idx);
238                    self.update_curr_date();
239                }
240            }
241            ReplayAction::JumpToStart => {
242                self.state.jump_to_bar(0);
243                self.update_curr_date();
244            }
245            ReplayAction::JumpToEnd => {
246                self.state.jump_to_bar(self.bars.len().saturating_sub(1));
247                self.update_curr_date();
248            }
249            ReplayAction::JumpToPercent(pct) => {
250                self.state.jump_to_percent(pct);
251                self.update_curr_date();
252            }
253
254            // Speed control
255            ReplayAction::SetSpeed(speed) => self.state.set_speed(ReplaySpeed::from(speed)),
256            ReplayAction::SpeedUp => self.state.speed_up(),
257            ReplayAction::SlowDown => self.state.slow_down(),
258
259            // Mode control
260            ReplayAction::EnterReplayMode => self.state.enter_replay_mode(),
261            ReplayAction::ExitReplayMode => self.state.exit_replay_mode(),
262            ReplayAction::ToggleReplayMode => {
263                if self.state.is_active() {
264                    self.state.exit_replay_mode();
265                } else {
266                    self.state.enter_replay_mode();
267                }
268            }
269
270            // Data loading (handled externally, this just updates state)
271            ReplayAction::LoadData { symbol, start, end } => {
272                // Data loading would be handled by the application
273                // This action signals intent to load data
274                if let (Some(s), Some(e)) = (start, end) {
275                    self.state.start_date = Some(s);
276                    self.state.end_date = Some(e);
277                }
278                self.state.symbol = Some(symbol);
279            }
280            ReplayAction::UnloadData => {
281                self.unload_data();
282            }
283
284            // Range selection
285            ReplayAction::SetStartDate(date) => {
286                self.state.start_date = Some(date);
287            }
288            ReplayAction::SetEndDate(date) => {
289                self.state.end_date = Some(date);
290            }
291            ReplayAction::SetDateRange { start, end } => {
292                self.state.start_date = Some(start);
293                self.state.end_date = Some(end);
294            }
295
296            // Markers
297            ReplayAction::AddMarker { label, color } => {
298                self.state.add_marker(label, color);
299            }
300            ReplayAction::RemoveMarker(bar_idx) => {
301                self.state.remove_marker(bar_idx);
302            }
303            ReplayAction::ClearMarkers => {
304                self.state.clear_markers();
305            }
306        }
307    }
308
309    /// Update state each frame (call in your render loop)
310    pub fn update(&mut self) -> usize {
311        let bars_advanced = self.state.update();
312        if bars_advanced > 0 {
313            self.update_curr_date();
314        }
315        bars_advanced
316    }
317
318    /// Find the bar index closest to the given date
319    fn find_bar_at_date(&self, target: &DateTime<Utc>) -> Option<usize> {
320        if self.ts.is_empty() {
321            return None;
322        }
323
324        // Binary search for the closest ts
325        let result = self.ts.binary_search(target);
326        match result {
327            Ok(idx) => Some(idx),
328            Err(idx) => {
329                if idx == 0 {
330                    Some(0)
331                } else if idx >= self.ts.len() {
332                    Some(self.ts.len() - 1)
333                } else {
334                    // Compare distances to adjacent ts
335                    let before_diff = *target - self.ts[idx - 1];
336                    let after_diff = self.ts[idx] - *target;
337                    if before_diff < after_diff {
338                        Some(idx - 1)
339                    } else {
340                        Some(idx)
341                    }
342                }
343            }
344        }
345    }
346
347    /// Update current date from bar ts
348    fn update_curr_date(&mut self) {
349        self.state.update_curr_date(&self.ts);
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use chrono::TimeZone;
357
358    fn create_test_bars(count: usize) -> (Vec<Bar>, Vec<DateTime<Utc>>) {
359        let mut bars = Vec::with_capacity(count);
360        let mut timestamps = Vec::with_capacity(count);
361
362        for i in 0..count {
363            // Use different days to avoid hour overflow (hours 0-23 only)
364            let day = (i / 24) as u32 + 1;
365            let hour = (i % 24) as u32;
366            let ts = Utc
367                .with_ymd_and_hms(2024, 1, day.min(28), hour, 0, 0)
368                .unwrap();
369            bars.push(Bar {
370                time: ts,
371                open: 100.0 + i as f64,
372                high: 101.0 + i as f64,
373                low: 99.0 + i as f64,
374                close: 100.5 + i as f64,
375                volume: 1000.0,
376                colors: None,
377                payload: None,
378            });
379            timestamps.push(ts);
380        }
381
382        (bars, timestamps)
383    }
384
385    #[test]
386    fn test_controller_creation() {
387        let (bars, ts) = create_test_bars(100);
388        let controller = ReplayController::new(bars, ts);
389
390        assert!(controller.has_data());
391        assert_eq!(controller.state.total_bars, 100);
392    }
393
394    #[test]
395    fn test_navigation() {
396        let (bars, ts) = create_test_bars(100);
397        let mut controller = ReplayController::new(bars, ts);
398
399        controller.handle_action(ReplayAction::EnterReplayMode);
400        assert!(controller.state.is_active());
401
402        controller.handle_action(ReplayAction::JumpToBar(50));
403        assert_eq!(controller.state.curr_bar, 50);
404
405        controller.handle_action(ReplayAction::StepForward);
406        assert_eq!(controller.state.curr_bar, 51);
407
408        controller.handle_action(ReplayAction::StepBackward);
409        assert_eq!(controller.state.curr_bar, 50);
410    }
411
412    fn play_to_end(controller: &mut ReplayController) {
413        controller.state.set_bar_duration(0.001);
414        controller.handle_action(ReplayAction::SetSpeed(100.0));
415        controller.handle_action(ReplayAction::Play);
416        while controller.state.is_playing() {
417            controller.update();
418        }
419    }
420
421    #[test]
422    fn playback_state_follows_seeks_and_steps() {
423        use ReplayAction::*;
424        type Setup = fn(&mut ReplayController);
425        let rows: [(Setup, Vec<ReplayAction>, (usize, PlaybackState)); 6] = [
426            (
427                play_to_end,
428                vec![JumpToPercent(0.3), TogglePlayPause],
429                (29, PlaybackState::Playing),
430            ),
431            (play_to_end, vec![Reset], (0, PlaybackState::Paused)),
432            (|_| {}, vec![JumpToEnd], (99, PlaybackState::Finished)),
433            (
434                |_| {},
435                vec![JumpToPercent(1.0)],
436                (99, PlaybackState::Finished),
437            ),
438            (
439                |_| {},
440                vec![StepForwardN(10)],
441                (99, PlaybackState::Finished),
442            ),
443            (
444                |_| {},
445                vec![JumpToBar(98), StepForward],
446                (99, PlaybackState::Finished),
447            ),
448        ];
449        for (index, (setup, actions, expected)) in rows.into_iter().enumerate() {
450            let (bars, ts) = create_test_bars(100);
451            let mut controller = ReplayController::new(bars, ts);
452            controller.handle_action(JumpToBar(95));
453            setup(&mut controller);
454            for action in actions {
455                controller.handle_action(action);
456            }
457            let state = &controller.state;
458            assert_eq!(
459                (state.curr_bar, state.playback_state),
460                expected,
461                "row {index}"
462            );
463        }
464    }
465
466    #[test]
467    fn interval_change_keeps_the_replay_time() {
468        type Row = (
469            fn(&mut ReplayController),
470            (usize, PlaybackState, DateTime<Utc>),
471        );
472        let days: Vec<_> = (1..=3)
473            .map(|day| Utc.with_ymd_and_hms(2024, 1, day, 0, 0, 0).unwrap())
474            .collect();
475        let rows: [Row; 2] = [
476            // Hourly bar 30 opens 2024-01-02 06:00, inside the second daily bar.
477            (
478                |c| {
479                    c.handle_action(ReplayAction::JumpToBar(30));
480                    c.handle_action(ReplayAction::Play);
481                },
482                (1, PlaybackState::Playing, days[1]),
483            ),
484            // Toggling play after the end restarts on hourly bar 0, inside the first daily bar.
485            (
486                |c| {
487                    play_to_end(c);
488                    c.handle_action(ReplayAction::TogglePlayPause);
489                },
490                (0, PlaybackState::Playing, days[0]),
491            ),
492        ];
493        for (index, (setup, (bar, playback, date))) in rows.into_iter().enumerate() {
494            let (bars, ts) = create_test_bars(48);
495            let mut controller = ReplayController::new(bars, ts);
496            setup(&mut controller);
497            let daily = days
498                .iter()
499                .map(|&time| Bar::new(time, 100.0, 101.0, 99.0, 100.5, 1000.0))
500                .collect();
501            controller.load_data(daily, days.clone(), String::new());
502
503            let state = &controller.state;
504            assert_eq!(
505                (state.curr_bar, state.playback_state, state.curr_date),
506                (bar, playback, Some(date)),
507                "row {index}"
508            );
509        }
510    }
511
512    #[test]
513    fn test_visible_bars() {
514        let (bars, ts) = create_test_bars(100);
515        let mut controller = ReplayController::new(bars, ts);
516
517        controller.state.curr_bar = 25;
518        let visible = controller.visible_bars();
519        assert_eq!(visible.len(), 26); // 0 through 25 inclusive
520    }
521
522    #[test]
523    fn test_find_bar_at_date() {
524        let (bars, ts) = create_test_bars(24);
525        let mut controller = ReplayController::new(bars, ts);
526
527        let target = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
528        controller.handle_action(ReplayAction::JumpToDate(target));
529        assert_eq!(controller.state.curr_bar, 12);
530    }
531}