1mod 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#[derive(Default)]
64pub struct ReplayController {
65 pub state: ReplayState,
67 pub config: ReplayConfig,
69 bars: Vec<Bar>,
71 ts: Vec<DateTime<Utc>>,
73}
74
75impl ReplayController {
76 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 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 pub fn with_config(mut self, config: ReplayConfig) -> Self {
99 self.config = config;
100 self
101 }
102
103 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
105 self.state.symbol = Some(symbol.into());
106 self
107 }
108
109 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 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 pub fn has_data(&self) -> bool {
157 !self.bars.is_empty()
158 }
159
160 pub fn curr_bar(&self) -> Option<&Bar> {
162 self.bars.get(self.state.curr_bar)
163 }
164
165 pub fn curr_ts(&self) -> Option<&DateTime<Utc>> {
167 self.ts.get(self.state.curr_bar)
168 }
169
170 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 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 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 pub fn handle_action(&mut self, action: ReplayAction) {
191 match action {
192 ReplayAction::None => {}
193
194 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 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 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 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 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 ReplayAction::LoadData { symbol, start, end } => {
272 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 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 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 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 fn find_bar_at_date(&self, target: &DateTime<Utc>) -> Option<usize> {
320 if self.ts.is_empty() {
321 return None;
322 }
323
324 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 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 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 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 (
478 |c| {
479 c.handle_action(ReplayAction::JumpToBar(30));
480 c.handle_action(ReplayAction::Play);
481 },
482 (1, PlaybackState::Playing, days[1]),
483 ),
484 (
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); }
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}