speed_reader_core/
state.rs1#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum State {
6 Idle,
7 Playing { token_index: usize },
8 Paused { token_index: usize },
9 Finished,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Event {
14 Play, Pause, Resume,
15 SkipForward(usize), SkipBackward(usize),
16 Stop, SpeedUp(u32), SpeedDown(u32), TokenAdvanced,
17}
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum StateChange { Transition(State, State), Stay(State) }
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum StateError { NoTokens, OutOfRange { index: usize, total: usize }, InvalidTransition { from: State, event: Event } }
24
25pub struct ReadingState {
26 state: State,
27 token_count: usize,
28 wpm: u32,
29}
30
31fn skip_forward(state: &mut State, idx: usize, n: usize, total: usize) {
32 let new = (idx + n).min(total - 1);
33 *state = if new >= total - 1 { State::Finished } else { State::Playing { token_index: new } };
34}
35
36fn skip_backward(state: &mut State, idx: usize, n: usize) {
37 *state = State::Playing { token_index: idx.saturating_sub(n) };
38}
39
40impl ReadingState {
41 pub fn new(token_count: usize, wpm: u32) -> Self {
42 Self { state: State::Idle, token_count, wpm }
43 }
44
45 pub fn current_state(&self) -> State { self.state }
46
47 pub fn current_index(&self) -> Option<usize> {
48 match self.state {
49 State::Playing { token_index } | State::Paused { token_index } => Some(token_index),
50 _ => None,
51 }
52 }
53
54 pub fn wpm(&self) -> u32 { self.wpm }
55
56 pub fn transition(&mut self, event: Event) -> Result<StateChange, StateError> {
57 if self.token_count == 0 { return Err(StateError::NoTokens) }
58 match (self.state, event) {
59 (State::Idle, Event::Play) => {
60 self.state = State::Playing { token_index: 0 };
61 Ok(StateChange::Transition(State::Idle, self.state))
62 }
63 (State::Playing { token_index }, Event::Pause) => {
64 self.state = State::Paused { token_index };
65 Ok(StateChange::Transition(State::Playing { token_index }, self.state))
66 }
67 (State::Paused { token_index }, Event::Resume) => {
68 self.state = State::Playing { token_index };
69 Ok(StateChange::Transition(State::Paused { token_index }, self.state))
70 }
71 (State::Playing { token_index } | State::Paused { token_index }, Event::SkipForward(n)) => {
72 let old = self.state;
73 skip_forward(&mut self.state, token_index, n, self.token_count);
74 Ok(StateChange::Transition(old, self.state))
75 }
76 (State::Playing { token_index } | State::Paused { token_index }, Event::SkipBackward(n)) => {
77 let old = self.state;
78 skip_backward(&mut self.state, token_index, n);
79 Ok(StateChange::Transition(old, self.state))
80 }
81 (State::Playing { token_index }, Event::TokenAdvanced) => {
82 let new = token_index + 1;
83 let old = self.state;
84 self.state = if new >= self.token_count { State::Finished } else { State::Playing { token_index: new } };
85 Ok(StateChange::Transition(old, self.state))
86 }
87 (State::Playing { .. } | State::Paused { .. }, Event::SpeedUp(delta)) => {
88 self.wpm = (self.wpm + delta).min(1000);
89 Ok(StateChange::Stay(self.state))
90 }
91 (State::Playing { .. } | State::Paused { .. }, Event::SpeedDown(delta)) => {
92 self.wpm = self.wpm.saturating_sub(delta).max(50);
93 Ok(StateChange::Stay(self.state))
94 }
95 (st, Event::Stop) => {
96 self.state = State::Idle;
97 Ok(StateChange::Transition(st, State::Idle))
98 }
99 _ => Err(StateError::InvalidTransition { from: self.state, event }),
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 fn ms() -> ReadingState { ReadingState::new(10, 300) }
108
109 #[test] fn init() { assert_eq!(ms().current_state(), State::Idle); }
110 #[test] fn play() { let mut s = ms(); s.transition(Event::Play).unwrap(); assert_eq!(s.current_index(), Some(0)); }
111 #[test] fn pause_resume() {
112 let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
113 assert_eq!(s.current_state(), State::Paused { token_index: 0 });
114 s.transition(Event::Resume).unwrap();
115 assert_eq!(s.current_state(), State::Playing { token_index: 0 });
116 }
117 #[test] fn skip_fwd() {
118 let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
119 s.transition(Event::SkipForward(3)).unwrap(); assert_eq!(s.current_index(), Some(3));
120 }
121 #[test] fn skip_bwd() {
122 let mut s = ms(); s.transition(Event::Play).unwrap(); s.transition(Event::Pause).unwrap();
123 s.transition(Event::SkipForward(5)).unwrap(); s.transition(Event::SkipBackward(2)).unwrap();
124 assert_eq!(s.current_index(), Some(3));
125 }
126 #[test] fn skip_while_playing() {
127 let mut s = ms(); s.transition(Event::Play).unwrap();
128 s.transition(Event::SkipForward(3)).unwrap();
129 assert_eq!(s.current_index(), Some(3));
130 assert!(matches!(s.current_state(), State::Playing { .. }));
131 }
132 #[test] fn speed_while_playing() {
133 let mut s = ms(); s.transition(Event::Play).unwrap();
134 s.transition(Event::SpeedUp(50)).unwrap();
135 assert_eq!(s.wpm(), 350);
136 }
137 #[test] fn finish() {
138 let mut s = ReadingState::new(1, 300);
139 s.transition(Event::Play).unwrap();
140 s.transition(Event::TokenAdvanced).unwrap();
141 assert_eq!(s.current_state(), State::Finished);
142 }
143 #[test] fn invalid() {
144 let mut s = ms();
145 assert!(matches!(s.transition(Event::Pause), Err(StateError::InvalidTransition { .. })));
146 }
147 #[test] fn no_tokens() {
148 assert!(matches!(ReadingState::new(0, 300).transition(Event::Play), Err(StateError::NoTokens)));
149 }
150}