diff options
| author | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-02-15 14:56:00 +0200 |
| commit | 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch) | |
| tree | 7672d0af277664f457c6c9462925c0005fe35dcf /crates/x11/src/platform/struts.rs | |
| parent | 413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff) | |
| download | srdwm-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/x11/src/platform/struts.rs')
| -rw-r--r-- | crates/x11/src/platform/struts.rs | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/crates/x11/src/platform/struts.rs b/crates/x11/src/platform/struts.rs new file mode 100644 index 0000000..1f58192 --- /dev/null +++ b/crates/x11/src/platform/struts.rs @@ -0,0 +1,222 @@ +//! `_NET_WM_STRUT_PARTIAL`/`_NET_WM_STRUT` tracking - the X11 half of +//! monitor usable-area reservation, matching what the Wayland backends +//! already do for a `zwlr_layer_shell_v1` bar/dock's exclusive zone (see +//! `udev/platform.rs`'s `monitors()` and its own `non_exclusive_zone` +//! doc comment). Confirmed missing entirely, live, by a peer session +//! (`aegis`) building its own X11-backend bar: an override-redirect +//! `_NET_WM_WINDOW_TYPE_DOCK` window setting a correct `_NET_WM_STRUT_ +//! PARTIAL` never shrank `srd monitors`' own reported usable rect at +//! all, so a real tiled client would have had its window placed right +//! underneath the bar. `grep -rl STRUT crates/x11/src` found nothing at +//! all before this file - the feature had simply never been built for +//! this backend, not a narrower bug in existing logic. + +use super::*; + +/// A strut reservation changed, so whatever `monitors()` would report has +/// too - `crates/srdwm/src/main.rs`'s own event loop already re-queries +/// `Platform::monitors()` and refreshes `WindowManager`'s cached list on +/// exactly this event (it's how output hotplug is handled), so reusing it +/// here is what actually makes a fresh strut reservation visible to `srd +/// monitors`/tiling/placement at all - the cached list otherwise only +/// changes on a real hotplug, never on a dock mapping or resizing itself. +/// The zero-sized placeholder `Monitor` is never read: that event handler +/// discards the payload and re-queries the real list unconditionally, the +/// same "cheap sentinel, ignored on arrival" shape the Wayland backends' +/// own equivalent trigger already uses (`layer_shell.rs`'s `layer_ +/// destroyed`, on an exclusive-zone change). +pub(super) fn monitors_changed_event() -> Event { + Event::MonitorAdded(Monitor::new(0, "", Rect::new(0, 0, 0, 0))) +} + +impl X11Platform { + /// Reads `window`'s current strut reservation, preferring `_NET_WM_ + /// STRUT_PARTIAL` (12 `CARDINAL`s: left, right, top, bottom, then each + /// edge's own start/end span) and falling back to the older, span-free + /// `_NET_WM_STRUT` (4 `CARDINAL`s: left, right, top, bottom) for a + /// client that only sets that - per the EWMH spec, a plain `_STRUT` + /// with no `_PARTIAL` reserves its margin across the *entire* length + /// of that edge, which is what defaulting its span to `0..root extent` + /// below encodes. `None` if neither property is set, or both are + /// present but empty/malformed - the same "nothing reserved" answer + /// either way. + pub(super) fn read_strut(&self, window: XWindow) -> Option<Strut> { + if let Ok(cookie) = self.conn.get_property(false, window, self.atoms._NET_WM_STRUT_PARTIAL, x11rb::protocol::xproto::AtomEnum::CARDINAL, 0, 12) { + if let Ok(reply) = cookie.reply() { + if let Some(v) = reply.value32().map(|it| it.collect::<Vec<u32>>()) { + if v.len() >= 12 { + let s = Strut { + left: v[0], + right: v[1], + top: v[2], + bottom: v[3], + left_start_y: v[4] as i32, + left_end_y: v[5] as i32, + right_start_y: v[6] as i32, + right_end_y: v[7] as i32, + top_start_x: v[8] as i32, + top_end_x: v[9] as i32, + bottom_start_x: v[10] as i32, + bottom_end_x: v[11] as i32, + }; + return if s == Strut::default() { None } else { Some(s) }; + } + } + } + } + let Ok(cookie) = self.conn.get_property(false, window, self.atoms._NET_WM_STRUT, x11rb::protocol::xproto::AtomEnum::CARDINAL, 0, 4) else { + return None; + }; + let reply = cookie.reply().ok()?; + let v: Vec<u32> = reply.value32()?.collect(); + if v.len() < 4 || v[..4] == [0, 0, 0, 0] { + return None; + } + let screen = &self.conn.setup().roots[0]; + let (w, h) = (screen.width_in_pixels as i32, screen.height_in_pixels as i32); + Some(Strut { + left: v[0], + right: v[1], + top: v[2], + bottom: v[3], + left_start_y: 0, + left_end_y: h, + right_start_y: 0, + right_end_y: h, + top_start_x: 0, + top_end_x: w, + bottom_start_x: 0, + bottom_end_x: w, + }) + } + + /// Called on `MapNotify` for any window this backend isn't already + /// managing as a regular client (see that call site's own comment for + /// why: a real panel/dock is typically override-redirect specifically + /// to skip window management entirely, so it never reaches `manage_ + /// new_window`/`xid_to_core` the way an ordinary top-level does). + /// Selecting `PROPERTY_CHANGE` here is what makes a later live resize + /// of the bar (`update_strut_property`, on `PropertyNotify`) actually + /// get noticed - without it, only the reservation this window had at + /// the moment it first mapped would ever be seen. Returns `true` when + /// a real reservation was found, so the caller can fire the same + /// `Event::MonitorAdded` re-query trigger `monitors()`'s own live + /// hotplug path already uses (see that call site's own comment for + /// why this can't just call `monitors()` again itself - the cached + /// list `srd monitors` actually reads lives in `WindowManager`, one + /// layer up, not in this struct). + pub(super) fn track_strut_window(&mut self, window: XWindow) -> bool { + let Some(strut) = self.read_strut(window) else { return false }; + self.struts.insert(window, strut); + let _ = self.conn.change_window_attributes(window, &ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE)); + let _ = self.conn.flush(); + true + } + + /// `PropertyNotify` on `_NET_WM_STRUT`/`_NET_WM_STRUT_PARTIAL` for a + /// window already being watched (`track_strut_window` selected the + /// event mask that makes this fire at all) - re-reads and either + /// updates or drops the reservation, matching whatever the client's + /// new property value actually says (a bar shrinking its own reserved + /// strip live, or clearing the property to reserve nothing at all). + /// Returns `true` only when the reservation actually changed (not + /// merely present) - see `track_strut_window`'s own doc comment for + /// what the caller does with that. + pub(super) fn update_strut_property(&mut self, window: XWindow) -> bool { + let new = self.read_strut(window); + let changed = self.struts.get(&window).copied() != new; + match new { + Some(strut) => { + self.struts.insert(window, strut); + } + None => { + self.struts.remove(&window); + } + } + changed + } + + /// `UnmapNotify`/`DestroyNotify` for a tracked strut window - a no-op + /// `HashMap::remove` for every other window, so this is safe to call + /// unconditionally from both handlers rather than needing its own + /// "was this actually a strut window" check first. Returns `true` + /// only when a real reservation actually existed and was removed -- + /// see `track_strut_window`'s own doc comment for what the caller + /// does with that. + pub(super) fn forget_strut_window(&mut self, window: XWindow) -> bool { + self.struts.remove(&window).is_some() + } + + /// `full`, shrunk by every tracked strut whose reserved band actually + /// overlaps it - the X11 equivalent of `udev/platform.rs`'s `non_ + /// exclusive_zone`-based `usable` computation, called once per + /// monitor from `monitors()`. See the free [`usable_rect`] function + /// below (the actual math, kept separate so it's unit-testable + /// without a live X11 connection) for exactly how. + pub(super) fn usable_rect_for(&self, full: Rect) -> Rect { + let screen = &self.conn.setup().roots[0]; + let screen_size = (screen.width_in_pixels as i32, screen.height_in_pixels as i32); + usable_rect(full, screen_size, self.struts.values().copied()) + } +} + +/// The actual shrink math behind [`X11Platform::usable_rect_for`], pulled +/// out as a free function of plain values (no live X11 connection needed) +/// specifically so it can be unit tested directly - `usable_rect_for` +/// itself needs `self.conn.setup()` for the root screen's own dimensions, +/// which only a real connected server can answer. +/// +/// Every strut value is a distance in from the *screen's* own edge +/// (ICCCM/EWMH `_NET_WM_STRUT_PARTIAL`: `top` reserves root-absolute +/// `y ∈ [0, top)`, `bottom` reserves `y ∈ [screen_height - bottom, +/// screen_height)`, and so on) - not from this monitor's own edge, so +/// the reserved boundary is computed in root-absolute coordinates first +/// (needing `screen_size` for the bottom/right cases) and only then +/// intersected against `full`. A strut anchored on an edge this monitor +/// doesn't border at all, or whose own start/end span doesn't overlap +/// this monitor's extent on the perpendicular axis (a bar on a different +/// monitor entirely, in a multi-monitor setup), correctly contributes no +/// shrink either way, since its reserved boundary then falls outside +/// `full` on that axis. +pub(super) fn usable_rect(full: Rect, screen_size: (i32, i32), struts: impl Iterator<Item = Strut>) -> Rect { + let (screen_w, screen_h) = screen_size; + let (full_left, full_top) = (full.x, full.y); + let (full_right, full_bottom) = (full.x + full.width as i32, full.y + full.height as i32); + let (mut left, mut top, mut right, mut bottom) = (full_left, full_top, full_right, full_bottom); + for strut in struts { + if strut.top > 0 && strut.top_end_x > full_left && strut.top_start_x < full_right { + top = top.max(strut.top as i32); + } + if strut.bottom > 0 && strut.bottom_end_x > full_left && strut.bottom_start_x < full_right { + bottom = bottom.min(screen_h - strut.bottom as i32); + } + if strut.left > 0 && strut.left_end_y > full_top && strut.left_start_y < full_bottom { + left = left.max(strut.left as i32); + } + if strut.right > 0 && strut.right_end_y > full_top && strut.right_start_y < full_bottom { + right = right.min(screen_w - strut.right as i32); + } + } + // Clamped against the monitor's own opposite edge, not just `0`: a + // strut reservation declared against the *whole screen* can still + // exceed a single monitor's own extent in a multi-monitor layout + // (e.g. a bar `top=32` on a screen where this particular monitor's + // usable band is otherwise smaller than that) - without this, an + // overshoot on one axis could invert `left > right`/`top > bottom` + // into a negative-size rect instead of clamping to "no usable space + // left on this monitor". Each axis's two edges are clamped as + // separate statements, in order (`left` before `right`, `top` before + // `bottom`), not one combined tuple `let` - a first version of this + // used `let (top, bottom) = (top.min(full_bottom), bottom.max(top))` + // in one statement, which reads `top` on the right-hand side of both + // tuple elements from the *pre-clamp* binding (a `let` only shadows + // once the whole statement finishes), silently clamping `bottom` + // against the wrong, oversized `top` - caught by a test asserting + // `usable.height == 0` for a strut taller than the monitor, which + // instead came back as `400`, not `0`. + let left = left.min(full_right); + let right = right.max(full_left).max(left); + let top = top.min(full_bottom); + let bottom = bottom.max(full_top).max(top); + Rect::new(left, top, (right - left).max(0) as u32, (bottom - top).max(0) as u32) +} |