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}