Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/selection.rs (24.1K)
1 //! Drag-selection of windows in overview.
2 //!
3 //! A left press on the bare desktop in overview, dragged, stretches a
4 //! rectangle from the press point to the pointer; every window the
5 //! rectangle touches is selected, live, while the button is held. It is the
6 //! compositor's version of cce-designer's network cursor region, with one
7 //! difference that follows from what is being selected: a node sits on one
8 //! lattice cell, so the designer asks whether that cell is inside the
9 //! region, while a window spans many cells and in overview the background
10 //! between two of them is a narrow strip — so here touching is enough.
11 //!
12 //! What the selection is for: pressing the body of a selected window moves
13 //! every selected window together (`Seat::group_move`, filled at the grab).
14 //! Like the designer, a new drag replaces the selection, a press on a window
15 //! outside it drops it, and there are no modifiers.
16 //!
17 //! The desktop IMAGES select the same way. They are `cce-grid`'s (its
18 //! desktop items, pinned to the virtual canvas), and the compositor never
19 //! sees them as anything but the grid surface's input region — so the grid
20 //! reports them over the control socket (`grid-items <id>:<x>:<y>:<w>:<h>
21 //! ...`, virtual units, on every change) and this module keeps the list
22 //! (`Selection::desktop_items`). The band picks them up by the same touch
23 //! rule, the highlight is drawn here beside the windows', and a group move
24 //! carries them: the compositor moves the rects it holds and pushes the new
25 //! positions over the status socket's `selection` topic (`move
26 //! <id>:<x>:<y> ...`, then `drop` at the release, on which the grid saves
27 //! its sidecar and reports the list afresh). A press on a selected image
28 //! starts the group move from the image's side (`PointerOpType::GroupMove`
29 //! — no grabbed window, the delta is the pointer's); a press on an
30 //! unselected one drops the selection and goes to the grid as before.
31 //!
32 //! The press that starts the drag used to exit overview on the spot. It
33 //! still does, on RELEASE, when the pointer never travelled: the press
34 //! cannot know which of the two it is.
35 //!
36 //! The rectangle is anchored in VIRTUAL (desk) coordinates, so it stays put
37 //! on the desk while the edge auto-pan scrolls the camera under a held drag.
38 //! Selection exists only in overview and is dropped when the mode is left
39 //! (`WindowManager::set_mode`).
40 //!
41 //! Drawing is a pool of (fill rect, glint bevel) pairs in one tree that
42 //! hangs off the scene ROOT, outside `interactive_tree`: `Scene::at` stops
43 //! at the first node it meets, and a node with no `SceneNodeData` reads as
44 //! "nothing here", so a highlight inside the interactive tree would turn a
45 //! press on a selected window into a press on the background.
46
47 use crate::ffi;
48 use crate::server::WlList;
49 use crate::window::Window;
50 use crate::window_manager::{WindowManager, WindowManagerMode};
51
52 /// Pointer travel, in layout px on either axis, past which a background
53 /// press is a drag and not a click. The same figure the overview tap on a
54 /// window uses (`handle_button`'s release path).
55 pub const DRAG_THRESHOLD: i32 = 5;
56
57 /// Fill opacity of the rubber band, and of the wash over a selected window.
58 const MARQUEE_FILL: f32 = 0.14;
59 const SELECTED_FILL: f32 = 0.12;
60 /// Corner radius of the rubber band, screen px.
61 const MARQUEE_RADIUS: f64 = 6.0;
62
63 /// One of the grid's desktop images, as it last reported it: a
64 /// per-process id the grid assigns, and its rect in virtual units.
65 #[derive(Clone, Copy, Debug, PartialEq)]
66 pub struct DesktopItem {
67 pub id: u64,
68 pub x: f64,
69 pub y: f64,
70 pub w: f64,
71 pub h: f64,
72 }
73
74 impl DesktopItem {
75 pub fn rect(&self) -> (f64, f64, f64, f64) {
76 (self.x, self.y, self.w, self.h)
77 }
78 }
79
80 /// Parse the grid's report: whitespace-separated `id:x:y:w:h` tokens, any
81 /// malformed one skipped. An empty report is an empty desk.
82 pub fn parse_desktop_items(tokens: &[&str]) -> Vec<DesktopItem> {
83 tokens
84 .iter()
85 .filter_map(|tok| {
86 let mut f = tok.split(':');
87 let id = f.next()?.parse::<u64>().ok()?;
88 let x = f.next()?.parse::<f64>().ok()?;
89 let y = f.next()?.parse::<f64>().ok()?;
90 let w = f.next()?.parse::<f64>().ok()?;
91 let h = f.next()?.parse::<f64>().ok()?;
92 Some(DesktopItem { id, x, y, w, h })
93 })
94 .collect()
95 }
96
97 /// The topmost item under a virtual point. The grid draws its list in
98 /// order, last on top, and reports it in that order.
99 pub fn item_at(items: &[DesktopItem], vx: f64, vy: f64) -> Option<u64> {
100 items
101 .iter()
102 .rev()
103 .find(|i| vx >= i.x && vx < i.x + i.w && vy >= i.y && vy < i.y + i.h)
104 .map(|i| i.id)
105 }
106
107 /// The rubber band: the press point and the pointer, in virtual coordinates.
108 #[derive(Clone, Copy, Debug, PartialEq)]
109 pub struct Marquee {
110 pub anchor: (f64, f64),
111 pub far: (f64, f64),
112 }
113
114 impl Marquee {
115 /// `(x, y, width, height)`, whichever way the drag went.
116 pub fn rect(&self) -> (f64, f64, f64, f64) {
117 let x = self.anchor.0.min(self.far.0);
118 let y = self.anchor.1.min(self.far.1);
119 (
120 x,
121 y,
122 (self.anchor.0 - self.far.0).abs(),
123 (self.anchor.1 - self.far.1).abs(),
124 )
125 }
126 }
127
128 /// Does `rect` touch `win`? Both `(x, y, width, height)`. Sharing only an
129 /// edge is not touching, and a window with no area is never touched. The
130 /// band itself may be a line: a drag straight down through a window has no
131 /// width and still crosses it.
132 pub fn touches(rect: (f64, f64, f64, f64), win: (f64, f64, f64, f64)) -> bool {
133 let (rx, ry, rw, rh) = rect;
134 let (wx, wy, ww, wh) = win;
135 ww > 0.0 && wh > 0.0 && rx < wx + ww && wx < rx + rw && ry < wy + wh && wy < ry + rh
136 }
137
138 pub struct Selection {
139 /// The selected windows, in `wm.windows` order. Entries are dropped in
140 /// `Window::destroy`; a window that is merely unmapped or minimized
141 /// stays listed and is skipped where it matters.
142 pub windows: Vec<*mut Window>,
143 /// The selected desktop images, by the grid's id, in report order.
144 pub items: Vec<u64>,
145 /// Every desktop image the grid has reported, in its draw order. Moved
146 /// here during a group move so the highlight follows; the grid's next
147 /// report replaces the lot.
148 pub desktop_items: Vec<DesktopItem>,
149 /// Where the armed background press landed (virtual). Set on press,
150 /// cleared on release.
151 pub anchor: Option<(f64, f64)>,
152 /// The rubber band, once the press has travelled past the threshold.
153 pub marquee: Option<Marquee>,
154 tree: *mut ffi::wlr_scene_tree,
155 boxes: Vec<(*mut ffi::wlr_scene_rect, *mut ffi::wlr_scene_bevel)>,
156 }
157
158 impl Default for Selection {
159 fn default() -> Self {
160 Self {
161 windows: Vec::new(),
162 items: Vec::new(),
163 desktop_items: Vec::new(),
164 anchor: None,
165 marquee: None,
166 tree: std::ptr::null_mut(),
167 boxes: Vec::new(),
168 }
169 }
170 }
171
172 impl WindowManager {
173 /// Layout box origin of the first enabled output, the one the camera is
174 /// measured against (`Window::virtual_to_screen` uses the same).
175 unsafe fn selection_output_origin(&self) -> (f64, f64) {
176 let outputs_list = &(*self.server).om.outputs as *const ffi::wl_list as *mut WlList;
177 let mut curr = (*outputs_list).next;
178 while curr != outputs_list {
179 let output = crate::container_of!(curr, crate::output::Output, link);
180 if (*output).sent.state == crate::output::OutputStateValue::Enabled {
181 let b = (*output).sent.box_layout();
182 return (b.x as f64, b.y as f64);
183 }
184 curr = (*curr).next;
185 }
186 (0.0, 0.0)
187 }
188
189 pub unsafe fn layout_to_virtual(&self, lx: f64, ly: f64) -> (f64, f64) {
190 let (cam, _, _) = self.layout_camera();
191 let zoom = cam.zoom.max(0.01);
192 let (ox, oy) = self.selection_output_origin();
193 (cam.pan_x + (lx - ox) / zoom, cam.pan_y + (ly - oy) / zoom)
194 }
195
196 pub unsafe fn virtual_to_layout(&self, vx: f64, vy: f64) -> (f64, f64) {
197 let (cam, _, _) = self.layout_camera();
198 let (ox, oy) = self.selection_output_origin();
199 (ox + (vx - cam.pan_x) * cam.zoom, oy + (vy - cam.pan_y) * cam.zoom)
200 }
201
202 pub fn is_selected(&self, window: *mut Window) -> bool {
203 self.selection.windows.iter().any(|&w| w == window)
204 }
205
206 pub fn is_item_selected(&self, id: u64) -> bool {
207 self.selection.items.iter().any(|&i| i == id)
208 }
209
210 /// Anything selected at all — what a background click drops, and what
211 /// a press on a selected image carries.
212 pub fn has_selection(&self) -> bool {
213 !self.selection.windows.is_empty() || !self.selection.items.is_empty()
214 }
215
216 /// The selected desktop image under a virtual point, if the topmost
217 /// one there is selected.
218 pub fn selected_item_at(&self, vx: f64, vy: f64) -> Option<u64> {
219 item_at(&self.selection.desktop_items, vx, vy).filter(|&id| self.is_item_selected(id))
220 }
221
222 pub fn desktop_item(&self, id: u64) -> Option<&DesktopItem> {
223 self.selection.desktop_items.iter().find(|i| i.id == id)
224 }
225
226 pub fn desktop_item_mut(&mut self, id: u64) -> Option<&mut DesktopItem> {
227 self.selection.desktop_items.iter_mut().find(|i| i.id == id)
228 }
229
230 /// The grid reported its images (`grid-items`). The list is replaced
231 /// wholesale; a selected id that is no longer in it was removed or
232 /// belongs to a restarted grid, and is dropped.
233 pub unsafe fn set_desktop_items(&mut self, items: Vec<DesktopItem>) {
234 self.selection.items.retain(|id| items.iter().any(|i| i.id == *id));
235 self.selection.desktop_items = items;
236 self.schedule_frame_all_outputs();
237 }
238
239 /// Arm a drag-selection at a background press. Nothing is selected or
240 /// drawn until the pointer travels (`selection_motion`).
241 pub unsafe fn selection_press(&mut self, lx: f64, ly: f64) {
242 self.selection.anchor = Some(self.layout_to_virtual(lx, ly));
243 self.selection.marquee = None;
244 }
245
246 /// The pointer moved with the press held. `travelled` is whether it has
247 /// left the click threshold; once it has, the band is up for the rest of
248 /// the press even if the pointer comes back to where it started.
249 pub unsafe fn selection_motion(&mut self, lx: f64, ly: f64, travelled: bool) {
250 let Some(anchor) = self.selection.anchor else { return };
251 if self.selection.marquee.is_none() && !travelled {
252 return;
253 }
254 let marquee = Marquee { anchor, far: self.layout_to_virtual(lx, ly) };
255 self.selection.marquee = Some(marquee);
256 let rect = marquee.rect();
257 let mut picked: Vec<*mut Window> = Vec::new();
258 for &w in self.windows.iter() {
259 if !self.selectable(w) {
260 continue;
261 }
262 let win = (
263 (*w).virtual_x,
264 (*w).virtual_y,
265 (*w).box_geom.width as f64,
266 (*w).box_geom.height as f64,
267 );
268 if touches(rect, win) {
269 picked.push(w);
270 }
271 }
272 if picked != self.selection.windows {
273 log::debug!("selection: {} window(s)", picked.len());
274 self.selection.windows = picked;
275 }
276 let picked_items: Vec<u64> = self
277 .selection
278 .desktop_items
279 .iter()
280 .filter(|i| touches(rect, i.rect()))
281 .map(|i| i.id)
282 .collect();
283 if picked_items != self.selection.items {
284 log::debug!("selection: {} image(s)", picked_items.len());
285 self.selection.items = picked_items;
286 }
287 self.schedule_frame_all_outputs();
288 }
289
290 /// The press ended. Returns whether it was a drag: a press that never
291 /// travelled is a click on the background, which the caller answers by
292 /// leaving overview.
293 pub unsafe fn selection_release(&mut self) -> bool {
294 let dragged = self.selection.marquee.is_some();
295 self.selection.anchor = None;
296 self.selection.marquee = None;
297 self.schedule_frame_all_outputs();
298 dragged
299 }
300
301 pub unsafe fn selection_clear(&mut self) {
302 if !self.has_selection() && self.selection.marquee.is_none() {
303 return;
304 }
305 self.selection.windows.clear();
306 self.selection.items.clear();
307 self.selection.marquee = None;
308 self.schedule_frame_all_outputs();
309 }
310
311 /// `window` is being destroyed: it must not stay listed for a window
312 /// allocated at the same address to inherit. The grid going away takes
313 /// its images with it.
314 pub unsafe fn selection_forget(&mut self, window: *mut Window) {
315 let before = self.selection.windows.len();
316 self.selection.windows.retain(|&w| w != window);
317 let mut changed = self.selection.windows.len() != before;
318 if (*window).is_grid() && !self.selection.desktop_items.is_empty() {
319 self.selection.desktop_items.clear();
320 self.selection.items.clear();
321 changed = true;
322 }
323 if changed {
324 self.schedule_frame_all_outputs();
325 }
326 }
327
328 /// Windows the band can pick up, and a group move can carry: the ones
329 /// with a place on the desk. The same set the overview displacement
330 /// considers (`seat::displace_covered`).
331 pub unsafe fn selectable(&self, w: *mut Window) -> bool {
332 if w.is_null() || (*w).closed || (*w).minimized {
333 return false;
334 }
335 if !matches!((*w).state, crate::window::WindowState::Mapped) {
336 return false;
337 }
338 if (*w).is_status_bar() || (*w).is_wallpaper() || (*w).is_grid() {
339 return false;
340 }
341 matches!(
342 self.get_mode_for_window(w),
343 crate::tiling::TilingMode::Floating | crate::tiling::TilingMode::Tiled
344 )
345 }
346
347 /// `selectable`, for a window a group move is already carrying: the
348 /// drag floats a Tiled one, which `selectable` would still accept, but
349 /// the pointer may dangle if the window closed mid-drag — so this one
350 /// checks it is still a window before looking at it.
351 pub unsafe fn selectable_in_drag(&self, w: *mut Window) -> bool {
352 self.windows.iter().any(|&p| p == w) && self.selectable(w)
353 }
354
355 /// Place the band and the selected windows' highlights for the frame
356 /// about to render. Called from `Output::render_and_commit` beside the
357 /// grid, so the highlights follow a window through a drag and the camera
358 /// through a pan; with nothing selected it is one disabled tree.
359 pub unsafe fn draw_selection(&mut self) {
360 let showing = self.mode == WindowManagerMode::Overview
361 && (*self.server).lock_manager.state == crate::lock_manager::LockState::Unlocked
362 && (self.selection.marquee.is_some() || self.has_selection());
363 if !showing {
364 if !self.selection.tree.is_null() {
365 ffi::wlr_scene_node_set_enabled(
366 self.selection.tree as *mut ffi::wlr_scene_node,
367 false,
368 );
369 }
370 return;
371 }
372
373 // (x, y, width, height, corner radius, fill opacity), layout px.
374 let mut wanted: Vec<(i32, i32, i32, i32, i32, f32)> = Vec::new();
375 for &w in self.selection.windows.iter() {
376 if !self.selectable(w) || (*w).rendering_requested.hidden {
377 continue;
378 }
379 let sc = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
380 let g = (*w).box_geom;
381 let radius = crate::window::widen_corner_radius(
382 (*w).root_plate_radius_base(),
383 g.width,
384 g.height,
385 );
386 wanted.push((
387 g.x,
388 g.y,
389 (g.width as f64 * sc) as i32,
390 (g.height as f64 * sc) as i32,
391 (radius as f64 * sc) as i32,
392 SELECTED_FILL,
393 ));
394 }
395 // The images are square-cornered quads on the grid surface, so
396 // their wash is too.
397 for &id in self.selection.items.iter() {
398 let Some(item) = self.desktop_item(id) else { continue };
399 let (x0, y0) = self.virtual_to_layout(item.x, item.y);
400 let (x1, y1) = self.virtual_to_layout(item.x + item.w, item.y + item.h);
401 wanted.push((
402 x0.round() as i32,
403 y0.round() as i32,
404 (x1 - x0).round() as i32,
405 (y1 - y0).round() as i32,
406 0,
407 SELECTED_FILL,
408 ));
409 }
410 if let Some(marquee) = self.selection.marquee {
411 let (vx, vy, vw, vh) = marquee.rect();
412 let (x0, y0) = self.virtual_to_layout(vx, vy);
413 let (x1, y1) = self.virtual_to_layout(vx + vw, vy + vh);
414 let (w, h) = ((x1 - x0).round() as i32, (y1 - y0).round() as i32);
415 let radius = MARQUEE_RADIUS.min(w.min(h) as f64 / 2.0) as i32;
416 wanted.push((x0.round() as i32, y0.round() as i32, w, h, radius, MARQUEE_FILL));
417 }
418
419 if self.selection.tree.is_null() {
420 let scene = &(*self.server).scene;
421 let tree = ffi::wlr_scene_tree_create(&mut (*scene.wlr_scene).tree);
422 if tree.is_null() {
423 return;
424 }
425 // Over everything interactive, under a drag icon.
426 ffi::wlr_scene_node_place_above(
427 tree as *mut ffi::wlr_scene_node,
428 scene.interactive_tree as *mut ffi::wlr_scene_node,
429 );
430 // Desk content, like the windows it marks: rendered with the
431 // camera's sub-pixel offset.
432 ffi::river_scene_tree_set_desk_offset(tree, true);
433 self.selection.tree = tree;
434 }
435 let tree = self.selection.tree;
436 ffi::wlr_scene_node_set_enabled(tree as *mut ffi::wlr_scene_node, true);
437
438 let layout = &self.layout;
439 let accent = layout.bevel_focus_color;
440 let (light_x, light_y) = {
441 let (lx, ly) = (layout.bevel_light_x, layout.bevel_light_y);
442 let len = (lx * lx + ly * ly).sqrt();
443 if len > 1e-6 { (lx / len, ly / len) } else { (-0.7071, -0.7071) }
444 };
445 // The glint has to read whatever the window bevels are set to,
446 // including off: it is the selection's outline, not a bevel.
447 let thickness = layout.bevel_thickness.max(2.0);
448 let light = layout.bevel_light_intensity.max(0.6);
449
450 let mut used = 0;
451 for &(x, y, w, h, radius, fill) in wanted.iter() {
452 if w < 1 || h < 1 {
453 continue;
454 }
455 // wlr_scene_rect colours are premultiplied.
456 let color = [accent[0] * fill, accent[1] * fill, accent[2] * fill, fill];
457 if used == self.selection.boxes.len() {
458 let rect = ffi::wlr_scene_rect_create(tree, w, h, color.as_ptr());
459 let bevel = ffi::wlr_scene_bevel_create(
460 tree,
461 w,
462 h,
463 radius,
464 thickness,
465 layout.bevel_color.as_ptr(),
466 );
467 if rect.is_null() || bevel.is_null() {
468 if !rect.is_null() {
469 ffi::wlr_scene_node_destroy(rect as *mut ffi::wlr_scene_node);
470 }
471 if !bevel.is_null() {
472 ffi::wlr_scene_node_destroy(&mut (*bevel).node as *mut ffi::wlr_scene_node);
473 }
474 break;
475 }
476 self.selection.boxes.push((rect, bevel));
477 }
478 let (rect, bevel) = self.selection.boxes[used];
479 used += 1;
480
481 let rect_node = rect as *mut ffi::wlr_scene_node;
482 ffi::wlr_scene_node_set_enabled(rect_node, true);
483 ffi::river_scene_node_set_position_if_changed(rect_node, x, y);
484 ffi::river_scene_rect_set_size_if_changed(rect, w, h);
485 ffi::river_scene_rect_set_corner_radius(rect, radius);
486 ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
487
488 let bevel_node = &mut (*bevel).node as *mut ffi::wlr_scene_node;
489 ffi::wlr_scene_node_set_enabled(bevel_node, true);
490 ffi::river_scene_node_set_position_if_changed(bevel_node, x, y);
491 ffi::wlr_scene_bevel_set_size(bevel, w, h);
492 ffi::wlr_scene_bevel_set_corner_radius(bevel, radius);
493 ffi::wlr_scene_bevel_set_thickness(bevel, thickness);
494 ffi::wlr_scene_bevel_set_light(
495 bevel,
496 light_x,
497 light_y,
498 light,
499 layout.bevel_shade_intensity,
500 );
501 ffi::wlr_scene_bevel_set_shoulder(bevel, layout.bevel_shoulder);
502 ffi::wlr_scene_bevel_set_color(bevel, layout.bevel_color.as_ptr());
503 // Focus 1 is the shader's glint-only branch: the accent on the
504 // rim and nothing else, the tint the designer marks a region
505 // and its nodes with.
506 ffi::wlr_scene_bevel_set_focus(
507 bevel,
508 1.0,
509 layout.bevel_focus_sharpness,
510 accent.as_ptr(),
511 );
512 }
513 for &(rect, bevel) in self.selection.boxes.iter().skip(used) {
514 ffi::wlr_scene_node_set_enabled(rect as *mut ffi::wlr_scene_node, false);
515 ffi::wlr_scene_node_set_enabled(&mut (*bevel).node as *mut ffi::wlr_scene_node, false);
516 }
517 }
518 }
519
520 #[cfg(test)]
521 mod tests {
522 use super::*;
523
524 #[test]
525 fn rect_is_the_same_whichever_way_the_drag_went() {
526 let down_right = Marquee { anchor: (10.0, 20.0), far: (110.0, 70.0) };
527 let up_left = Marquee { anchor: (110.0, 70.0), far: (10.0, 20.0) };
528 let mixed = Marquee { anchor: (110.0, 20.0), far: (10.0, 70.0) };
529 let want = (10.0, 20.0, 100.0, 50.0);
530 assert_eq!(down_right.rect(), want);
531 assert_eq!(up_left.rect(), want);
532 assert_eq!(mixed.rect(), want);
533 }
534
535 #[test]
536 fn a_band_that_clips_a_corner_selects_the_window() {
537 let win = (100.0, 100.0, 400.0, 300.0);
538 assert!(touches((50.0, 50.0, 60.0, 60.0), win));
539 // Entirely inside the window's bounds is unreachable from a
540 // background press, but it is still a touch.
541 assert!(touches((200.0, 200.0, 10.0, 10.0), win));
542 // And the window entirely inside the band.
543 assert!(touches((0.0, 0.0, 1000.0, 1000.0), win));
544 }
545
546 #[test]
547 fn a_band_in_the_gap_beside_a_window_selects_nothing() {
548 let win = (100.0, 100.0, 400.0, 300.0);
549 assert!(!touches((0.0, 100.0, 90.0, 300.0), win));
550 // Sharing an edge is not touching.
551 assert!(!touches((0.0, 100.0, 100.0, 300.0), win));
552 assert!(!touches((100.0, 400.0, 400.0, 50.0), win));
553 }
554
555 #[test]
556 fn a_straight_drag_through_a_window_selects_it() {
557 let win = (100.0, 100.0, 400.0, 300.0);
558 // Pressed above the window, dragged straight down through it.
559 assert!(touches((200.0, 50.0, 0.0, 200.0), win));
560 // The same line beside the window.
561 assert!(!touches((50.0, 50.0, 0.0, 200.0), win));
562 }
563
564 #[test]
565 fn a_window_with_no_area_is_never_touched() {
566 assert!(!touches((0.0, 0.0, 1000.0, 1000.0), (100.0, 100.0, 0.0, 300.0)));
567 }
568
569 #[test]
570 fn the_grid_report_parses_and_skips_a_bad_token() {
571 let items = parse_desktop_items(&["3:10.5:20:300:200", "junk", "4:0:0:1:1:extra", "x:1:2:3:4"]);
572 assert_eq!(
573 items,
574 vec![
575 DesktopItem { id: 3, x: 10.5, y: 20.0, w: 300.0, h: 200.0 },
576 DesktopItem { id: 4, x: 0.0, y: 0.0, w: 1.0, h: 1.0 },
577 ]
578 );
579 assert!(parse_desktop_items(&[]).is_empty());
580 }
581
582 #[test]
583 fn the_last_reported_image_is_on_top() {
584 let items = vec![
585 DesktopItem { id: 1, x: 0.0, y: 0.0, w: 100.0, h: 100.0 },
586 DesktopItem { id: 2, x: 50.0, y: 50.0, w: 100.0, h: 100.0 },
587 ];
588 assert_eq!(item_at(&items, 75.0, 75.0), Some(2));
589 assert_eq!(item_at(&items, 10.0, 10.0), Some(1));
590 assert_eq!(item_at(&items, 200.0, 200.0), None);
591 // Half-open: the far edge belongs to nothing.
592 assert_eq!(item_at(&items, 150.0, 150.0), None);
593 }
594 }