Skip to main content

rdi_core/
fake.rs

1//! An in-memory [`DesktopBackend`] implementation
2//! for unit and integration tests.
3//!
4//! The backend is `Clone`-able (all internal state lives behind
5//! `Arc<Mutex<_>>`), so a test can hold one clone for observation while
6//! handing another clone to the [`DesktopController`](crate::DesktopController).
7//!
8//! ```
9//! use rdi_core::fake::FakeDesktop;
10//! use rdi_core::{IconId, IconSnapshot, Point};
11//!
12//! let desktop = FakeDesktop::new();
13//! desktop.add_icon(IconSnapshot::new(
14//!     IconId::from("a"), "A", None, false, Point::new(0, 0),
15//! ));
16//!
17//! // Handing a clone to the controller (M2 wiring covered elsewhere).
18//! let backend = desktop.clone();
19//! # let _ = backend;
20//!
21//! // The test can still inspect state:
22//! assert_eq!(desktop.icon_count(), 1);
23//! ```
24
25use std::collections::HashMap;
26use std::sync::{Arc, Mutex};
27
28use crate::backend::DesktopBackend;
29use crate::{
30    DesktopError, FinalCommitOutcome, IconId, IconRenderPlan, IconSnapshot, MonitorInfo,
31    OverlayRenderOptions, Point, Rect,
32};
33
34/// Observable, cloneable fake desktop backed by a `HashMap`.
35#[derive(Clone, Debug, Default)]
36pub struct FakeDesktop {
37    inner: Arc<Mutex<FakeDesktopInner>>,
38}
39
40#[derive(Debug)]
41pub struct FakeDesktopInner {
42    /// All icons currently on the fake desktop.
43    pub icons: HashMap<IconId, IconSnapshot>,
44    /// Current folder-view flags word.
45    pub flags: u32,
46    /// Log of every `set_positions` batch, in order. Each entry is the
47    /// list of `(id, new_pos)` pairs that were actually applied (missing
48    /// ids are excluded).
49    pub commit_log: Vec<Vec<(IconId, Point)>>,
50    /// Log of every `apply_flags(mask, values)` call.
51    pub flag_ops: Vec<(u32, u32)>,
52    /// If set, `list_icons` will fail with this error the next time it is
53    /// called. Cleared after the failing call.
54    pub next_list_error: Option<DesktopError>,
55    /// Simulated monitor layout. Defaults to a single 1920x1080 primary
56    /// monitor at the origin so tests that don't care about multi-
57    /// monitor behaviour keep working.
58    pub monitors: Vec<MonitorInfo>,
59    pub desktop_info: Option<crate::DesktopInfo>,
60
61    // --- overlay-session observability --------------------
62    /// If set, `begin_overlay_session` will fail with this error the
63    /// next time it is called. Cleared after the failing call.
64    pub next_overlay_error: Option<DesktopError>,
65    /// Whether an overlay session is currently open on this fake
66    /// backend.
67    pub overlay_active: bool,
68    pub real_icons_visible: bool,
69    /// The `IconRenderPlan` list passed to the most recent successful
70    /// `begin_overlay_session` call.
71    pub last_overlay_plans: Vec<IconRenderPlan>,
72    /// The [`OverlayRenderOptions`] passed to the most recent
73    /// successful `begin_overlay_session` call. Initialised to
74    /// [`OverlayRenderOptions::all_enabled`] until a session opens.
75    pub last_overlay_render_options: OverlayRenderOptions,
76    /// Log of every `commit_overlay_frame` call, in order.
77    pub overlay_frame_log: Vec<Vec<(IconId, Point)>>,
78    pub visual_frame_log: Vec<Vec<crate::IconFrame>>,
79    /// Log of every `finalize_overlay_session` call — the final
80    /// positions committed to the Shell.
81    pub overlay_finalize_log: Vec<Vec<(IconId, Point)>>,
82    /// Queue of pending errors to return from the next
83    /// `commit_overlay_frame` calls (one per call, in order).
84    /// Populated by tests via [`FakeDesktop::queue_commit_error`]
85    /// so the engine's cancellation / error branches can be
86    /// exercised deterministically.
87    pub next_commit_errors: std::collections::VecDeque<DesktopError>,
88}
89
90impl Default for FakeDesktopInner {
91    fn default() -> Self {
92        Self {
93            icons: HashMap::new(),
94            flags: 0,
95            commit_log: Vec::new(),
96            flag_ops: Vec::new(),
97            next_list_error: None,
98            monitors: vec![MonitorInfo {
99                id: "FAKE-PRIMARY".into(),
100                name: "FAKE-PRIMARY".into(),
101                bounds: Rect::from_origin_size(0, 0, 1920, 1080),
102                work_area: Rect::from_origin_size(0, 0, 1920, 1040),
103                is_primary: true,
104                scale_factor: 1.0,
105            }],
106            next_overlay_error: None,
107            desktop_info: None,
108            overlay_active: false,
109            real_icons_visible: true,
110            last_overlay_plans: Vec::new(),
111            last_overlay_render_options: OverlayRenderOptions::all_enabled(),
112            overlay_frame_log: Vec::new(),
113            visual_frame_log: Vec::new(),
114            overlay_finalize_log: Vec::new(),
115            next_commit_errors: std::collections::VecDeque::new(),
116        }
117    }
118}
119
120impl FakeDesktop {
121    pub fn real_icons_visible(&self) -> bool {
122        self.inner.lock().expect("FakeDesktop poisoned").real_icons_visible
123    }
124
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Add or replace an icon.
130    pub fn add_icon(&self, snap: IconSnapshot) {
131        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
132        guard.icons.insert(snap.id.clone(), snap);
133    }
134
135    /// Remove an icon (simulates the user deleting it while an animation
136    /// is running).
137    pub fn remove_icon(&self, id: &IconId) -> bool {
138        self.inner
139            .lock()
140            .expect("FakeDesktop poisoned")
141            .icons
142            .remove(id)
143            .is_some()
144    }
145
146    pub fn icon_count(&self) -> usize {
147        self.inner.lock().expect("FakeDesktop poisoned").icons.len()
148    }
149
150    /// Look up the current position of one icon.
151    pub fn position_of(&self, id: &IconId) -> Option<Point> {
152        self.inner
153            .lock()
154            .expect("FakeDesktop poisoned")
155            .icons
156            .get(id)
157            .map(|s| s.position)
158    }
159
160    /// Current folder-view flags.
161    pub fn flags(&self) -> u32 {
162        self.inner.lock().expect("FakeDesktop poisoned").flags
163    }
164
165    /// Take a copy of the commit log so far. Does not clear it.
166    pub fn commit_log(&self) -> Vec<Vec<(IconId, Point)>> {
167        self.inner
168            .lock()
169            .expect("FakeDesktop poisoned")
170            .commit_log
171            .clone()
172    }
173
174    /// Take a copy of the flag-op log so far.
175    pub fn flag_ops(&self) -> Vec<(u32, u32)> {
176        self.inner
177            .lock()
178            .expect("FakeDesktop poisoned")
179            .flag_ops
180            .clone()
181    }
182
183    /// Cause the next `list_icons` call to fail with this error.
184    pub fn set_next_list_error(&self, err: DesktopError) {
185        self.inner
186            .lock()
187            .expect("FakeDesktop poisoned")
188            .next_list_error = Some(err);
189    }
190
191    /// Replace the simulated monitor layout. Useful for testing
192    /// multi-monitor placement without needing a real second display.
193    pub fn set_monitors(&self, monitors: Vec<MonitorInfo>) {
194        self.inner
195            .lock()
196            .expect("FakeDesktop poisoned")
197            .monitors = monitors;
198    }
199
200    pub fn set_desktop_info(&self, desktop: crate::DesktopInfo) {
201        self.inner.lock().expect("FakeDesktop poisoned").desktop_info = Some(desktop);
202    }
203
204    /// Cause the next `begin_overlay_session` call to fail with this
205    /// error. Used by tests to exercise the engine's overlay-unavailable
206    /// fallback path.
207    pub fn set_next_overlay_error(&self, err: DesktopError) {
208        self.inner
209            .lock()
210            .expect("FakeDesktop poisoned")
211            .next_overlay_error = Some(err);
212    }
213
214    /// Whether an overlay session is currently open.
215    pub fn overlay_active(&self) -> bool {
216        self.inner
217            .lock()
218            .expect("FakeDesktop poisoned")
219            .overlay_active
220    }
221
222    /// Snapshot of the plans handed to the most recent successful
223    /// `begin_overlay_session` call.
224    pub fn last_overlay_plans(&self) -> Vec<IconRenderPlan> {
225        self.inner
226            .lock()
227            .expect("FakeDesktop poisoned")
228            .last_overlay_plans
229            .clone()
230    }
231
232    /// Snapshot of the render options handed to the most recent
233    /// successful `begin_overlay_session` call. Defaults to
234    /// `all_enabled` before any session has opened.
235    pub fn last_overlay_render_options(&self) -> OverlayRenderOptions {
236        self.inner
237            .lock()
238            .expect("FakeDesktop poisoned")
239            .last_overlay_render_options
240    }
241
242    /// Snapshot of every `commit_overlay_frame` call in order.
243    pub fn overlay_frame_log(&self) -> Vec<Vec<(IconId, Point)>> {
244        self.inner
245            .lock()
246            .expect("FakeDesktop poisoned")
247            .overlay_frame_log
248            .clone()
249    }
250
251    pub fn visual_frame_log(&self) -> Vec<Vec<crate::IconFrame>> {
252        self.inner.lock().expect("FakeDesktop poisoned").visual_frame_log.clone()
253    }
254
255    /// Snapshot of every `finalize_overlay_session` call in order.
256    pub fn overlay_finalize_log(&self) -> Vec<Vec<(IconId, Point)>> {
257        self.inner
258            .lock()
259            .expect("FakeDesktop poisoned")
260            .overlay_finalize_log
261            .clone()
262    }
263
264    /// Queue an error to be returned from the next
265    /// `commit_overlay_frame` call. Multiple queued errors are
266    /// consumed in FIFO order.
267    ///
268    /// Useful for exercising the engine's
269    /// [`DesktopError::OverlayCancelled`] path from tests.
270    pub fn queue_commit_error(&self, err: DesktopError) {
271        self.inner
272            .lock()
273            .expect("FakeDesktop poisoned")
274            .next_commit_errors
275            .push_back(err);
276    }
277}
278
279impl DesktopBackend for FakeDesktop {
280    fn list_icons(&mut self) -> Result<Vec<IconSnapshot>, DesktopError> {
281        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
282        if let Some(err) = guard.next_list_error.take() {
283            return Err(err);
284        }
285        Ok(guard.icons.values().cloned().collect())
286    }
287
288    fn get_flags(&mut self) -> Result<u32, DesktopError> {
289        Ok(self.inner.lock().expect("FakeDesktop poisoned").flags)
290    }
291
292    fn apply_flags(&mut self, mask: u32, values: u32) -> Result<(), DesktopError> {
293        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
294        guard.flag_ops.push((mask, values));
295        guard.flags = (guard.flags & !mask) | (values & mask);
296        Ok(())
297    }
298
299    fn set_positions(
300        &mut self,
301        moves: &[(IconId, Point)],
302    ) -> Result<Vec<IconId>, DesktopError> {
303        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
304        let mut applied = Vec::with_capacity(moves.len());
305        let mut missing = Vec::new();
306        for (id, pt) in moves {
307            if let Some(snap) = guard.icons.get_mut(id) {
308                snap.position = *pt;
309                applied.push((id.clone(), *pt));
310            } else {
311                missing.push(id.clone());
312            }
313        }
314        guard.commit_log.push(applied);
315        Ok(missing)
316    }
317
318    fn list_monitors(&mut self) -> Result<Vec<MonitorInfo>, DesktopError> {
319        Ok(self.inner.lock().expect("FakeDesktop poisoned").monitors.clone())
320    }
321
322    fn desktop_info(&mut self, _icons: &[IconSnapshot]) -> Result<crate::DesktopInfo, DesktopError> {
323        self.inner.lock().expect("FakeDesktop poisoned").desktop_info.clone()
324            .ok_or(DesktopError::UnsupportedPlatform)
325    }
326
327    fn begin_overlay_session(
328        &mut self,
329        plans: &[IconRenderPlan],
330        render_options: OverlayRenderOptions,
331    ) -> Result<(), DesktopError> {
332        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
333        if let Some(err) = guard.next_overlay_error.take() {
334            return Err(err);
335        }
336        guard.overlay_active = true;
337        guard.real_icons_visible = false;
338        guard.last_overlay_plans = plans.to_vec();
339        guard.last_overlay_render_options = render_options;
340        Ok(())
341    }
342
343    fn commit_overlay_frame(
344        &mut self,
345        positions: &[(IconId, Point)],
346    ) -> Result<(), DesktopError> {
347        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
348        if let Some(err) = guard.next_commit_errors.pop_front() {
349            return Err(err);
350        }
351        guard.overlay_frame_log.push(positions.to_vec());
352        Ok(())
353    }
354
355    fn discard_overlay_session(&mut self) {
356        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
357        guard.overlay_active = false;
358        guard.real_icons_visible = true;
359    }
360
361    fn commit_visual_frame(&mut self, frame: &[crate::IconFrame]) -> Result<(), DesktopError> {
362        self.commit_overlay_frame(&frame.iter().map(|entry| (entry.id.clone(), entry.position)).collect::<Vec<_>>())?;
363        self.inner.lock().expect("FakeDesktop poisoned").visual_frame_log.push(frame.to_vec());
364        Ok(())
365    }
366
367    fn poll_overlay_session(&mut self) -> Result<(), DesktopError> {
368        match self.inner.lock().expect("FakeDesktop poisoned").next_commit_errors.pop_front() {
369            Some(error) => Err(error),
370            None => Ok(()),
371        }
372    }
373
374    fn set_real_icons_visible(&mut self, visible: bool) -> Result<(), DesktopError> {
375        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
376        if !guard.overlay_active {
377            return Err(DesktopError::BackendUnavailable("no active overlay".into()));
378        }
379        guard.real_icons_visible = visible;
380        Ok(())
381    }
382
383    fn finalize_overlay_session(
384        &mut self,
385        final_positions: &[(IconId, Point)],
386    ) -> Result<FinalCommitOutcome, DesktopError> {
387        let mut guard = self.inner.lock().expect("FakeDesktop poisoned");
388        guard.overlay_finalize_log.push(final_positions.to_vec());
389        guard.overlay_active = false;
390        guard.real_icons_visible = true;
391
392        // Apply the moves to the fake icon storage — this is what a
393        // real backend achieves via `SelectAndPositionItems`. Track
394        // moved / missing separately for parity with the trait doc.
395        let mut moved = Vec::with_capacity(final_positions.len());
396        let mut missing = Vec::new();
397        for (id, pt) in final_positions {
398            match guard.icons.get_mut(id) {
399                Some(snap) => {
400                    snap.position = *pt;
401                    moved.push(id.clone());
402                }
403                None => missing.push(id.clone()),
404            }
405        }
406        Ok(FinalCommitOutcome {
407            moved_ids: moved,
408            missing_ids: missing,
409        })
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    fn snap(id: &str, x: i32, y: i32) -> IconSnapshot {
418        IconSnapshot::new(IconId::from(id), id, None, false, Point::new(x, y))
419    }
420
421    #[test]
422    fn apply_flags_mask_semantics_match_shell() {
423        let d = FakeDesktop::new();
424        let mut b = d.clone();
425        // Start from 0b0101.
426        b.apply_flags(0b1111, 0b0101).unwrap();
427        assert_eq!(d.flags(), 0b0101);
428
429        // OR-set (legacy set_desktop_flags): mask = f, values = 0xFFFF_FFFF.
430        b.apply_flags(0b0010, 0xFFFF_FFFF).unwrap();
431        assert_eq!(d.flags(), 0b0111);
432
433        // AND-clear (legacy unset_desktop_flags): mask = f, values = 0.
434        b.apply_flags(0b0100, 0).unwrap();
435        assert_eq!(d.flags(), 0b0011);
436    }
437
438    #[test]
439    fn set_positions_updates_and_reports_missing() {
440        let d = FakeDesktop::new();
441        d.add_icon(snap("a", 0, 0));
442        d.add_icon(snap("b", 10, 10));
443
444        let mut b = d.clone();
445        let missing = b
446            .set_positions(&[
447                (IconId::from("a"), Point::new(1, 2)),
448                (IconId::from("ghost"), Point::new(0, 0)),
449            ])
450            .unwrap();
451
452        assert_eq!(missing, vec![IconId::from("ghost")]);
453        assert_eq!(d.position_of(&IconId::from("a")), Some(Point::new(1, 2)));
454        assert_eq!(d.position_of(&IconId::from("b")), Some(Point::new(10, 10)));
455        let log = d.commit_log();
456        assert_eq!(log.len(), 1);
457        assert_eq!(log[0], vec![(IconId::from("a"), Point::new(1, 2))]);
458    }
459
460    #[test]
461    fn list_icons_returns_error_when_armed() {
462        let d = FakeDesktop::new();
463        d.set_next_list_error(DesktopError::BackendUnavailable("bang".into()));
464        let mut b = d.clone();
465        assert!(matches!(
466            b.list_icons(),
467            Err(DesktopError::BackendUnavailable(_))
468        ));
469        // Second call succeeds again (error consumed).
470        assert!(b.list_icons().is_ok());
471    }
472}