1use super::actions::ReplaySpeed;
6use chrono::{DateTime, Utc};
7use web_time::Instant;
8
9#[derive(Debug, Clone)]
11pub struct ReplayMarker {
12 pub bar_idx: usize,
14 pub label: Option<String>,
16 pub color: [u8; 4],
18 pub created_at: DateTime<Utc>,
20}
21
22impl ReplayMarker {
23 pub fn new(bar_idx: usize) -> Self {
25 Self {
26 bar_idx,
27 label: None,
28 color: [255, 165, 0, 255], created_at: Utc::now(),
30 }
31 }
32
33 pub fn with_label(mut self, label: impl Into<String>) -> Self {
35 self.label = Some(label.into());
36 self
37 }
38
39 pub fn with_color(mut self, color: [u8; 4]) -> Self {
41 self.color = color;
42 self
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum PlaybackState {
49 #[default]
51 Inactive,
52 Paused,
54 Playing,
56 Finished,
58}
59
60impl PlaybackState {
61 pub fn is_active(&self) -> bool {
63 !matches!(self, PlaybackState::Inactive)
64 }
65
66 pub fn is_playing(&self) -> bool {
68 matches!(self, PlaybackState::Playing)
69 }
70
71 pub fn is_paused(&self) -> bool {
73 matches!(self, PlaybackState::Paused)
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct ReplayState {
80 pub playback_state: PlaybackState,
83 pub speed: ReplaySpeed,
85
86 pub curr_bar: usize,
89 pub total_bars: usize,
91 pub visible_bars: usize,
93
94 pub start_date: Option<DateTime<Utc>>,
97 pub end_date: Option<DateTime<Utc>>,
99 pub curr_date: Option<DateTime<Utc>>,
101
102 pub symbol: Option<String>,
105
106 pub speed_dropdown_open: bool,
109 pub date_picker_open: bool,
111 pub settings_open: bool,
113
114 pub markers: Vec<ReplayMarker>,
117
118 last_update: Option<Instant>,
121 accumulated_time: f64,
123 bar_duration_secs: f64,
125}
126
127impl ReplayState {
128 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, }
148 }
149
150 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 pub fn set_bar_duration(&mut self, seconds: f64) {
171 self.bar_duration_secs = seconds.max(0.001);
172 }
173
174 pub fn is_active(&self) -> bool {
176 self.playback_state.is_active()
177 }
178
179 pub fn is_playing(&self) -> bool {
181 self.playback_state.is_playing()
182 }
183
184 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 pub fn remaining_bars(&self) -> usize {
195 self.total_bars.saturating_sub(self.curr_bar + 1)
196 }
197
198 pub fn at_end(&self) -> bool {
200 self.curr_bar >= self.total_bars.saturating_sub(1)
201 }
202
203 pub fn at_start(&self) -> bool {
205 self.curr_bar == 0
206 }
207
208 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 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 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 self.curr_bar = 0;
235 self.play();
236 }
237 PlaybackState::Inactive => {}
238 }
239 }
240
241 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 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 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 pub fn exit_replay_mode(&mut self) {
270 self.stop();
271 }
272
273 pub fn step_forward(&mut self) {
277 if !self.at_end() {
278 self.curr_bar += 1;
279 }
280 self.sync_finished();
281 }
282
283 pub fn step_backward(&mut self) {
285 self.curr_bar = self.curr_bar.saturating_sub(1);
286 self.sync_finished();
287 }
288
289 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 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 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 pub fn set_speed(&mut self, speed: ReplaySpeed) {
317 self.speed = speed;
318 }
319
320 pub fn speed_up(&mut self) {
322 self.speed = self.speed.faster();
323 }
324
325 pub fn slow_down(&mut self) {
327 self.speed = self.speed.slower();
328 }
329
330 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 pub fn remove_marker(&mut self, bar_idx: usize) {
346 self.markers.retain(|m| m.bar_idx != bar_idx);
347 }
348
349 pub fn clear_markers(&mut self) {
351 self.markers.clear();
352 }
353
354 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 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 self.accumulated_time += delta * self.speed.multiplier() as f64;
381
382 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 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 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 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 self.curr_bar = current_bar;
435 self.total_bars = total_bars;
436
437 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 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 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}