egui_charts/ui/replay/
session.rs1use super::{ReplayAction, ReplayController};
4use crate::model::ReplaySession;
5use chrono::{DateTime, Utc};
6
7impl ReplayController {
8 pub fn seek_cutoff(&mut self, time: DateTime<Utc>, clock: &crate::model::BarClock) -> bool {
11 let end = self.ts.partition_point(|&open| {
12 clock
13 .next_open(clock.bar_open(open))
14 .map_or(open < time, |close| close <= time)
15 });
16 if end == 0 {
17 return false;
18 }
19 self.handle_action(ReplayAction::JumpToBar(end - 1));
20 true
21 }
22
23 pub fn cutoff(&self, clock: &crate::model::BarClock) -> Option<DateTime<Utc>> {
26 if !self.state.is_active() {
27 return None;
28 }
29 let time = *self.curr_ts()?;
30 clock
31 .next_open(clock.bar_open(time))
32 .or_else(|| time.checked_add_signed(chrono::TimeDelta::nanoseconds(1)))
33 }
34
35 pub fn session(&self) -> Option<ReplaySession> {
37 self.state.is_active().then_some(ReplaySession {
38 symbol: self.state.symbol.clone().unwrap_or_default(),
39 time: *self.curr_ts()?,
40 speed: self.state.speed.multiplier(),
41 })
42 }
43
44 pub fn restore_session(&mut self, session: &ReplaySession) -> bool {
48 if self.state.symbol.as_deref().unwrap_or_default() != session.symbol
49 || !session.speed.is_finite()
50 || !(0.1..=100.0).contains(&session.speed)
51 {
52 return false;
53 }
54 let Ok(index) = self.ts.binary_search(&session.time) else {
55 return false;
56 };
57 self.handle_action(ReplayAction::EnterReplayMode);
58 self.handle_action(ReplayAction::SetSpeed(session.speed));
59 self.handle_action(ReplayAction::JumpToBar(index));
60 self.handle_action(ReplayAction::Pause);
61 true
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::model::{Bar, BarClock, Timeframe};
69
70 #[test]
71 fn checkpoints_require_matching_history_and_restore_paused() {
72 let bars: Vec<_> = (0..10)
73 .map(|minute| {
74 Bar::new(
75 DateTime::from_timestamp(1_704_067_200 + minute * 60, 0).unwrap(),
76 10.0,
77 12.0,
78 9.0,
79 11.0,
80 1.0,
81 )
82 })
83 .collect();
84 let mut replay = ReplayController::new(bars.clone(), vec![]).with_symbol("TEST");
85 replay.handle_action(ReplayAction::JumpToBar(4));
86 replay.handle_action(ReplayAction::SetSpeed(3.0));
87 replay.handle_action(ReplayAction::Play);
88 let saved = replay.session().unwrap();
89 let saved: ReplaySession =
90 serde_json::from_str(&serde_json::to_string(&saved).unwrap()).unwrap();
91 replay.handle_action(ReplayAction::JumpToEnd);
92 assert!(replay.restore_session(&saved));
93 assert_eq!(replay.state.curr_bar, 4);
94 assert!(!replay.state.is_playing());
95 assert_eq!(replay.state.speed.multiplier(), 3.0);
96 let clock = BarClock::new(Timeframe::Min1);
97 assert_eq!(replay.cutoff(&clock), Some(bars[5].time));
98 for invalid in [
99 ReplaySession {
100 symbol: "OTHER".into(),
101 ..saved.clone()
102 },
103 ReplaySession {
104 time: bars[0].time - chrono::TimeDelta::minutes(1),
105 ..saved.clone()
106 },
107 ReplaySession {
108 speed: f32::NAN,
109 ..saved.clone()
110 },
111 ReplaySession {
112 speed: 0.0,
113 ..saved.clone()
114 },
115 ] {
116 assert!(!replay.restore_session(&invalid));
117 assert_eq!(replay.state.curr_bar, 4);
118 }
119 assert!(!replay.seek_cutoff(bars[0].time, &clock));
120 assert!(replay.seek_cutoff(bars[8].time, &clock));
121 assert_eq!(replay.state.curr_bar, 7);
122 replay.handle_action(ReplayAction::ExitReplayMode);
123 assert!(replay.cutoff(&clock).is_none());
124 replay.load_data(vec![], vec![], "TEST".into());
125 assert!(replay.session().is_none());
126 assert!(!replay.state.is_active());
127 }
128}