diff options
| author | srdusr <[email protected]> | 2026-04-20 00:48:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-04-20 00:48:00 +0200 |
| commit | 436d42da6ef61a5ea20d5102c4baed7bf0993606 (patch) | |
| tree | 5b013ae1877d81bfb67e422fdf56896d761076ed /crates/wayland/src/virtual_pointer.rs | |
| parent | 1f708f8aa09bf8bbce82314a76b7f34def90b798 (diff) | |
| download | srdwm-436d42da6ef61a5ea20d5102c4baed7bf0993606.tar.gz srdwm-436d42da6ef61a5ea20d5102c4baed7bf0993606.zip | |
Confirm Nemo's popup works; fix the two bugs that hid it, and shadow bleed across a monitor seam
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.
Two real bugs turned up in the way of testing it.
zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().
The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.
Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.
New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.
489 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland/src/virtual_pointer.rs')
| -rw-r--r-- | crates/wayland/src/virtual_pointer.rs | 42 |
1 files changed, 35 insertions, 7 deletions
diff --git a/crates/wayland/src/virtual_pointer.rs b/crates/wayland/src/virtual_pointer.rs index f30d7fb..4808ed5 100644 --- a/crates/wayland/src/virtual_pointer.rs +++ b/crates/wayland/src/virtual_pointer.rs @@ -225,8 +225,7 @@ impl Dispatch<ZwlrVirtualPointerV1, VirtualPointerData> for CompState { pinned_move_to(state, data, &surface, target, time); return; } - let Some(udev) = state.udev.as_ref() else { return }; - let (min_x, min_y, max_x, max_y) = udev.bounds(); + let (min_x, min_y, max_x, max_y) = pointer_bounds(state); let pos = last_pointer_pos(state); let target = Point::<f64, Logical>::from(( (pos.x + dx.to_f64()).clamp(min_x, (max_x - 1.0).max(min_x)), @@ -249,9 +248,6 @@ impl Dispatch<ZwlrVirtualPointerV1, VirtualPointerData> for CompState { pinned_move_to(state, data, &surface, target, time); return; } - if state.udev.is_none() { - return; - } let (nx, ny) = (x as f64 / x_extent as f64, y as f64 / y_extent as f64); // Mapped onto the requested output's own full geometry if // `create_virtual_pointer_with_output` named one, otherwise @@ -264,12 +260,12 @@ impl Dispatch<ZwlrVirtualPointerV1, VirtualPointerData> for CompState { match state.wm.borrow().monitors().iter().find(|m| m.name == name) { Some(m) => (m.full_geometry.x as f64, m.full_geometry.y as f64, m.full_geometry.width as f64, m.full_geometry.height as f64), None => { - let (min_x, min_y, max_x, max_y) = state.udev.as_ref().unwrap().bounds(); + let (min_x, min_y, max_x, max_y) = pointer_bounds(state); (min_x, min_y, max_x - min_x, max_y - min_y) } } } else { - let (min_x, min_y, max_x, max_y) = state.udev.as_ref().unwrap().bounds(); + let (min_x, min_y, max_x, max_y) = pointer_bounds(state); (min_x, min_y, max_x - min_x, max_y - min_y) }; let target = Point::<f64, Logical>::from(((min_x + nx * w).clamp(min_x, min_x + w - 1.0), (min_y + ny * h).clamp(min_y, min_y + h - 1.0))); @@ -384,6 +380,38 @@ impl CompState { } } +/// The whole addressable pointer span, in logical coordinates - the union +/// of every head on the DRM backend, and the union of every +/// `WindowManager` monitor otherwise. +/// +/// Every `Motion`/`MotionAbsolute` bounds lookup here used to read +/// `UdevState::bounds()` directly, behind an early `return` when +/// `state.udev` was `None`. That field is `Some` only for the DRM backend +/// (see `state/mod.rs`), so on the nested winit backend this protocol +/// advertised its global, accepted `create_virtual_pointer`, accepted +/// every request, and then silently discarded all motion: no error, no +/// log, nothing on screen. That is the exact backend a nested test +/// instance runs on, so the one safe way to drive synthetic input at a +/// throwaway compositor - a Wayland client of that compositor, which +/// cannot reach any other session by construction, unlike a uinput-level +/// tool such as `ydotool` - did not work at all. Found while trying to +/// verify Nemo's right-click popup without clicking blind at the user's +/// real desktop. +/// +/// `WindowManager::monitors()` is filled from `Platform::monitors()` at +/// startup and on every hotplug poll (`crates/srdwm/src/main.rs`), by both +/// backends, so it is the backend-agnostic source. The DRM branch stays +/// first and unchanged: `heads` is what that backend actually clamps its +/// own `pointer_pos` against, and the two lists can legitimately disagree +/// mid-hotplug. +fn pointer_bounds(state: &CompState) -> (f64, f64, f64, f64) { + if let Some(udev) = state.udev.as_ref() { + return udev.bounds(); + } + let wm = state.wm.borrow(); + crate::udev::bounds_of(wm.monitors().iter().map(|m| (m.full_geometry.x, m.full_geometry.y, m.full_geometry.width as i32, m.full_geometry.height as i32))) +} + /// This pinned stream's target window's own current content size, /// physical pixels - `core::Window::geometry` is already physical, the /// same convention `MotionEvent.location` and everything else in this |