1use std::sync::{Arc, Condvar, Mutex, MutexGuard};
2use std::thread::ThreadId;
3use std::time::{Duration, Instant};
4
5use crossbeam_channel::{Receiver, Sender, bounded, unbounded};
6
7use crate::{AnimationHandle, DesktopError, FinalCommitOutcome, FinishReason, IconAnimationState, IconId};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum TimelineCloseMode {
11 RestoreOrigins,
12 LeaveInPlace,
13 TeleportToTarget,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum TimelineState {
18 Paused,
19 Playing,
20 Closed,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum PlaybackOutcome {
25 Reached,
26 Interrupted,
27 Closed,
28}
29
30struct State {
31 position: f64,
32 speed: f64,
33 phase: TimelineState,
34 worker: Option<ThreadId>,
35 icons: Vec<IconAnimationState>,
36 real_icons_visible: bool,
37}
38
39type Shared = Arc<Mutex<State>>;
40
41fn lock(shared: &Shared) -> MutexGuard<'_, State> {
42 shared.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
43}
44
45fn check_thread(shared: &Shared) -> Result<(), DesktopError> {
46 if lock(shared).worker == Some(std::thread::current().id()) {
47 return Err(DesktopError::BackendUnavailable("blocking timeline calls cannot run on the animation worker".into()));
48 }
49 Ok(())
50}
51
52fn closed_error() -> DesktopError {
53 DesktopError::BackendUnavailable("timeline session is closed".into())
54}
55
56pub struct PlaybackHandle {
57 result: Arc<Completion>,
58 shared: Shared,
59}
60
61#[derive(Default)]
62struct Completion {
63 outcome: Mutex<Option<PlaybackOutcome>>,
64 ready: Condvar,
65}
66
67impl Completion {
68 fn complete(&self, outcome: PlaybackOutcome) {
69 *self.outcome.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(outcome);
70 self.ready.notify_all();
71 }
72}
73
74impl PlaybackHandle {
75 pub fn wait(&self) -> Result<PlaybackOutcome, DesktopError> {
76 check_thread(&self.shared)?;
77 let state = self.result.outcome.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
78 let state = self.result.ready.wait_while(state, |outcome| outcome.is_none())
79 .unwrap_or_else(std::sync::PoisonError::into_inner);
80 Ok(state.unwrap())
81 }
82
83 pub fn wait_timeout(&self, timeout: Duration) -> Result<Option<PlaybackOutcome>, DesktopError> {
84 check_thread(&self.shared)?;
85 let state = self.result.outcome.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
86 let (state, _) = self.result.ready.wait_timeout_while(state, timeout, |outcome| outcome.is_none())
87 .unwrap_or_else(std::sync::PoisonError::into_inner);
88 Ok(*state)
89 }
90}
91
92pub struct TimelineSession {
93 tx: Sender<Request>,
94 shared: Shared,
95 handle: AnimationHandle,
96}
97
98impl TimelineSession {
99 pub(crate) fn pair(handle: AnimationHandle) -> (Self, Runtime) {
100 let (tx, rx) = unbounded();
101 let shared = Arc::new(Mutex::new(State {
102 position: 0.0, speed: 1.0, phase: TimelineState::Paused,
103 worker: None, icons: Vec::new(), real_icons_visible: false,
104 }));
105 let runtime = Runtime {
106 rx, shared: shared.clone(), clock: Clock::new(0.0, Instant::now()),
107 playback: None, acknowledgements: Vec::new(), captures: Vec::new(), reached: false,
108 };
109 (Self { tx, shared, handle }, runtime)
110 }
111
112 fn request(&self, command: Command) -> Result<(), DesktopError> {
113 check_thread(&self.shared)?;
114 let (reply, response) = bounded(1);
115 self.tx.send(Request { command, reply }).map_err(|_| closed_error())?;
116 response.recv().map_err(|_| closed_error())?
117 }
118
119 pub fn play_to(&self, position: f64, speed: f64) -> Result<PlaybackHandle, DesktopError> {
120 validate_position(position)?;
121 validate_speed(speed)?;
122 let result = Arc::new(Completion::default());
123 self.request(Command::Play { position, speed, result: result.clone() })?;
124 Ok(PlaybackHandle { result, shared: self.shared.clone() })
125 }
126
127 pub fn seek(&self, position: f64) -> Result<(), DesktopError> {
128 validate_position(position)?;
129 self.request(Command::Seek(position))
130 }
131
132 pub fn pause(&self) -> Result<(), DesktopError> { self.request(Command::Pause) }
133
134 pub fn capture(&self) -> Result<crate::CapturedFrame, DesktopError> {
135 let (reply, response) = bounded(1);
136 self.request(Command::Capture(reply))?;
137 response.recv().map_err(|_| closed_error())?
138 }
139
140 pub fn seek_and_capture(&self, position: f64) -> Result<crate::CapturedFrame, DesktopError> {
141 validate_position(position)?;
142 let (reply, response) = bounded(1);
143 self.request(Command::SeekCapture(position, reply))?;
144 response.recv().map_err(|_| closed_error())?
145 }
146
147 pub fn set_speed(&self, speed: f64) -> Result<(), DesktopError> {
148 validate_speed(speed)?;
149 self.request(Command::Speed(speed))
150 }
151
152 pub fn position(&self) -> f64 { lock(&self.shared).position }
153 pub fn real_icons_visible(&self) -> bool { lock(&self.shared).real_icons_visible }
154
155 pub fn set_real_icons_visible(&self, visible: bool) -> Result<(), DesktopError> {
156 self.request(Command::RealIconsVisible(visible))
157 }
158 pub fn speed(&self) -> f64 { lock(&self.shared).speed }
159 pub fn state(&self) -> TimelineState { lock(&self.shared).phase }
160 pub fn snapshot(&self) -> Vec<IconAnimationState> { lock(&self.shared).icons.clone() }
161 pub fn missing_icons(&self) -> Vec<IconId> { self.handle.missing_icons() }
162 pub fn finish_reason(&self) -> Option<FinishReason> { self.handle.finish_reason() }
163 pub fn final_commit(&self) -> Option<FinalCommitOutcome> { self.handle.final_commit() }
164
165 pub fn close(&self, mode: TimelineCloseMode) -> Result<FinishReason, DesktopError> {
166 check_thread(&self.shared)?;
167 if self.state() != TimelineState::Closed {
168 let _ = self.request(Command::Close(mode));
169 }
170 Ok(self.handle.wait())
171 }
172}
173
174impl Drop for TimelineSession {
175 fn drop(&mut self) {
176 let (reply, _) = bounded(1);
177 let _ = self.tx.send(Request { command: Command::Close(TimelineCloseMode::RestoreOrigins), reply });
178 }
179}
180
181fn validate_position(position: f64) -> Result<(), DesktopError> {
182 if !position.is_finite() || !(0.0..=1.0).contains(&position) {
183 return Err(DesktopError::InvalidDuration("timeline position must be finite and within [0, 1]".into()));
184 }
185 Ok(())
186}
187
188fn validate_speed(speed: f64) -> Result<(), DesktopError> {
189 if !speed.is_finite() || speed <= 0.0 {
190 return Err(DesktopError::InvalidDuration("timeline speed must be finite and positive".into()));
191 }
192 Ok(())
193}
194
195pub(crate) struct Request {
196 command: Command,
197 reply: Sender<Result<(), DesktopError>>,
198}
199
200enum Command {
201 Capture(Sender<Result<crate::CapturedFrame, DesktopError>>),
202 SeekCapture(f64, Sender<Result<crate::CapturedFrame, DesktopError>>),
203 Play { position: f64, speed: f64, result: Arc<Completion> },
204 Seek(f64),
205 Pause,
206 Speed(f64),
207 RealIconsVisible(bool),
208 Close(TimelineCloseMode),
209}
210
211struct Clock {
212 duration: f64,
213 anchor: f64,
214 wall: Instant,
215 speed: f64,
216 destination: Option<f64>,
217}
218
219impl Clock {
220 fn new(duration: f64, now: Instant) -> Self {
221 Self { duration, anchor: 0.0, wall: now, speed: 1.0, destination: None }
222 }
223
224 fn position_at(&self, now: Instant) -> f64 {
225 let Some(destination) = self.destination else { return self.anchor; };
226 if self.duration == 0.0 { return destination; }
227 let distance = now.saturating_duration_since(self.wall).as_secs_f64() * self.speed / self.duration;
228 if destination >= self.anchor { (self.anchor + distance).min(destination) }
229 else { (self.anchor - distance).max(destination) }
230 }
231
232 fn reanchor(&mut self, now: Instant) {
233 self.anchor = self.position_at(now);
234 self.wall = now;
235 }
236}
237
238pub(crate) struct Runtime {
239 pub rx: Receiver<Request>,
240 shared: Shared,
241 clock: Clock,
242 playback: Option<Arc<Completion>>,
243 acknowledgements: Vec<Sender<Result<(), DesktopError>>>,
244 captures: Vec<Sender<Result<crate::CapturedFrame, DesktopError>>>,
245 reached: bool,
246}
247
248impl Runtime {
249 pub fn activate(&mut self, duration: f64) {
250 self.clock = Clock::new(duration, Instant::now());
251 lock(&self.shared).worker = Some(std::thread::current().id());
252 }
253
254 fn end_playback(&mut self, outcome: PlaybackOutcome) {
255 if let Some(reply) = self.playback.take() { reply.complete(outcome); }
256 }
257
258 pub fn control(&mut self, request: Request, now: Instant, backend: &mut dyn crate::DesktopBackend) -> Option<TimelineCloseMode> {
259 if let Command::Capture(reply) = request.command {
260 let seconds = lock(&self.shared).position * self.clock.duration;
261 let _ = reply.send(backend.capture_overlay(seconds));
262 let _ = request.reply.send(Ok(()));
263 return None;
264 }
265 if let Command::RealIconsVisible(visible) = request.command {
266 let result = backend.set_real_icons_visible(visible);
267 if result.is_ok() { lock(&self.shared).real_icons_visible = visible; }
268 let _ = request.reply.send(result);
269 return None;
270 }
271 self.clock.reanchor(now);
272 self.acknowledgements.push(request.reply);
273 match request.command {
274 Command::Capture(_) => unreachable!(),
275 Command::SeekCapture(position, reply) => {
276 self.end_playback(PlaybackOutcome::Interrupted);
277 self.clock.anchor = position;
278 self.clock.destination = None;
279 self.captures.push(reply);
280 }
281 Command::Play { position, speed, result } => {
282 self.end_playback(PlaybackOutcome::Interrupted);
283 self.clock.destination = Some(position);
284 self.clock.speed = speed;
285 self.playback = Some(result);
286 }
287 Command::Seek(position) => {
288 self.end_playback(PlaybackOutcome::Interrupted);
289 self.clock.anchor = position;
290 self.clock.destination = None;
291 }
292 Command::Pause => {
293 self.end_playback(PlaybackOutcome::Interrupted);
294 self.clock.destination = None;
295 }
296 Command::Speed(speed) => self.clock.speed = speed,
297 Command::RealIconsVisible(_) => unreachable!(),
298 Command::Close(mode) => return Some(mode),
299 }
300 None
301 }
302
303 pub fn sample(&mut self, now: Instant) -> (f64, f64) {
304 let position = self.clock.position_at(now);
305 self.reached = self.clock.destination == Some(position);
306 if self.reached {
307 self.clock.anchor = position;
308 self.clock.destination = None;
309 }
310 (position, position * self.clock.duration)
311 }
312
313 pub fn playing(&self) -> bool { self.clock.destination.is_some() }
314
315 pub fn capture_pending(&mut self, backend: &mut dyn crate::DesktopBackend, seconds: f64) {
316 for reply in self.captures.drain(..) { let _ = reply.send(backend.capture_overlay(seconds)); }
317 }
318
319 pub fn publish(&mut self, position: f64, icons: Vec<IconAnimationState>) {
320 {
321 let mut state = lock(&self.shared);
322 state.position = position;
323 state.speed = self.clock.speed;
324 state.phase = if self.playing() { TimelineState::Playing } else { TimelineState::Paused };
325 state.icons = icons;
326 }
327 if self.reached { self.end_playback(PlaybackOutcome::Reached); }
328 self.acknowledge();
329 }
330
331 pub fn acknowledge(&mut self) {
332 for reply in self.acknowledgements.drain(..) { let _ = reply.send(Ok(())); }
333 }
334
335 pub fn finish(&mut self) {
336 lock(&self.shared).phase = TimelineState::Closed;
337 self.end_playback(PlaybackOutcome::Closed);
338 }
339}
340
341impl Drop for Runtime {
342 fn drop(&mut self) {
343 self.finish();
344 for reply in self.acknowledgements.drain(..) { let _ = reply.send(Err(closed_error())); }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn anchored_clock_reverses_and_changes_speed_without_jumps() {
354 let now = Instant::now();
355 let mut clock = Clock::new(2.0, now);
356 clock.destination = Some(0.5);
357 assert_eq!(clock.position_at(now + Duration::from_secs(3)), 0.5);
358 clock.reanchor(now + Duration::from_millis(500));
359 assert_eq!(clock.anchor, 0.25);
360 clock.speed = 2.0;
361 assert_eq!(clock.position_at(now + Duration::from_millis(750)), 0.5);
362 clock.reanchor(now + Duration::from_millis(750));
363 clock.destination = Some(0.0);
364 assert_eq!(clock.position_at(now + Duration::from_secs(1)), 0.25);
365 clock.reanchor(now + Duration::from_secs(1));
366 clock.destination = None;
367 assert_eq!(clock.position_at(now + Duration::from_secs(100)), 0.25);
368 }
369
370 #[test]
371 fn rejects_invalid_controls() {
372 for value in [f64::NAN, f64::INFINITY, -0.1, 1.1] { assert!(validate_position(value).is_err()); }
373 for value in [f64::NAN, f64::INFINITY, -1.0, 0.0] { assert!(validate_speed(value).is_err()); }
374 }
375}