1
2#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum State {
7 Idle,
8 Playing { token_index: usize },
9 Paused { token_index: usize },
10 Finished,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum Event {
15 Play,
16 Pause,
17 Resume,
18 SkipForward(usize),
19 SkipBackward(usize),
20 Stop,
21 SpeedUp(u32),
22 SpeedDown(u32),
23 TokenAdvanced,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub enum StateChange {
28 Transition(State, State),
29 Stay(State),
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum StateError {
34 NoTokens,
35 OutOfRange { index: usize, total: usize },
36 InvalidTransition { from: State, event: Event },
37}
38
39pub struct ReadingState {
40 state: State,
41 token_count: usize,
42 wpm: u32,
43}
44
45impl ReadingState {
46 pub fn new(token_count: usize, wpm: u32) -> Self {
47 Self {
48 state: State::Idle,
49 token_count,
50 wpm,
51 }
52 }
53
54 pub fn current_state(&self) -> State {
55 self.state
56 }
57
58 pub fn current_index(&self) -> Option<usize> {
59 match self.state {
60 State::Playing { token_index } | State::Paused { token_index } => Some(token_index),
61 _ => None,
62 }
63 }
64
65 pub fn wpm(&self) -> u32 {
66 self.wpm
67 }
68
69 pub fn transition(&mut self, event: Event) -> Result<StateChange, StateError> {
70 if self.token_count == 0 {
71 return Err(StateError::NoTokens);
72 }
73 match (self.state, event) {
74 (State::Idle, Event::Play) => {
75 self.state = State::Playing { token_index: 0 };
76 Ok(StateChange::Transition(State::Idle, self.state))
77 }
78 (State::Playing { token_index }, Event::Pause) => {
79 self.state = State::Paused { token_index };
80 Ok(StateChange::Transition(
81 State::Playing { token_index },
82 self.state,
83 ))
84 }
85 (State::Paused { token_index }, Event::Resume) => {
86 self.state = State::Playing { token_index };
87 Ok(StateChange::Transition(
88 State::Paused { token_index },
89 self.state,
90 ))
91 }
92 (State::Paused { token_index }, Event::SkipForward(n)) => {
93 let new_index = (token_index + n).min(self.token_count - 1);
94 if new_index >= self.token_count - 1 {
95 self.state = State::Finished;
96 } else {
97 self.state = State::Paused { token_index: new_index };
98 }
99 Ok(StateChange::Transition(
100 State::Paused { token_index },
101 self.state,
102 ))
103 }
104 (State::Paused { token_index }, Event::SkipBackward(n)) => {
105 let new_index = token_index.saturating_sub(n);
106 self.state = State::Paused {
107 token_index: new_index,
108 };
109 Ok(StateChange::Transition(
110 State::Paused { token_index },
111 self.state,
112 ))
113 }
114 (State::Playing { token_index }, Event::TokenAdvanced) => {
115 let new_index = token_index + 1;
116 if new_index >= self.token_count {
117 self.state = State::Finished;
118 } else {
119 self.state = State::Playing {
120 token_index: new_index,
121 };
122 }
123 Ok(StateChange::Transition(
124 State::Playing { token_index },
125 self.state,
126 ))
127 }
128 (State::Paused { .. }, Event::SpeedUp(delta)) => {
129 self.wpm = (self.wpm + delta).min(1000);
130 Ok(StateChange::Stay(self.state))
131 }
132 (State::Paused { .. }, Event::SpeedDown(delta)) => {
133 self.wpm = self.wpm.saturating_sub(delta).max(50);
134 Ok(StateChange::Stay(self.state))
135 }
136 (state, Event::Stop) => {
137 self.state = State::Idle;
138 Ok(StateChange::Transition(state, State::Idle))
139 }
140 (_, Event::Play) | (_, Event::Pause) | (_, Event::Resume) => {
141 Err(StateError::InvalidTransition {
142 from: self.state,
143 event,
144 })
145 }
146 (_, Event::SkipForward(_))
147 | (_, Event::SkipBackward(_))
148 | (_, Event::SpeedUp(_))
149 | (_, Event::SpeedDown(_)) => Err(StateError::InvalidTransition {
150 from: self.state,
151 event,
152 }),
153 (_, Event::TokenAdvanced) => Err(StateError::InvalidTransition {
154 from: self.state,
155 event,
156 }),
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn make_state() -> ReadingState {
166 ReadingState::new(10, 300)
167 }
168
169 #[test]
170 fn test_initial_state() {
171 let s = make_state();
172 assert_eq!(s.current_state(), State::Idle);
173 }
174
175 #[test]
176 fn test_play_from_idle() {
177 let mut s = make_state();
178 let result = s.transition(Event::Play);
179 assert!(result.is_ok());
180 assert_eq!(s.current_index(), Some(0));
181 }
182
183 #[test]
184 fn test_pause_resume() {
185 let mut s = make_state();
186 s.transition(Event::Play).unwrap();
187 s.transition(Event::Pause).unwrap();
188 assert_eq!(s.current_state(), State::Paused { token_index: 0 });
189 s.transition(Event::Resume).unwrap();
190 assert_eq!(s.current_state(), State::Playing { token_index: 0 });
191 }
192
193 #[test]
194 fn test_skip_forward() {
195 let mut s = make_state();
196 s.transition(Event::Play).unwrap();
197 s.transition(Event::Pause).unwrap();
198 s.transition(Event::SkipForward(3)).unwrap();
199 assert_eq!(s.current_index(), Some(3));
200 }
201
202 #[test]
203 fn test_skip_backward() {
204 let mut s = make_state();
205 s.transition(Event::Play).unwrap();
206 s.transition(Event::Pause).unwrap();
207 s.transition(Event::SkipForward(5)).unwrap();
208 s.transition(Event::SkipBackward(2)).unwrap();
209 assert_eq!(s.current_index(), Some(3));
210 }
211
212 #[test]
213 fn test_token_advanced_to_finished() {
214 let mut s = ReadingState::new(1, 300);
215 s.transition(Event::Play).unwrap();
216 let result = s.transition(Event::TokenAdvanced);
217 assert!(result.is_ok());
218 assert_eq!(s.current_state(), State::Finished);
219 }
220
221 #[test]
222 fn test_speed_adjustment() {
223 let mut s = make_state();
224 s.transition(Event::Play).unwrap();
225 s.transition(Event::Pause).unwrap();
226 s.transition(Event::SpeedUp(50)).unwrap();
227 assert_eq!(s.wpm(), 350);
228 s.transition(Event::SpeedDown(100)).unwrap();
229 assert_eq!(s.wpm(), 250);
230 }
231
232 #[test]
233 fn test_invalid_transition() {
234 let mut s = make_state();
235 let result = s.transition(Event::Pause);
237 assert!(matches!(result, Err(StateError::InvalidTransition { .. })));
238 }
239
240 #[test]
241 fn test_no_tokens() {
242 let mut s = ReadingState::new(0, 300);
243 let result = s.transition(Event::Play);
244 assert_eq!(result, Err(StateError::NoTokens));
245 }
246}