Skip to main content

rdi_core/
handle.rs

1//! Public [`AnimationHandle`] and the shared state it exposes.
2//!
3//! The handle is what user code manipulates while an animation is running.
4//! Internally it wraps an `Arc<HandleInner>` shared with the worker thread
5//! so both sides can update / observe the same state.
6
7use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
8use std::time::{Duration as StdDuration, Instant};
9
10use crossbeam_channel::Sender;
11
12use crate::engine::WorkerMsg;
13use crate::events::{
14    FinishReason, IconAnimationState, StartContext, StopMode, TickContext,
15};
16use crate::{FinalCommitOutcome, IconId};
17
18// ---------------------------------------------------------------------------
19// Observer callback types
20// ---------------------------------------------------------------------------
21
22pub(crate) type ObserverStart = Arc<dyn Fn(&StartContext) + Send + Sync>;
23pub(crate) type ObserverTick = Arc<dyn Fn(&TickContext) + Send + Sync>;
24pub(crate) type ObserverIconComplete = Arc<dyn Fn(&IconId) + Send + Sync>;
25pub(crate) type ObserverFinish = Arc<dyn Fn(&FinishReason) + Send + Sync>;
26
27#[derive(Default)]
28pub(crate) struct Observers {
29    pub on_start: Vec<ObserverStart>,
30    pub on_tick: Vec<ObserverTick>,
31    pub on_icon_complete: Vec<ObserverIconComplete>,
32    pub on_finish: Vec<ObserverFinish>,
33}
34
35/// Bundle of observers to attach *atomically* to a new animation.
36///
37/// [`AnimationHandle::on_*`](AnimationHandle) can only be called after the
38/// handle is returned, which races against the worker's first tick — a
39/// completion that fires before your `on_icon_complete` callback is
40/// registered will simply be lost. Passing observers through this bundle
41/// via [`DesktopController::animate_with_observers`](crate::DesktopController::animate_with_observers)
42/// installs them *before* the worker enters its tick loop.
43///
44/// ```
45/// # use rdi_core::PreObservers;
46/// let obs = PreObservers::new()
47///     .on_tick(|ctx| println!("tick #{}", ctx.finalized_icons))
48///     .on_finish(|reason| println!("finished: {reason:?}"));
49/// # let _ = obs;
50/// ```
51#[derive(Default, Clone)]
52pub struct PreObservers {
53    pub(crate) on_start: Vec<ObserverStart>,
54    pub(crate) on_tick: Vec<ObserverTick>,
55    pub(crate) on_icon_complete: Vec<ObserverIconComplete>,
56    pub(crate) on_finish: Vec<ObserverFinish>,
57}
58
59impl PreObservers {
60    #[inline]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    #[must_use]
66    pub fn on_start<F>(mut self, cb: F) -> Self
67    where
68        F: Fn(&StartContext) + Send + Sync + 'static,
69    {
70        self.on_start.push(Arc::new(cb));
71        self
72    }
73
74    #[must_use]
75    pub fn on_tick<F>(mut self, cb: F) -> Self
76    where
77        F: Fn(&TickContext) + Send + Sync + 'static,
78    {
79        self.on_tick.push(Arc::new(cb));
80        self
81    }
82
83    #[must_use]
84    pub fn on_icon_complete<F>(mut self, cb: F) -> Self
85    where
86        F: Fn(&IconId) + Send + Sync + 'static,
87    {
88        self.on_icon_complete.push(Arc::new(cb));
89        self
90    }
91
92    #[must_use]
93    pub fn on_finish<F>(mut self, cb: F) -> Self
94    where
95        F: Fn(&FinishReason) + Send + Sync + 'static,
96    {
97        self.on_finish.push(Arc::new(cb));
98        self
99    }
100}
101
102// ---------------------------------------------------------------------------
103// Shared state
104// ---------------------------------------------------------------------------
105
106#[derive(Default)]
107pub(crate) struct HandleState {
108    pub running: bool,
109    pub progress: f32,
110    pub per_icon: Vec<IconAnimationState>,
111    pub missing: Vec<IconId>,
112    pub finish_reason: Option<FinishReason>,
113    /// Result of the Shell-side final commit, once
114    /// `finalize_overlay_session` has returned. `None` while the
115    /// animation is still running, on the overlay-unavailable fallback
116    /// path, and when finalization itself failed.
117    pub final_commit: Option<FinalCommitOutcome>,
118}
119
120pub(crate) struct HandleInner {
121    /// Sender for control messages ([`WorkerMsg::AnimStop`], …). Cloned
122    /// from the controller's tx when the animation starts.
123    pub(crate) ctrl_tx: Sender<WorkerMsg>,
124    pub(crate) state: Mutex<HandleState>,
125    pub(crate) observers: Mutex<Observers>,
126    pub(crate) finish_cv: Condvar,
127}
128
129impl HandleInner {
130    pub(crate) fn new(ctrl_tx: Sender<WorkerMsg>) -> Arc<Self> {
131        Arc::new(Self {
132            ctrl_tx,
133            // Start in the "running" state so that a caller who immediately
134            // calls `wait` / `wait_timeout` on the returned handle does not
135            // observe a spurious "already finished" state before the worker
136            // has had a chance to enter its tick loop. The worker will flip
137            // `running` back to `false` when the animation truly ends
138            // (either via the tick loop's normal termination or through
139            // `finalize_with_error`).
140            state: Mutex::new(HandleState {
141                running: true,
142                progress: 0.0,
143                per_icon: Vec::new(),
144                missing: Vec::new(),
145                finish_reason: None,
146                final_commit: None,
147            }),
148            observers: Mutex::new(Observers::default()),
149            finish_cv: Condvar::new(),
150        })
151    }
152
153    /// Clone the current observer list under a short lock, so the caller
154    /// can fire callbacks without holding the state lock.
155    pub(crate) fn observers_snapshot(&self) -> Observers {
156        let g = self.lock_observers();
157        Observers {
158            on_start: g.on_start.clone(),
159            on_tick: g.on_tick.clone(),
160            on_icon_complete: g.on_icon_complete.clone(),
161            on_finish: g.on_finish.clone(),
162        }
163    }
164
165    /// Move the pre-configured observers from a [`PreObservers`] bundle
166    /// into the shared observer list. Called by the worker *before* it
167    /// hands the handle back to the caller so the observers are visible
168    /// on the very first tick.
169    pub(crate) fn install_pre_observers(&self, pre: PreObservers) {
170        let mut g = self.lock_observers();
171        g.on_start.extend(pre.on_start);
172        g.on_tick.extend(pre.on_tick);
173        g.on_icon_complete.extend(pre.on_icon_complete);
174        g.on_finish.extend(pre.on_finish);
175    }
176
177    /// Lock [`Self::state`], recovering from poisoning.
178    ///
179    /// A poisoned lock here means the worker panicked mid-animation.
180    /// `HandleState` is plain data — progress numbers, a reason enum,
181    /// per-icon snapshots — so there is no half-updated invariant a
182    /// panic could have left behind, and the worst a reader sees is a
183    /// stale value. Propagating the poison instead would turn one
184    /// worker panic into a permanently-panicking handle: every later
185    /// `progress()` / `wait()` / `snapshot()` would panic too, and
186    /// through PyO3 that surfaces as `PanicException` with no way back.
187    #[inline]
188    pub(crate) fn lock_state(&self) -> MutexGuard<'_, HandleState> {
189        self.state.lock().unwrap_or_else(PoisonError::into_inner)
190    }
191
192    /// Lock [`Self::observers`], recovering from poisoning. Same
193    /// rationale as [`Self::lock_state`] — the observer lists are
194    /// `Vec<Arc<dyn Fn>>` with no cross-field invariant.
195    #[inline]
196    pub(crate) fn lock_observers(&self) -> MutexGuard<'_, Observers> {
197        self.observers
198            .lock()
199            .unwrap_or_else(PoisonError::into_inner)
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Public handle
205// ---------------------------------------------------------------------------
206
207/// Handle returned by [`DesktopController::animate`](crate::DesktopController::animate).
208///
209/// The handle is `Clone`-able and thread-safe: user code can hand copies
210/// to different threads (e.g. one thread waiting on completion, another
211/// polling progress or issuing `stop`).
212#[derive(Clone)]
213pub struct AnimationHandle {
214    inner: Arc<HandleInner>,
215}
216
217impl AnimationHandle {
218    pub(crate) fn from_inner(inner: Arc<HandleInner>) -> Self {
219        Self { inner }
220    }
221
222    /// `true` while the animation is still ticking.
223    pub fn is_running(&self) -> bool {
224        self.inner.lock_state().running
225    }
226
227    /// Global progress `∈ [0, 1]` computed as the mean per-icon `t`.
228    pub fn progress(&self) -> f32 {
229        self.inner.lock_state().progress
230    }
231
232    /// Copy of the current per-icon state.
233    pub fn snapshot(&self) -> Vec<IconAnimationState> {
234        self.inner.lock_state().per_icon.clone()
235    }
236
237    /// Ids that were listed in the spec but were not present on the
238    /// desktop at animation start. Populated once, before the first tick.
239    pub fn missing_icons(&self) -> Vec<IconId> {
240        self.inner.lock_state().missing.clone()
241    }
242
243    /// Read the finish reason if the animation has already ended.
244    pub fn finish_reason(&self) -> Option<FinishReason> {
245        self.inner.lock_state().finish_reason.clone()
246    }
247
248    /// Outcome of the Shell-side final commit, available once the
249    /// animation has finished.
250    ///
251    /// `None` means the commit outcome is unknown: the animation is
252    /// still running, it took the overlay-unavailable fallback path, or
253    /// `finalize_overlay_session` itself failed (in which case
254    /// [`Self::finish_reason`] carries the error).
255    ///
256    /// Read [`FinalCommitOutcome::moved_ids`] carefully — the Windows
257    /// backend stops polling at the *first* confirmed icon, so a
258    /// non-empty `moved_ids` means "the Shell confirmed the commit
259    /// landed", not "here is every icon that moved".
260    /// [`FinalCommitOutcome::missing_ids`] is the complete list of ids
261    /// the backend could not resolve.
262    pub fn final_commit(&self) -> Option<FinalCommitOutcome> {
263        self.inner.lock_state().final_commit.clone()
264    }
265
266    // -------- animation control -----------------------------------------
267
268    /// Ask the engine to stop the animation. Returns immediately; use
269    /// [`Self::wait`] to await the actual finish.
270    ///
271    /// This is the **only** way to alter a running animation. To change
272    /// targets, curves or the icon set, stop, read [`Self::snapshot`] for
273    /// the settled positions, and start a fresh
274    /// [`animate`](crate::DesktopController::animate).
275    pub fn stop(&self, mode: StopMode) {
276        let _ = self.inner.ctrl_tx.send(WorkerMsg::AnimStop { mode, owner: Arc::downgrade(&self.inner) });
277    }
278
279    // -------- waiting ---------------------------------------------------
280
281    /// Block until the animation finishes and return the finish reason.
282    pub fn wait(&self) -> FinishReason {
283        let mut state = self.inner.lock_state();
284        while state.running {
285            state = self
286                .inner
287                .finish_cv
288                .wait(state)
289                .unwrap_or_else(PoisonError::into_inner);
290        }
291        state
292            .finish_reason
293            .clone()
294            .unwrap_or(FinishReason::Completed)
295    }
296
297    /// Block for up to `timeout` waiting for the animation to finish.
298    /// Returns `None` if the timeout elapsed while the animation was
299    /// still running.
300    pub fn wait_timeout(&self, timeout: StdDuration) -> Option<FinishReason> {
301        let mut state = self.inner.lock_state();
302        let deadline = Instant::now() + timeout;
303        while state.running {
304            let now = Instant::now();
305            if now >= deadline {
306                return None;
307            }
308            let (new_state, result) = self
309                .inner
310                .finish_cv
311                .wait_timeout(state, deadline - now)
312                .unwrap_or_else(PoisonError::into_inner);
313            state = new_state;
314            if result.timed_out() && state.running {
315                return None;
316            }
317        }
318        Some(
319            state
320                .finish_reason
321                .clone()
322                .unwrap_or(FinishReason::Completed),
323        )
324    }
325
326    // -------- observers -------------------------------------------------
327
328    pub fn on_start<F>(&self, cb: F)
329    where
330        F: Fn(&StartContext) + Send + Sync + 'static,
331    {
332        self.inner.lock_observers().on_start.push(Arc::new(cb));
333    }
334
335    pub fn on_tick<F>(&self, cb: F)
336    where
337        F: Fn(&TickContext) + Send + Sync + 'static,
338    {
339        self.inner.lock_observers().on_tick.push(Arc::new(cb));
340    }
341
342    pub fn on_icon_complete<F>(&self, cb: F)
343    where
344        F: Fn(&IconId) + Send + Sync + 'static,
345    {
346        self.inner
347            .lock_observers()
348            .on_icon_complete
349            .push(Arc::new(cb));
350    }
351
352    pub fn on_finish<F>(&self, cb: F)
353    where
354        F: Fn(&FinishReason) + Send + Sync + 'static,
355    {
356        self.inner.lock_observers().on_finish.push(Arc::new(cb));
357    }
358}