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/winit | |
| 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/winit')
| -rw-r--r-- | crates/wayland/src/winit/capture.rs | 52 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 9 |
2 files changed, 59 insertions, 2 deletions
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) { |