Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: let the grid layer be hit-tested through its input region
The grid layer was forced input-transparent in Scene::at so every click
reached the desktop underneath. That is all-or-nothing, and it is why
nothing on the canvas could ever be clicked — including the images
pinned to it.
The grid now advertises an input region covering exactly its items, so
wlr_scene_node_at misses it over bare canvas by itself: background
clicks, hover, panning and the overview background-exit behave as
before, while a press ON an item reaches the client that owns it. An
empty region — a desktop with no items — is transparent exactly as the
old skip was.
Two consequences handled here:
- The drop target can no longer be found by hit-testing, since drops
mostly land on bare canvas which the region excludes. grid_surface_at
resolves the surface directly and maps the point through the surface
node's own layout origin and the window's display scale. A drop target
has to be named, not hit-tested.
- The grid must never be a border/move/resize target, nor an overview
click target: it is the canvas, not a window. Without this a press on
an item grabs the whole canvas as if dragging a window.
Shadow-verified after the change: dragging an image moves it, the
bare-canvas right-click still spawns the desktop menu, a background
click still exits overview, and a Firefox drop still lands.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/server/cursor.rs | 77 ++++++++++++++++++++++++++++++++++++----------------
src/server/scene.rs | 21 ++++++--------
2 files changed, 62 insertions(+), 36 deletions(-)
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index b40ffe1..f7f5e92 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -787,27 +787,11 @@ impl Cursor {
// it, and only while the pointer is over the background, so clicks,
// hover and the overview background-exit are untouched.
if (*self.seat).drag != crate::seat::DragState::None {
- if let Some(result) = (*server).scene.at_including_grid(lx, ly) {
- if !result.surface.is_null() {
- if let SceneNodeDataVal::Window(window) = result.data {
- if (*window).is_grid() {
- log::debug!("[drag] focus -> grid at ({lx:.0}, {ly:.0})");
- ffi::wlr_seat_pointer_notify_enter(
- (*self.seat).wlr_seat,
- result.surface,
- result.sx,
- result.sy,
- );
- ffi::wlr_seat_pointer_notify_motion(
- (*self.seat).wlr_seat,
- time_msec,
- result.sx,
- result.sy,
- );
- return;
- }
- }
- }
+ if let Some((surface, sx, sy)) = grid_surface_at(server, lx, ly) {
+ log::debug!("[drag] focus -> grid at ({lx:.0}, {ly:.0})");
+ ffi::wlr_seat_pointer_notify_enter((*self.seat).wlr_seat, surface, sx, sy);
+ ffi::wlr_seat_pointer_notify_motion((*self.seat).wlr_seat, time_msec, sx, sy);
+ return;
}
}
@@ -1184,9 +1168,13 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
(*clicked_win).tiling_mode,
crate::tiling::TilingMode::Overlay | crate::tiling::TilingMode::Popup
));
+ // The grid counts as background in overview: a press on it must
+ // exit overview like any desktop press, never grab the canvas
+ // itself as if it were a window.
let overview_win_valid = !clicked_win.is_null()
&& !(*clicked_win).is_status_bar()
- && !(*clicked_win).is_wallpaper();
+ && !(*clicked_win).is_wallpaper()
+ && !(*clicked_win).is_grid();
let overview_border_zone = if overview_win_valid {
get_border_zone(clicked_win, lx, ly)
} else {
@@ -1395,7 +1383,7 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
}
}
- if !border_target_win.is_null() && !(*border_target_win).is_status_bar() && !(*border_target_win).is_wallpaper() && (
+ if !border_target_win.is_null() && !(*border_target_win).is_status_bar() && !(*border_target_win).is_wallpaper() && !(*border_target_win).is_grid() && (
(*border_target_win).tiling_mode != crate::tiling::TilingMode::Popup
&& (*border_target_win).tiling_mode != crate::tiling::TilingMode::Fullscreen
) {
@@ -2749,6 +2737,49 @@ pub enum BorderZone {
pub use crate::window::HOVER_BAND_MIN;
+/// The grid client's surface and the surface-local coordinates of a layout
+/// point, ignoring the input region.
+///
+/// Hit-testing cannot be used for this: the grid's input region covers only
+/// its desktop items, so a point over bare canvas — where drops mostly land —
+/// misses it by design. A drop target does not need to be hit-testable, only
+/// named, so the surface is resolved directly and the point is mapped through
+/// the surface node's own layout origin and the window's display scale, which
+/// is the same pair the renderer draws with.
+pub unsafe fn grid_surface_at(
+ server: *mut crate::server::Server,
+ lx: f64,
+ ly: f64,
+) -> Option<(*mut ffi::wlr_surface, f64, f64)> {
+ for &w in (*server).wm.windows.iter() {
+ if w.is_null() || (*w).closed || !(*w).is_grid() {
+ continue;
+ }
+ if !matches!((*w).state, crate::window::WindowState::Mapped) {
+ continue;
+ }
+ let surface = (*w).root_surface();
+ if surface.is_null() {
+ continue;
+ }
+ let node = (*w).surfaces.tree as *mut ffi::wlr_scene_node;
+ let (mut nx, mut ny) = (0, 0);
+ if !ffi::wlr_scene_node_coords(node, &mut nx, &mut ny) {
+ continue;
+ }
+ let scale = if (*w).scale > 0.0 { (*w).scale } else { 1.0 };
+ let (sx, sy) = ((lx - nx as f64) / scale, (ly - ny as f64) / scale);
+ // Only claim points that actually fall on the grid's patch. box_geom
+ // is the surface's own logical size, which is the space sx/sy are in.
+ let (bw, bh) = ((*w).box_geom.width as f64, (*w).box_geom.height as f64);
+ if sx < 0.0 || sy < 0.0 || (bw > 0.0 && sx >= bw) || (bh > 0.0 && sy >= bh) {
+ continue;
+ }
+ return Some((surface, sx, sy));
+ }
+ None
+}
+
pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f64) -> BorderZone {
if (*window).tiling_mode == crate::tiling::TilingMode::Popup
|| (*window).tiling_mode == crate::tiling::TilingMode::Fullscreen
diff --git a/src/server/scene.rs b/src/server/scene.rs
index 81ba271..e720a2d 100644
--- a/src/server/scene.rs
+++ b/src/server/scene.rs
@@ -126,16 +126,6 @@ impl Scene {
pub unsafe fn deinit(&mut self) {}
pub unsafe fn at(&self, lx: f64, ly: f64) -> Option<AtResult> {
- self.at_impl(lx, ly, false)
- }
-
- /// `at`, but the grid layer participates like any other surface. The only
- /// caller is the drag path: the grid client is the desktop's drop target
- /// (it draws the canvas, so it owns what "dropped here" means), and this
- /// resolves the drop point through wlroots so the surface-local
- /// coordinates account for the grid's buffer scale — hand-deriving them
- /// from box_geom would drift the moment the camera zoomed.
- pub unsafe fn at_including_grid(&self, lx: f64, ly: f64) -> Option<AtResult> {
self.at_impl(lx, ly, true)
}
@@ -160,9 +150,14 @@ impl Scene {
if let Some(scene_node_data) = SceneNodeData::from_node(node) {
if let SceneNodeDataVal::Window(window) = scene_node_data.data {
- // The grid layer is input-transparent: every input path
- // (clicks, hover, overview background-exit) sees what is
- // underneath it, exactly as if it were the backdrop.
+ // The grid layer used to be skipped here to keep it
+ // input-transparent. It now advertises an input region
+ // covering exactly its desktop items, so wlr_scene_node_at
+ // already misses it over bare canvas and every other input
+ // path (clicks, hover, overview background-exit) still sees
+ // through it — while a click ON an item reaches the client
+ // that owns it. The parameter stays for the callers that
+ // must never see the grid at all.
if !include_grid && (*window).is_grid() {
let tree_node = (*window).tree as *mut ffi::wlr_scene_node;
ffi::wlr_scene_node_set_enabled(tree_node, false);