git.lucas.co / cce-compositor
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 }