screen_app/
editor_session.rs1#![allow(
15 clippy::needless_pass_by_value,
16 reason = "Tauri injects State<'_, T> into #[command] fns by value; it is borrowed, not moved"
17)]
18
19use std::sync::Mutex;
20use std::time::Duration;
21
22use playback::EditorPlayer;
23use serde::{Deserialize, Serialize};
24use tauri::State;
25
26#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
28pub struct EditorStatusView {
29 pub current_frame: u64,
31 pub duration_frames: u64,
33 pub playing: bool,
35 pub fps: u32,
37 pub rate: f32,
39 pub in_frame: u64,
41 pub out_frame: u64,
43 pub looping: bool,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Deserialize)]
50#[serde(tag = "kind", rename_all = "snake_case")]
51pub enum TransportAction {
52 Play,
54 Pause,
56 TogglePlay,
58 Tick {
60 dt_ms: u32,
62 },
63 Seek {
65 frame: u64,
67 },
68 Step {
70 delta: i64,
72 },
73 SetRate {
75 rate: f32,
77 },
78 SetInOut {
80 a: u64,
82 b: u64,
84 },
85 ClearInOut,
87 SetLooping {
89 looping: bool,
91 },
92 SetDuration {
95 frames: u64,
97 },
98 Status,
100}
101
102pub struct EditorSession {
104 player: EditorPlayer,
105}
106
107impl EditorSession {
108 #[must_use]
110 pub fn new(fps: u32, duration_frames: u64) -> Self {
111 Self {
112 player: EditorPlayer::new(fps, duration_frames),
113 }
114 }
115
116 pub fn apply(&mut self, action: TransportAction) -> EditorStatusView {
118 match action {
119 TransportAction::Play => self.player.play(),
120 TransportAction::Pause => self.player.pause(),
121 TransportAction::TogglePlay => self.player.toggle_play(),
122 TransportAction::Tick { dt_ms } => {
123 self.player.tick(Duration::from_millis(u64::from(dt_ms)));
124 }
125 TransportAction::Seek { frame } => self.player.seek(frame),
126 TransportAction::Step { delta } => self.player.step(delta),
127 TransportAction::SetRate { rate } => self.player.set_rate(rate),
128 TransportAction::SetInOut { a, b } => self.player.set_in_out(a, b),
129 TransportAction::ClearInOut => self.player.clear_in_out(),
130 TransportAction::SetLooping { looping } => self.player.set_looping(looping),
131 TransportAction::SetDuration { frames } => self.player.set_duration(frames),
132 TransportAction::Status => {}
133 }
134 self.status()
135 }
136
137 #[must_use]
139 pub fn status(&self) -> EditorStatusView {
140 EditorStatusView {
141 current_frame: self.player.current_frame(),
142 duration_frames: self.player.duration_frames(),
143 playing: self.player.is_playing(),
144 fps: self.player.fps(),
145 rate: self.player.rate(),
146 in_frame: self.player.in_frame(),
147 out_frame: self.player.out_frame(),
148 looping: self.player.looping(),
149 }
150 }
151}
152
153#[derive(Default)]
155pub struct EditorSessionState(pub Mutex<Option<EditorSession>>);
156
157impl std::fmt::Debug for EditorSessionState {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("EditorSessionState").finish_non_exhaustive()
160 }
161}
162
163#[tauri::command]
166pub fn editor_transport(
167 action: TransportAction,
168 state: State<'_, EditorSessionState>,
169) -> Option<EditorStatusView> {
170 let mut guard = state
171 .0
172 .lock()
173 .unwrap_or_else(std::sync::PoisonError::into_inner);
174 let session = guard.as_mut()?;
175 Some(session.apply(action))
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn session() -> EditorSession {
183 EditorSession::new(30, 900)
184 }
185
186 #[test]
187 fn play_pause_reflected_in_status() {
188 let mut s = session();
189 assert!(!s.status().playing);
190 assert!(s.apply(TransportAction::Play).playing);
191 assert!(!s.apply(TransportAction::Pause).playing);
192 assert!(s.apply(TransportAction::TogglePlay).playing);
193 }
194
195 #[test]
196 fn tick_advances_while_playing() {
197 let mut s = session();
198 s.apply(TransportAction::Play);
199 let st = s.apply(TransportAction::Tick { dt_ms: 1000 }); assert_eq!(st.current_frame, 30);
201 }
202
203 #[test]
204 fn seek_and_step() {
205 let mut s = session();
206 assert_eq!(
207 s.apply(TransportAction::Seek { frame: 100 }).current_frame,
208 100
209 );
210 assert_eq!(
211 s.apply(TransportAction::Step { delta: 1 }).current_frame,
212 101
213 );
214 let st = s.apply(TransportAction::Step { delta: -5 });
215 assert_eq!(st.current_frame, 96);
216 assert!(!st.playing, "stepping pauses");
217 }
218
219 #[test]
220 fn rate_and_in_out_and_loop_in_status() {
221 let mut s = session();
222 let st = s.apply(TransportAction::SetRate { rate: 2.0 });
223 assert!((st.rate - 2.0).abs() < 1e-6);
224 let st = s.apply(TransportAction::SetInOut { a: 200, b: 100 });
225 assert_eq!((st.in_frame, st.out_frame), (100, 200));
226 assert!(
227 s.apply(TransportAction::SetLooping { looping: true })
228 .looping
229 );
230 let st = s.apply(TransportAction::ClearInOut);
231 assert_eq!((st.in_frame, st.out_frame), (0, 900));
232 }
233
234 #[test]
235 fn status_action_is_a_pure_read() {
236 let mut s = session();
237 s.apply(TransportAction::Seek { frame: 42 });
238 let a = s.apply(TransportAction::Status);
239 let b = s.apply(TransportAction::Status);
240 assert_eq!(a, b);
241 assert_eq!(a.current_frame, 42);
242 }
243}