Skip to main content

egui_charts/ui/replay/
state.rs

1//! Replay state
2//!
3//! Mutable state for replay mode UI and playback control.
4
5use super::actions::ReplaySpeed;
6use chrono::{DateTime, Utc};
7use web_time::Instant;
8
9/// Replay marker for annotating specific bars
10#[derive(Debug, Clone)]
11pub struct ReplayMarker {
12    /// Bar index where marker is placed
13    pub bar_idx: usize,
14    /// Optional label text
15    pub label: Option<String>,
16    /// Marker color (RGBA)
17    pub color: [u8; 4],
18    /// Ts when marker was created
19    pub created_at: DateTime<Utc>,
20}
21
22impl ReplayMarker {
23    /// Create a new marker at the given bar index
24    pub fn new(bar_idx: usize) -> Self {
25        Self {
26            bar_idx,
27            label: None,
28            color: [255, 165, 0, 255], // Orange default
29            created_at: Utc::now(),
30        }
31    }
32
33    /// Set the label
34    pub fn with_label(mut self, label: impl Into<String>) -> Self {
35        self.label = Some(label.into());
36        self
37    }
38
39    /// Set the color
40    pub fn with_color(mut self, color: [u8; 4]) -> Self {
41        self.color = color;
42        self
43    }
44}
45
46/// Playback state for the replay
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum PlaybackState {
49    /// Not in replay mode
50    #[default]
51    Inactive,
52    /// In replay mode but paused
53    Paused,
54    /// Currently playing
55    Playing,
56    /// Reached end of data
57    Finished,
58}
59
60impl PlaybackState {
61    /// Check if replay mode is active
62    pub fn is_active(&self) -> bool {
63        !matches!(self, PlaybackState::Inactive)
64    }
65
66    /// Check if currently playing
67    pub fn is_playing(&self) -> bool {
68        matches!(self, PlaybackState::Playing)
69    }
70
71    /// Check if paused (active but not playing)
72    pub fn is_paused(&self) -> bool {
73        matches!(self, PlaybackState::Paused)
74    }
75}
76
77/// Main replay state
78#[derive(Debug, Clone)]
79pub struct ReplayState {
80    // Playback state
81    /// Current playback state
82    pub playback_state: PlaybackState,
83    /// Current playback speed
84    pub speed: ReplaySpeed,
85
86    // Pos tracking
87    /// Current bar index (0-based)
88    pub curr_bar: usize,
89    /// Total number of bars available
90    pub total_bars: usize,
91    /// Visible bar count (how many bars to show on chart)
92    pub visible_bars: usize,
93
94    // Time tracking
95    /// Start date of replay data
96    pub start_date: Option<DateTime<Utc>>,
97    /// End date of replay data
98    pub end_date: Option<DateTime<Utc>>,
99    /// Current date/time at playback position
100    pub curr_date: Option<DateTime<Utc>>,
101
102    // Symbol info
103    /// Symbol being replayed
104    pub symbol: Option<String>,
105
106    // UI state
107    /// Whether speed dropdown is open
108    pub speed_dropdown_open: bool,
109    /// Whether date picker is open
110    pub date_picker_open: bool,
111    /// Whether settings panel is open
112    pub settings_open: bool,
113
114    // Markers
115    /// User-placed markers
116    pub markers: Vec<ReplayMarker>,
117
118    // Internal timing
119    /// Last update instant (for frame timing)
120    last_update: Option<Instant>,
121    /// Accumulated time since last bar advance
122    accumulated_time: f64,
123    /// Target time per bar in seconds (based on timeframe)
124    bar_duration_secs: f64,
125}
126
127impl ReplayState {
128    /// Create new replay state
129    pub fn new() -> Self {
130        Self {
131            playback_state: PlaybackState::Inactive,
132            speed: ReplaySpeed::Normal,
133            curr_bar: 0,
134            total_bars: 0,
135            visible_bars: 100,
136            start_date: None,
137            end_date: None,
138            curr_date: None,
139            symbol: None,
140            speed_dropdown_open: false,
141            date_picker_open: false,
142            settings_open: false,
143            markers: Vec::new(),
144            last_update: None,
145            accumulated_time: 0.0,
146            bar_duration_secs: 1.0, // Default 1 second per bar at 1x
147        }
148    }
149
150    /// Initialize replay with data params
151    pub fn init(
152        &mut self,
153        total_bars: usize,
154        start_date: DateTime<Utc>,
155        end_date: DateTime<Utc>,
156        symbol: String,
157    ) {
158        self.total_bars = total_bars;
159        self.start_date = Some(start_date);
160        self.end_date = Some(end_date);
161        self.curr_date = Some(start_date);
162        self.symbol = Some(symbol);
163        self.curr_bar = 0;
164        self.playback_state = PlaybackState::Paused;
165        self.markers.clear();
166        self.sync_finished();
167    }
168
169    /// Set the bar duration (time between bars at 1x speed)
170    pub fn set_bar_duration(&mut self, seconds: f64) {
171        self.bar_duration_secs = seconds.max(0.001);
172    }
173
174    /// Check if replay mode is active
175    pub fn is_active(&self) -> bool {
176        self.playback_state.is_active()
177    }
178
179    /// Check if currently playing
180    pub fn is_playing(&self) -> bool {
181        self.playback_state.is_playing()
182    }
183
184    /// Get progress as percentage (0.0 - 1.0)
185    pub fn progress(&self) -> f32 {
186        if self.total_bars == 0 {
187            0.0
188        } else {
189            self.curr_bar as f32 / (self.total_bars - 1).max(1) as f32
190        }
191    }
192
193    /// Get remaining bars
194    pub fn remaining_bars(&self) -> usize {
195        self.total_bars.saturating_sub(self.curr_bar + 1)
196    }
197
198    /// Check if at the end
199    pub fn at_end(&self) -> bool {
200        self.curr_bar >= self.total_bars.saturating_sub(1)
201    }
202
203    /// Check if at the beginning
204    pub fn at_start(&self) -> bool {
205        self.curr_bar == 0
206    }
207
208    // Playback control methods
209
210    /// Start playing
211    pub fn play(&mut self) {
212        if self.total_bars > 0 && !self.at_end() {
213            self.playback_state = PlaybackState::Playing;
214            self.last_update = Some(Instant::now());
215            self.accumulated_time = 0.0;
216        }
217    }
218
219    /// Pause playback
220    pub fn pause(&mut self) {
221        if self.playback_state.is_playing() {
222            self.playback_state = PlaybackState::Paused;
223            self.last_update = None;
224        }
225    }
226
227    /// Toggle play/pause
228    pub fn toggle_play_pause(&mut self) {
229        match self.playback_state {
230            PlaybackState::Playing => self.pause(),
231            PlaybackState::Paused => self.play(),
232            PlaybackState::Finished => {
233                // Restart from beginning
234                self.curr_bar = 0;
235                self.play();
236            }
237            PlaybackState::Inactive => {}
238        }
239    }
240
241    /// Stop and exit replay mode
242    pub fn stop(&mut self) {
243        self.playback_state = PlaybackState::Inactive;
244        self.curr_bar = 0;
245        self.last_update = None;
246        self.accumulated_time = 0.0;
247    }
248
249    /// Reset to beginning without exiting replay mode
250    pub fn reset(&mut self) {
251        self.pause();
252        self.curr_bar = 0;
253        self.curr_date = self.start_date;
254        self.accumulated_time = 0.0;
255        self.sync_finished();
256    }
257
258    /// Enter replay mode
259    pub fn enter_replay_mode(&mut self) {
260        if self.total_bars > 0 {
261            self.playback_state = PlaybackState::Paused;
262            self.curr_bar = 0;
263            self.curr_date = self.start_date;
264            self.sync_finished();
265        }
266    }
267
268    /// Exit replay mode
269    pub fn exit_replay_mode(&mut self) {
270        self.stop();
271    }
272
273    // Navigation methods
274
275    /// Step forward one bar
276    pub fn step_forward(&mut self) {
277        if !self.at_end() {
278            self.curr_bar += 1;
279        }
280        self.sync_finished();
281    }
282
283    /// Step backward one bar
284    pub fn step_backward(&mut self) {
285        self.curr_bar = self.curr_bar.saturating_sub(1);
286        self.sync_finished();
287    }
288
289    /// Jump to specific bar
290    pub fn jump_to_bar(&mut self, bar: usize) {
291        self.curr_bar = bar.min(self.total_bars.saturating_sub(1));
292        self.sync_finished();
293    }
294
295    /// Keep an active replay `Finished` exactly when it is on the last bar, however it got there.
296    fn sync_finished(&mut self) {
297        if !self.is_active() {
298            return;
299        }
300        if self.at_end() {
301            self.playback_state = PlaybackState::Finished;
302        } else if self.playback_state == PlaybackState::Finished {
303            self.playback_state = PlaybackState::Paused;
304        }
305    }
306
307    /// Jump to percentage position
308    pub fn jump_to_percent(&mut self, percent: f32) {
309        let bar = ((self.total_bars.saturating_sub(1)) as f32 * percent.clamp(0.0, 1.0)) as usize;
310        self.jump_to_bar(bar);
311    }
312
313    // Speed control
314
315    /// Set playback speed
316    pub fn set_speed(&mut self, speed: ReplaySpeed) {
317        self.speed = speed;
318    }
319
320    /// Increase speed
321    pub fn speed_up(&mut self) {
322        self.speed = self.speed.faster();
323    }
324
325    /// Decrease speed
326    pub fn slow_down(&mut self) {
327        self.speed = self.speed.slower();
328    }
329
330    // Marker management
331
332    /// Add a marker at current position
333    pub fn add_marker(&mut self, label: Option<String>, color: Option<[u8; 4]>) {
334        let mut marker = ReplayMarker::new(self.curr_bar);
335        if let Some(l) = label {
336            marker = marker.with_label(l);
337        }
338        if let Some(c) = color {
339            marker = marker.with_color(c);
340        }
341        self.markers.push(marker);
342    }
343
344    /// Remove marker at bar index
345    pub fn remove_marker(&mut self, bar_idx: usize) {
346        self.markers.retain(|m| m.bar_idx != bar_idx);
347    }
348
349    /// Clear all markers
350    pub fn clear_markers(&mut self) {
351        self.markers.clear();
352    }
353
354    /// Get markers in visible range
355    pub fn visible_markers(&self, start_bar: usize, end_bar: usize) -> Vec<&ReplayMarker> {
356        self.markers
357            .iter()
358            .filter(|m| m.bar_idx >= start_bar && m.bar_idx <= end_bar)
359            .collect()
360    }
361
362    // Update method for frame-based playback
363
364    /// Update playback state. Call this each frame.
365    /// Returns the number of bars actually advanced (usually 0 or 1).
366    pub fn update(&mut self) -> usize {
367        if !self.playback_state.is_playing() {
368            return 0;
369        }
370
371        let now = Instant::now();
372        let delta = if let Some(last) = self.last_update {
373            now.duration_since(last).as_secs_f64()
374        } else {
375            0.0
376        };
377        self.last_update = Some(now);
378
379        // Accumulate time adjusted by speed
380        self.accumulated_time += delta * self.speed.multiplier() as f64;
381
382        // Calculate how many bars to advance
383        let bars_to_advance = (self.accumulated_time / self.bar_duration_secs) as usize;
384
385        if bars_to_advance > 0 {
386            self.accumulated_time -= bars_to_advance as f64 * self.bar_duration_secs;
387
388            let last_bar = self.total_bars.saturating_sub(1);
389            let advanced = bars_to_advance.min(last_bar.saturating_sub(self.curr_bar));
390            self.curr_bar += advanced;
391            if self.curr_bar >= last_bar {
392                self.playback_state = PlaybackState::Finished;
393            }
394            return advanced;
395        }
396
397        0
398    }
399
400    /// Update current date based on bar index
401    pub fn update_curr_date(&mut self, bar_ts: &[DateTime<Utc>]) {
402        if let Some(ts) = bar_ts.get(self.curr_bar) {
403            self.curr_date = Some(*ts);
404        }
405    }
406
407    /// Sync replay state from external values.
408    ///
409    /// `active` / `playing` drive the playback state machine,
410    /// `current_bar` / `total_bars` sync position, and `speed` sets playback rate.
411    pub fn sync_from(
412        &mut self,
413        active: bool,
414        playing: bool,
415        current_bar: usize,
416        total_bars: usize,
417        speed: ReplaySpeed,
418    ) {
419        // Update playback state based on central state
420        if active {
421            if playing {
422                if self.playback_state != PlaybackState::Playing {
423                    self.playback_state = PlaybackState::Playing;
424                    self.last_update = Some(Instant::now());
425                }
426            } else if self.playback_state == PlaybackState::Playing {
427                self.playback_state = PlaybackState::Paused;
428            }
429        } else if self.playback_state != PlaybackState::Inactive {
430            self.playback_state = PlaybackState::Inactive;
431        }
432
433        // Sync position
434        self.curr_bar = current_bar;
435        self.total_bars = total_bars;
436
437        // Sync speed
438        self.speed = speed;
439    }
440}
441
442impl Default for ReplayState {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_replay_progress() {
454        let mut state = ReplayState::new();
455        state.total_bars = 100;
456        state.curr_bar = 50;
457        assert!((state.progress() - 0.505).abs() < 0.01);
458    }
459
460    #[test]
461    fn update_near_end_advances_only_remaining_bars() {
462        let mut state = ReplayState::new();
463        state.total_bars = 10;
464        state.curr_bar = 8;
465        state.play();
466        // A frame stall worth 100 bars with only one bar left.
467        state.accumulated_time = 100.0 * state.bar_duration_secs;
468
469        assert_eq!(state.update(), 1);
470        assert_eq!(state.curr_bar, 9);
471        assert_eq!(state.playback_state, PlaybackState::Finished);
472    }
473
474    #[test]
475    fn any_speed_keeps_playback_advancing() {
476        let speeds = [
477            ReplaySpeed::from(f32::NAN),
478            ReplaySpeed::from(-1.0),
479            ReplaySpeed::from(f32::INFINITY),
480            ReplaySpeed::from(0.0),
481            ReplaySpeed::Custom(f32::NAN),
482        ];
483        let bars = speeds.map(|speed| {
484            let mut state = ReplayState::new();
485            state.total_bars = 100;
486            state.set_speed(speed);
487            state.play();
488            state.last_update =
489                Instant::now().checked_sub(std::time::Duration::from_millis(10_500));
490            state.update()
491        });
492        // 10.5 s at 1 s per bar: a speed plays clamped to 0.1x..100x, and at 1x when not finite.
493        assert_eq!(bars, [10, 1, 10, 1, 10]);
494    }
495
496    #[test]
497    fn test_markers() {
498        let mut state = ReplayState::new();
499        state.curr_bar = 5;
500
501        state.add_marker(Some("Test".to_string()), None);
502        assert_eq!(state.markers.len(), 1);
503        assert_eq!(state.markers[0].bar_idx, 5);
504
505        state.remove_marker(5);
506        assert!(state.markers.is_empty());
507    }
508}