Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: make the desktop background a drop target during a drag
Wayland delivers drops to surfaces, and the desktop background is not
one — the wallpaper client was retired and the compositor draws the
canvas itself — so every drag released over the desktop was cancelled.
The grid client stands in: it already covers the canvas and renders it,
so it is the thing that can say what "dropped at this spot" means. It
stays input-transparent for every other purpose; only a drag, and only
over the background, resolves onto it, so clicks, hover and the overview
background-exit are untouched. Scene::at_including_grid resolves the
point through wlroots rather than deriving surface-local coordinates
from box_geom, which would drift as soon as the camera zoomed.
Also: a drag now supersedes the implicit pointer grab. wlroots' drag
grab owns focus for its duration and the whole point is that focus
follows the pointer onto what it is dragged over — holding focus on the
source meant the target never changed and the drop went nowhere, which
is what the grid-focus path above ran into first.
Shadow-verified with a real Firefox drag: focus reaches the grid, the
drop is delivered, and dragging a window by its border still moves it
exactly as injected.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/server/cursor.rs | 42 +++++++++++++++++++++++++++++++++++++++++-
src/server/scene.rs | 16 +++++++++++++++-
src/server/seat.rs | 1 +
3 files changed, 57 insertions(+), 2 deletions(-)
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 5ee4bc1..b40ffe1 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -689,7 +689,12 @@ impl Cursor {
// outside the window. Focus is neither re-evaluated nor cleared until
// the last such button releases; wlroots nulls the focused surface if
// it is destroyed mid-grab, which falls through to normal dispatch.
- if !self.notified_pressed.is_empty() {
+ // A DRAG supersedes the implicit grab: wlroots' drag grab owns pointer
+ // focus for its duration, and the whole point is that focus follows
+ // the pointer onto whatever it is dragged over. Holding focus on the
+ // source here means the drag target never changes, so no drop is ever
+ // delivered anywhere — the source keeps receiving motion instead.
+ if !self.notified_pressed.is_empty() && (*self.seat).drag == crate::seat::DragState::None {
let focused =
ffi::river_wlr_seat_get_pointer_focused_surface((*self.seat).wlr_seat);
if !focused.is_null() {
@@ -771,6 +776,41 @@ impl Cursor {
}
}
+ // Nothing under the pointer: the desktop background. A DRAG in
+ // progress is the one case that still needs a surface here — Wayland
+ // delivers drops to surfaces, and the background is not one, so
+ // without this every drag onto the desktop is cancelled on release.
+ // The grid client stands in as the desktop's drop target: it already
+ // covers the canvas and renders it, so it is the thing that can say
+ // what "dropped at this spot" means. It stays input-transparent for
+ // every other purpose (see `Scene::at`) — only the drag resolves onto
+ // 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;
+ }
+ }
+ }
+ }
+ }
+
self.set_border_hover(std::ptr::null_mut(), None);
self.clear_focus();
}
diff --git a/src/server/scene.rs b/src/server/scene.rs
index cadfcd8..81ba271 100644
--- a/src/server/scene.rs
+++ b/src/server/scene.rs
@@ -126,6 +126,20 @@ 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)
+ }
+
+ unsafe fn at_impl(&self, lx: f64, ly: f64, include_grid: bool) -> Option<AtResult> {
let mut disabled_nodes = Vec::new();
let mut result = None;
@@ -149,7 +163,7 @@ impl Scene {
// 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.
- if (*window).is_grid() {
+ 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);
disabled_nodes.push(tree_node);
diff --git a/src/server/seat.rs b/src/server/seat.rs
index bccdc30..68d3c79 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1738,6 +1738,7 @@ unsafe extern "C" fn handle_start_drag(
assert!(seat.drag == DragState::None);
let grab_type = ffi::river_wlr_drag_get_grab_type(wlr_drag);
+ log::debug!("[drag] started (grab type {grab_type})");
match grab_type {
ffi::wlr_drag_grab_type_WLR_DRAG_GRAB_KEYBOARD_POINTER => {
seat.drag = DragState::Pointer;