srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/input/layers.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/input/layers.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz
srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
Diffstat (limited to 'crates/wayland/src/input/layers.rs')
-rw-r--r--crates/wayland/src/input/layers.rs169
1 files changed, 169 insertions, 0 deletions
diff --git a/crates/wayland/src/input/layers.rs b/crates/wayland/src/input/layers.rs
new file mode 100644
index 0000000..718d83f
--- /dev/null
+++ b/crates/wayland/src/input/layers.rs
@@ -0,0 +1,169 @@
+//! `zwlr_layer_shell_v1` pointer hit-testing (bars, docks, launchers) and
+//! the layer-driven maximize-geometry computation both backends' `monitors()`
+//! need.
+
+use smithay::desktop::{layer_map_for_output, WindowSurfaceType};
+use smithay::output::Output;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::reexports::wayland_server::Resource as _;
+use smithay::utils::{Logical, Point};
+use smithay::wayland::compositor::with_states;
+use smithay::wayland::shell::wlr_layer::{Anchor, ExclusiveZone, Layer, LayerSurfaceCachedState};
+
+use crate::state::CompState;
+
+/// Topmost layer-shell surface (if any) under `pos`, checked in the same
+/// above-everything-else stacking order `space_render_elements` renders
+/// `Overlay`/`Top` layers in (bars, launchers, notifications, lock UIs).
+/// `Background`/`Bottom` layers (wallpapers) deliberately aren't checked
+/// here: nothing in scope for the daily-driver gate needs pointer input
+/// routed to them, and space windows should stay clickable over a
+/// wallpaper.
+/// `pos` is in the global space; layer geometry is relative to its own
+/// output, so the pointer is translated into output-local coordinates
+/// before hit-testing and the result translated back out.
+/// Only checked for `Overlay`/`Top` before a window hit-test, and again for
+/// `Bottom`/`Background` after one comes up empty - see the two call
+/// sites in `handle_pointer_button`/`handle_pointer_position` for why it's
+/// split rather than one four-layer loop here. A `Bottom`/`Background`
+/// surface (a desktop-icons layer, a wallpaper daemon that wants clicks) is
+/// meant to sit *behind* normal windows, so a window covering that point
+/// should still get the click; `Overlay`/`Top` (an on-screen keyboard, a
+/// bar, a dock) are meant to sit in front of everything, windows included.
+///
+/// Was `Overlay`/`Top` only, full stop - a `Bottom`-layer surface was
+/// silently unclickable no matter what, since nothing else in
+/// `handle_pointer_button` ever checked layers at all. Not the cause of
+/// the live "clicking the dock does nothing" report (confirmed: that dock
+/// uses `Layer::Top`, which was already checked), but a real, separate gap
+/// found while chasing it - worth closing regardless of whether anything
+/// currently deployed sits at `Bottom`/`Background` yet.
+pub(super) fn layer_surface_under_layers(state: &CompState, pos: Point<f64, Logical>, layers: [Layer; 2]) -> Option<(WlSurface, Point<i32, Logical>)> {
+ let entry = state.output_at(pos)?;
+ let origin = entry.location;
+ let local = pos - origin.to_f64();
+ let map = layer_map_for_output(&entry.output);
+ for layer_kind in layers {
+ // Not `map.layer_under(layer_kind, local)` - that hands back only
+ // the single topmost surface whose *bounding box* contains `local`,
+ // and if that one surface's own input region excludes the point
+ // (its `surface_under` below returns `None`), the old code gave up
+ // on this whole layer-kind rather than trying whatever real,
+ // clickable surface is stacked underneath it. A bbox-only pick is
+ // exactly wrong the moment two surfaces on the same layer-kind
+ // overlap - a transparent, mapped-but-mostly-empty surface (a
+ // backdrop-dismiss popup, concretely: `Overview`'s own bbox-wide
+ // fallback region was exactly this shape before it was fixed
+ // AGS-side) sitting in front of a real one in z-order would
+ // silently swallow every click and even every hover/motion event
+ // meant for the surface underneath, with no way to reach it at
+ // all. Walking every candidate on this layer-kind, topmost first
+ // (`.rev()`, matching `layer_under`'s own z-order convention), and
+ // falling through to the next when a candidate's real input region
+ // doesn't cover the point, is what `layer_under` alone can't do.
+ for layer in map.layers_on(layer_kind).rev() {
+ let Some(geo) = map.layer_geometry(layer) else { continue };
+ if !geo.to_f64().contains(local) {
+ continue;
+ }
+ // Temporary: verifying the `layer_surfaces_shown_once` fix
+ // (state/layers.rs) actually stops a reused `wl_surface`'s
+ // stale layer-shell entry from outliving its role destroy --
+ // live-reproduced this session as a full-monitor click-catcher
+ // popup whose hit-tested geometry came back wider than the
+ // real output after several open/close cycles. Remove once a
+ // restart confirms the geometry stays sane across repeated
+ // popup toggles.
+ let local_in_surface = local - geo.loc.to_f64();
+ // `None` here means "no region ever committed" - per-protocol
+ // that means the *whole* surface is input-sensitive, not that
+ // nothing is, so it is its own distinct, meaningful answer from
+ // `Some([])` (a region was committed and it is empty).
+ let region_dump = with_states(layer.wl_surface(), |states| {
+ states.cached_state.get::<smithay::wayland::compositor::SurfaceAttributes>().current().input_region.as_ref().map(|r| r.rects.clone())
+ });
+ log::info!(
+ "layer_hit_test: layer={:?} namespace={:?} surface={:?} geo={:?} local_in_surface={:?} input_region={:?}",
+ layer_kind,
+ layer.namespace(),
+ layer.wl_surface().id(),
+ geo,
+ local_in_surface,
+ region_dump
+ );
+ if let Some((surface, surface_loc)) = layer.surface_under(local - geo.loc.to_f64(), WindowSurfaceType::ALL) {
+ return Some((surface, origin + geo.loc + surface_loc));
+ }
+ }
+ }
+ None
+}
+
+pub(super) fn layer_surface_under(state: &CompState, pos: Point<f64, Logical>) -> Option<(WlSurface, Point<i32, Logical>)> {
+ layer_surface_under_layers(state, pos, [Layer::Overlay, Layer::Top])
+}
+
+/// The `Bottom`/`Background` half of the same lookup - see
+/// `layer_surface_under_layers`'s doc comment for the ordering rationale.
+pub(super) fn background_layer_surface_under(state: &CompState, pos: Point<f64, Logical>) -> Option<(WlSurface, Point<i32, Logical>)> {
+ layer_surface_under_layers(state, pos, [Layer::Bottom, Layer::Background])
+}
+
+/// `full` with only a top-anchored layer surface's exclusive zone (a menu
+/// bar) subtracted back out - see `Monitor::maximize_geometry`'s own doc
+/// comment for why maximize needs this third rect, distinct from both
+/// `geometry` (every zone subtracted) and `full_geometry` (none). Shared by
+/// both backends' `monitors()`, same as everything else in this module.
+/// Deliberately re-derived from the layer list rather than reusing
+/// `non_exclusive_zone()`: that smithay helper folds every anchor
+/// together with no way to ask it to skip one edge - see below for which
+/// edges this now shrinks for and why.
+///
+/// Shrinks for a reservation on *any* edge (top, bottom, left, or right),
+/// not top only - reported live as a maximized window's own bottom edge
+/// and border ending up underneath a bottom-anchored dock, indistinguishable
+/// from the dock not rendering at all. An earlier version of this
+/// function shrank only for a top-anchored bar, on the reasoning that
+/// maximize should be able to "go past" a dock while fullscreen (which
+/// already ignores every zone, via `full_geometry`) covers the case that
+/// wants the screen entirely to itself - but no other edge actually
+/// benefits from that distinction the way a top menu bar does, and
+/// respecting every edge here is what every mainstream desktop's own
+/// maximize convention already does. Fullscreen is unaffected - it never
+/// called this function, and still doesn't.
+pub(crate) fn maximize_geometry_for(output: &Output, full: srdwm_core::Rect) -> srdwm_core::Rect {
+ let mut rect = full;
+ // `exclusive_zone`/`margin` are logical (a layer-shell client reports
+ // its own reservation the same way every other layer-shell geometry
+ // is expressed), while `full` is physical pixels - same unit
+ // mismatch `Platform::monitors()` needed fixing for, and the same
+ // fix: scale the logical amount into physical pixels before touching
+ // a physical rect with it. Left unconverted, a scaled output's
+ // maximize target shrank by the wrong number of physical rows/columns
+ // for its own bar/dock (too few at scale < 1.0, too many above 1.0).
+ let scale = output.current_scale().fractional_scale();
+ for layer in layer_map_for_output(output).layers() {
+ let data = with_states(layer.wl_surface(), |states| *states.cached_state.get::<LayerSurfaceCachedState>().current());
+ let ExclusiveZone::Exclusive(amount) = data.exclusive_zone else { continue };
+ let scaled = |margin: i32| ((amount as f64 + margin as f64) * scale).round().max(0.0) as i32;
+ if data.anchor.contains(Anchor::TOP) && !data.anchor.contains(Anchor::BOTTOM) {
+ let shrink = scaled(data.margin.top);
+ rect.y += shrink;
+ rect.height = rect.height.saturating_sub(shrink as u32);
+ }
+ if data.anchor.contains(Anchor::BOTTOM) && !data.anchor.contains(Anchor::TOP) {
+ let shrink = scaled(data.margin.bottom);
+ rect.height = rect.height.saturating_sub(shrink as u32);
+ }
+ if data.anchor.contains(Anchor::LEFT) && !data.anchor.contains(Anchor::RIGHT) {
+ let shrink = scaled(data.margin.left);
+ rect.x += shrink;
+ rect.width = rect.width.saturating_sub(shrink as u32);
+ }
+ if data.anchor.contains(Anchor::RIGHT) && !data.anchor.contains(Anchor::LEFT) {
+ let shrink = scaled(data.margin.right);
+ rect.width = rect.width.saturating_sub(shrink as u32);
+ }
+ }
+ rect
+}