diff options
| author | srdusr <[email protected]> | 2024-04-12 22:35:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-04-12 22:35:00 +0200 |
| commit | a9dd8a6d4947cb537f7919c5a66ba4624af61800 (patch) | |
| tree | d4731cded04ce159ebf3547d4dfe95ca24f4bb02 /crates/core/src/geometry.rs | |
| parent | 709333908d5b7d3157165d1811c8bc1795ab0028 (diff) | |
| download | srdwm-a9dd8a6d4947cb537f7919c5a66ba4624af61800.tar.gz srdwm-a9dd8a6d4947cb537f7919c5a66ba4624af61800.zip | |
udev: connector hotplug, and rescue windows on an unplugged monitor
Monitors were probed once at startup, so plugging or unplugging one while
srdwm was running went unnoticed. A UdevBackend event source now watches for
the kernel's `change` uevent and reconciles the head list against a fresh
connector probe - forcing a re-probe rather than trusting cached status,
since on a hotplug the cache is exactly what has gone stale.
Removing a head tears down everything it owned: the wl_output global, its
place in the Space, its DRM framebuffers and dumb buffers (dropping the Rust
structs alone leaks the kernel-side objects, which matters when a cable is
plugged repeatedly), and any lock surface for it - otherwise
confirm_lock_if_presented would wait forever on a monitor that no longer
exists. New connectors go through the same bring_up_head path as startup, so
a monitor plugged in later is set up identically to one present at boot.
Heads are then repositioned left-to-right, since removing one shifts the
rest, and layer maps re-arranged so bars follow their moved output.
set_monitors rehomes windows stranded by the change, and main.rs re-queries
the whole monitor list on MonitorAdded/MonitorRemoved rather than applying
the single monitor in the event, because the others' positions move too.
The rehoming had a bug that unit tests missed and live testing caught.
It originally keyed off Window::monitor, but that field records the monitor
a window was *assigned* at creation, not where it is: add_window always sets
it from the primary monitor, so a window placed on the second monitor by a
rule - or dragged there - still reads monitor == 0. The field-only check
saw a valid id, skipped the window, and left it at coordinates that no
longer existed: invisible and unreachable. Found by unplugging a monitor out
from under a real xterm and watching it vanish from both heads. It now keys
off geometry, with a regression test that fails against the old logic.
Verified in the QEMU VM, booting with one connector and toggling the second
at runtime: plug in -> head added and rendering at its own resolution;
unplug -> head removed cleanly; and an xterm at global x=1500 survived its
monitor being unplugged, reappearing at x=680 (= min(1500, 1280-600)) with
its size intact. Writing to /sys/class/drm/<connector>/status changes the
connector but emits no uevent on this kernel, so the signal the kernel would
send is synthesized with `udevadm trigger`; the whole reaction path is
genuinely exercised.
Diffstat (limited to 'crates/core/src/geometry.rs')
| -rw-r--r-- | crates/core/src/geometry.rs | 17 |
1 files changed, 17 insertions, 0 deletions
diff --git a/crates/core/src/geometry.rs b/crates/core/src/geometry.rs index 75feeef..3dd205a 100644 --- a/crates/core/src/geometry.rs +++ b/crates/core/src/geometry.rs @@ -42,6 +42,23 @@ impl Rect { pub fn center(&self) -> (i32, i32) { (self.x + self.width as i32 / 2, self.y + self.height as i32 / 2) } + + /// Moves this rect so it lies inside `bounds`, shrinking it only if it + /// is genuinely larger than `bounds`. + /// + /// Used when a monitor is unplugged and its windows have to be rehomed: + /// a window at coordinates that no longer exist would otherwise be + /// off-screen and unreachable. Position is adjusted in preference to + /// size so a window keeps the dimensions the user gave it. + pub fn clamped_into(&self, bounds: Rect) -> Rect { + let width = self.width.min(bounds.width); + let height = self.height.min(bounds.height); + // `max(bounds.x)` after `min` so that a bounds smaller than the rect + // still yields the bounds' own origin rather than a negative offset. + let x = (self.x).min(bounds.right() - width as i32).max(bounds.x); + let y = (self.y).min(bounds.bottom() - height as i32).max(bounds.y); + Rect { x, y, width, height } + } } #[cfg(test)] |