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/decoration/shadow.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/decoration/shadow.rs')
| -rw-r--r-- | crates/wayland/src/decoration/shadow.rs | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/crates/wayland/src/decoration/shadow.rs b/crates/wayland/src/decoration/shadow.rs index df0a0dd..7c89ae0 100644 --- a/crates/wayland/src/decoration/shadow.rs +++ b/crates/wayland/src/decoration/shadow.rs @@ -30,6 +30,61 @@ pub fn shadow_rect(geometry: srdwm_core::Rect) -> srdwm_core::Rect { srdwm_core::Rect::new(geometry.x - s, geometry.y - s, geometry.width + SHADOW_SIZE * 2, geometry.height + SHADOW_SIZE * 2) } +/// [`shadow_rect`], clipped so a shadow can never land on a monitor the +/// window itself does not occupy. +/// +/// Reported live as "windows show a bit in the other monitor" with a real +/// second monitor connected: `srd clients` showed several windows sitting +/// at exactly `x: 1920`, the seam between two 1920-wide outputs, and a +/// window flush against that seam has nowhere to put its 24px shadow strip +/// except the neighbouring screen. The earlier shadow work only ever +/// considered a window's neighbouring *tile*; [`shadow_rect`] expands by +/// [`SHADOW_SIZE`] on every side with no monitor-boundary awareness at +/// all, so this survived it. +/// +/// `bounds` is every monitor's `full_geometry`. The clip box is the +/// bounding box of the monitors the window's own geometry actually +/// touches, not just of the one it is assigned to: a window straddling a +/// seam genuinely occupies both screens, and clipping such a window's +/// shadow at the seam would cut it off in the middle of its own visible +/// body. A window touching no monitor at all (off-screen, or no monitors +/// yet) is returned unclipped - there is nothing to clip against, and +/// silently collapsing it to an empty rect would drop the shadow instead. +pub fn shadow_rect_clipped(geometry: srdwm_core::Rect, bounds: &[srdwm_core::Rect]) -> srdwm_core::Rect { + let rect = shadow_rect(geometry); + let mut clip: Option<srdwm_core::Rect> = None; + for m in bounds.iter().filter(|m| overlaps(**m, geometry)) { + clip = Some(match clip { + None => *m, + Some(c) => union(c, *m), + }); + } + match clip { + Some(c) => intersect(rect, c), + None => rect, + } +} + +fn overlaps(a: srdwm_core::Rect, b: srdwm_core::Rect) -> bool { + a.x < b.x + b.width as i32 && b.x < a.x + a.width as i32 && a.y < b.y + b.height as i32 && b.y < a.y + a.height as i32 +} + +fn union(a: srdwm_core::Rect, b: srdwm_core::Rect) -> srdwm_core::Rect { + let x = a.x.min(b.x); + let y = a.y.min(b.y); + let right = (a.x + a.width as i32).max(b.x + b.width as i32); + let bottom = (a.y + a.height as i32).max(b.y + b.height as i32); + srdwm_core::Rect::new(x, y, (right - x).max(0) as u32, (bottom - y).max(0) as u32) +} + +fn intersect(a: srdwm_core::Rect, b: srdwm_core::Rect) -> srdwm_core::Rect { + let x = a.x.max(b.x); + let y = a.y.max(b.y); + let right = (a.x + a.width as i32).min(b.x + b.width as i32); + let bottom = (a.y + a.height as i32).min(b.y + b.height as i32); + srdwm_core::Rect::new(x, y, (right - x).max(0) as u32, (bottom - y).max(0) as u32) +} + /// Renders a window's drop shadow as a BGRA8 bitmap: black at an alpha that /// falls off linearly from [`SHADOW_MAX_ALPHA`] right at the window's own /// edge to fully transparent [`SHADOW_SIZE`] pixels out. `win_width`/ @@ -184,3 +239,62 @@ fn edge_distance(pos: u32, margin: u32, extent: u32) -> u32 { 0 } } + +#[cfg(test)] +mod clip_tests { + use super::shadow_rect_clipped; + use srdwm_core::Rect; + + /// Two 1920x1080 outputs side by side, the exact arrangement the + /// "windows show a bit in the other monitor" report was taken on. + fn two_monitors() -> Vec<Rect> { + vec![Rect::new(0, 0, 1920, 1080), Rect::new(1920, 0, 1920, 1080)] + } + + #[test] + fn a_window_flush_against_the_seam_does_not_shadow_the_next_monitor() { + // Right edge exactly on the seam at x=1920. + let w = Rect::new(1120, 100, 800, 600); + let r = shadow_rect_clipped(w, &two_monitors()); + assert_eq!(r.x + r.width as i32, 1920, "shadow crossed the seam"); + assert_eq!(r.x, 1120 - 24, "the left side should still get its full shadow"); + } + + #[test] + fn a_window_at_the_left_edge_of_the_second_monitor_does_not_shadow_the_first() { + let w = Rect::new(1920, 100, 800, 600); + let r = shadow_rect_clipped(w, &two_monitors()); + assert_eq!(r.x, 1920, "shadow crossed the seam"); + } + + #[test] + fn a_window_in_the_middle_of_a_monitor_is_unclipped() { + let w = Rect::new(500, 300, 400, 300); + let r = shadow_rect_clipped(w, &two_monitors()); + assert_eq!((r.x, r.y, r.width, r.height), (500 - 24, 300 - 24, 400 + 48, 300 + 48)); + } + + #[test] + fn a_window_straddling_the_seam_keeps_its_shadow_on_both_monitors() { + let w = Rect::new(1720, 100, 400, 600); + let r = shadow_rect_clipped(w, &two_monitors()); + assert_eq!(r.x, 1720 - 24); + assert_eq!(r.x + r.width as i32, 2120 + 24); + } + + #[test] + fn the_outer_edges_of_the_whole_desktop_still_clip() { + // Nothing to bleed onto past x=0, but the clip must not invent + // space that no monitor covers either. + let w = Rect::new(0, 0, 400, 300); + let r = shadow_rect_clipped(w, &two_monitors()); + assert_eq!((r.x, r.y), (0, 0)); + } + + #[test] + fn no_monitors_leaves_the_rect_unclipped() { + let w = Rect::new(10, 10, 100, 100); + let r = shadow_rect_clipped(w, &[]); + assert_eq!((r.x, r.y, r.width, r.height), (10 - 24, 10 - 24, 100 + 48, 100 + 48)); + } +} |