diff options
Diffstat (limited to 'crates/wayland')
| -rw-r--r-- | crates/wayland/src/decoration.rs | 2 | ||||
| -rw-r--r-- | crates/wayland/src/decoration/shadow.rs | 114 | ||||
| -rw-r--r-- | crates/wayland/src/protocols/xdg_shell.rs | 28 | ||||
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 2 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 15 | ||||
| -rw-r--r-- | crates/wayland/src/virtual_pointer.rs | 42 | ||||
| -rw-r--r-- | crates/wayland/src/winit/capture.rs | 52 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 9 |
8 files changed, 226 insertions, 38 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs index 49cf416..9ff43b6 100644 --- a/crates/wayland/src/decoration.rs +++ b/crates/wayland/src/decoration.rs @@ -43,7 +43,7 @@ pub(crate) use buttons::HOVER_GLYPH_DURATION; pub(crate) use color::{mix_rgb, rgb_to_bgra}; pub(crate) use corners::{round_bottom_corners, round_top_corners}; pub(crate) use font::{blit_glyph, find_system_font, measure_text_width, FONT_PIXELS, TEXT_LEFT_PADDING}; -pub use shadow::{shadow_bitmap, shadow_rect}; +pub use shadow::{shadow_bitmap, shadow_rect, shadow_rect_clipped}; pub(crate) use shadow::{SHADOW_MAX_ALPHA, SHADOW_SIZE}; pub use titlebar::render_titlebar; 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)); + } +} diff --git a/crates/wayland/src/protocols/xdg_shell.rs b/crates/wayland/src/protocols/xdg_shell.rs index 4fe0f6f..0236736 100644 --- a/crates/wayland/src/protocols/xdg_shell.rs +++ b/crates/wayland/src/protocols/xdg_shell.rs @@ -8,7 +8,6 @@ use smithay::input::pointer::Focus; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_seat; -use smithay::reexports::wayland_server::Resource; use smithay::utils::Serial; use smithay::wayland::shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState}; @@ -185,22 +184,11 @@ impl XdgShellHandler for CompState { /// real follow-up, not this fix); an occasional popup placed near a /// screen edge may render partly off it, which is cosmetic, not a hang. fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) { - // Temporary: live report is that Nemo's right-click context menu - // never appears at all (not mispositioned - entirely invisible), - // while the exact same xdg_popup mechanism works for Firefox. Logs - // the unconstrained geometry this popup gets so a live repro tells - // us whether it's landing off-screen/degenerate (the known gap this - // function's own doc comment already flags) or something else - // entirely. Remove once resolved. - let geom = positioner.get_geometry(); - let parent = surface.get_parent_surface(); - log::warn!("POPUP-GEOM-DIAG geometry={geom:?} parent={:?}", parent.as_ref().map(|s| s.id())); surface.with_pending_state(|state| { - state.geometry = geom; + state.geometry = positioner.get_geometry(); state.positioner = positioner; }); if surface.send_configure().is_err() { - log::warn!("POPUP-GEOM-DIAG send_configure failed"); return; } let _ = self.popups.track_popup(smithay::desktop::PopupKind::Xdg(surface)); @@ -223,19 +211,9 @@ impl XdgShellHandler for CompState { /// `resize_request` already ignore the same parameter. fn grab(&mut self, surface: PopupSurface, _seat: wl_seat::WlSeat, serial: Serial) { let popup = PopupKind::Xdg(surface); - let Ok(root) = find_popup_root_surface(&popup) else { - log::warn!("POPUP-GRAB-DIAG find_popup_root_surface failed"); - return; - }; + let Ok(root) = find_popup_root_surface(&popup) else { return }; let seat = self.seat.clone(); - let grab = match self.popups.grab_popup(root, popup, &seat, serial) { - Ok(g) => g, - Err(e) => { - log::warn!("POPUP-GRAB-DIAG grab_popup failed: {e:?}"); - return; - } - }; - log::warn!("POPUP-GRAB-DIAG grab established, has_pointer={} has_keyboard={}", seat.get_pointer().is_some(), seat.get_keyboard().is_some()); + let Ok(grab) = self.popups.grab_popup(root, popup, &seat, serial) else { return }; if let Some(keyboard) = seat.get_keyboard() { keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); } diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs index 22df0e1..cae7177 100644 --- a/crates/wayland/src/udev/mod.rs +++ b/crates/wayland/src/udev/mod.rs @@ -338,7 +338,7 @@ impl UdevState { /// `(x, y, width, height)` tuples rather than real `UdevHead`s - pulled /// out so it's testable without a real DRM/`Card` handle, which every /// `UdevHead` in this module otherwise needs to even construct. -fn bounds_of(heads: impl Iterator<Item = (i32, i32, i32, i32)>) -> (f64, f64, f64, f64) { +pub(crate) fn bounds_of(heads: impl Iterator<Item = (i32, i32, i32, i32)>) -> (f64, f64, f64, f64) { let mut min_x = 0; let mut min_y = 0; let mut max_x = 0; diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index b435276..ed62290 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -40,6 +40,11 @@ impl CompState { // below just re-offsets these same positions by its own `origin`. self.ensure_desktop_icons(); let desktop_icon_render_list = self.desktop_icon_render_list(); + // Same "gather immutable state before `self.udev` is borrowed + // mutably" reason as everything else in this block - used by the + // per-window shadow push below to keep a shadow off any monitor its + // own window does not occupy (`decoration::shadow_rect_clipped`). + let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect(); // Captured-and-blurred backgrounds collected during the per-head // loop below, applied via `self.capture_output` only after it // ends - `self.udev`'s mutable borrow is held for the whole loop @@ -876,11 +881,17 @@ impl CompState { // corners nearly meet, which this compositor's default // cascade placement does constantly. if let Some(shadow) = self.shadow_buffers.get(&id) { - let rect = decoration::shadow_rect(frame); + // `full` is the bitmap's own extent and stays + // unclipped, because `src` below indexes into that + // bitmap; `rect` is the same box clipped to the + // monitors this window actually occupies, and only + // decides which fragments get drawn. + let full = decoration::shadow_rect(frame); + let rect = decoration::shadow_rect_clipped(frame, &monitor_bounds); for fragment in crate::elements::visible_border_fragments(rect, &occluders) { let pos = ((fragment.x - origin.x) as f64, (fragment.y - origin.y) as f64); let src = Rectangle::new( - Point::from(((fragment.x - rect.x) as f64, (fragment.y - rect.y) as f64)), + Point::from(((fragment.x - full.x) as f64, (fragment.y - full.y) as f64)), Size::from((fragment.width as f64, fragment.height as f64)), ); match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, shadow, None, Some(src), None, Kind::Unspecified) { 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 diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs index 3b1d38e..d21ed72 100644 --- a/crates/wayland/src/winit/capture.rs +++ b/crates/wayland/src/winit/capture.rs @@ -28,9 +28,34 @@ impl WaylandPlatform { // any layer-shell surface at all. let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen); let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new(); + // Popups first, so they land above everything else - exactly the + // order both on-screen render loops already use. + // + // This pass had no popup step at all, so no tooltip, dropdown or + // right-click menu could ever appear in a screenshot taken on this + // backend, no matter how correctly it was drawn on screen. That is + // a capture-only gap, not a rendering one: the DRM backend serves + // screencopy out of its own on-screen frame (`udev/render.rs` + // drains `screencopy_pending` and hands `service_pending` the same + // framebuffer it just drew), so it never had the gap; this backend + // renders the scene a second time into an offscreen buffer, and + // that second scene was missing a tier. + // + // It cost real time to find. The whole point of the nested backend + // is validating behaviour with `grim`, and this made `grim` state + // the opposite of the truth about every popup: a menu that drew + // perfectly on screen photographed as absent, which reads exactly + // like the client never opened one. + let popup_targets = crate::elements::popup_targets(&self.state); + custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, renderer, (0, 0))); if !hide_top_layers { custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Top | Layer::Overlay))); } + // Front-to-back, so a window already pushed occludes everything a + // later one draws - accumulated for the shadow clip below, exactly + // as both on-screen render loops do it. + let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect(); + let mut occluders: Vec<srdwm_core::Rect> = Vec::new(); for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::<Vec<_>>() { let Some(w) = self.wm.borrow().window(id).cloned() else { continue }; if let Some(deco) = self.state.decorations.get(&id) { @@ -55,6 +80,33 @@ impl WaylandPlatform { custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity)); } } + // The drop shadow, last in this window's own group so it sits + // under its own decoration and content but still over every + // window behind it. `shadow_buffers` only holds an entry for a + // window that is meant to have one at all (see + // `state/lifecycle.rs`), so no separate floating/maximized + // check belongs here. + // + // Shadows were the other half of this pass's missing tier: a + // screenshot taken on this backend showed no shadow on any + // window, which is exactly the thing a shadow bug gets reported + // and re-checked by. Border strips are still absent - a real + // remaining gap, called out here rather than left silent. + if let Some(shadow) = self.state.shadow_buffers.get(&id) { + let full = crate::decoration::shadow_rect(w.geometry); + let rect = crate::decoration::shadow_rect_clipped(w.geometry, &monitor_bounds); + for fragment in crate::elements::visible_border_fragments(rect, &occluders) { + let src = Rectangle::new( + Point::from(((fragment.x - full.x) as f64, (fragment.y - full.y) as f64)), + Size::from((fragment.width as f64, fragment.height as f64)), + ); + match MemoryRenderBufferRenderElement::from_buffer(renderer, (fragment.x as f64, fragment.y as f64), shadow, None, Some(src), None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)), + Err(e) => log::warn!("screencopy: failed to import shadow buffer for window {id}: {e}"), + } + } + } + occluders.push(w.geometry); } custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Background | Layer::Bottom))); diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs index a07e074..dff425f 100644 --- a/crates/wayland/src/winit/render.rs +++ b/crates/wayland/src/winit/render.rs @@ -210,6 +210,10 @@ impl WaylandPlatform { // it correctly. The border strips and titlebar bitmap are // different: outside `geometry`, so they still need `occluders`' // explicit clip against whichever window is stacked in front. + // See the matching push in `udev/render.rs` - this backend only + // ever has the one output, so the clip is a no-op here, but reading + // the same source keeps the two shadow paths from drifting. + let monitor_bounds: Vec<srdwm_core::Rect> = self.wm.borrow().monitors().iter().map(|m| m.full_geometry).collect(); let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len()); for id in ids { let Some(w) = self.wm.borrow().window(id).cloned() else { continue }; @@ -405,11 +409,12 @@ impl WaylandPlatform { // exactly where two windows' corners nearly meet, which this // compositor's default cascade placement does constantly. if let Some(shadow) = self.state.shadow_buffers.get(&id) { - let rect = decoration::shadow_rect(frame); + let full = decoration::shadow_rect(frame); + let rect = decoration::shadow_rect_clipped(frame, &monitor_bounds); for fragment in crate::elements::visible_border_fragments(rect, &occluders) { let pos = (fragment.x as f64, fragment.y as f64); let src = Rectangle::new( - Point::from(((fragment.x - rect.x) as f64, (fragment.y - rect.y) as f64)), + Point::from(((fragment.x - full.x) as f64, (fragment.y - full.y) as f64)), Size::from((fragment.width as f64, fragment.height as f64)), ); match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, Some(src), None, Kind::Unspecified) { |