srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-15 23:53:00 +0200
committersrdusr <[email protected]>2025-08-15 23:53:00 +0200
commitbd6aadf4d166c05f22f1ab6cdfe2e815e071f162 (patch)
tree2059f67bd8f961468a9a76c2ef77af17e1b93c66
parentcc22fe69a68e0fff027d833029aea850976488c8 (diff)
downloadsrdwm-bd6aadf4d166c05f22f1ab6cdfe2e815e071f162.tar.gz
srdwm-bd6aadf4d166c05f22f1ab6cdfe2e815e071f162.zip
Fix a dragged/resized window rendering wrong on a different-scale monitor mid-gesture
Reported live: moving a window onto the other monitor "looks very messed up". This machine's two real monitors have genuinely different scales (eDP-1 at 1.0, HDMI-A-1 at ~0.843) - the exact condition needed to expose this. WindowManager::update_drag/update_resize only corrected w.monitor once, at end_drag (update_resize never corrected it at all, not even at the end) - but state/geometry.rs::sync_geometry reads that field on every motion tick to pick which monitor's scale converts the client's physical size into the logical points xdg_toplevel::configure sends it. Crossing onto a different-scale monitor mid-drag kept every configure computed against the origin monitor's stale scale for the gesture's whole remaining duration, only self-correcting once the button came up. Both functions now re-derive w.monitor from which monitor the window's live geometry actually overlaps, every motion tick - the same Rect::overlaps lookup end_drag already used once at the end, now run continuously instead. end_drag's own fixup stays as a final-word safety net for a drag that starts and ends between two motion ticks. Does not close the related, already-documented gap where a client that doesn't speak wp-fractional-scale-v1 still mismatches once settled on a sub-1.0-scaled monitor - this only fixes the stale-during-the-gesture half. Two new tests, full workspace suite and clippy clean.
-rw-r--r--crates/core/src/manager/dragresize.rs49
-rw-r--r--crates/core/src/manager/tests.rs53
-rw-r--r--docs/TODO.md10
3 files changed, 101 insertions, 11 deletions
diff --git a/crates/core/src/manager/dragresize.rs b/crates/core/src/manager/dragresize.rs
index b3132a5..912c267 100644
--- a/crates/core/src/manager/dragresize.rs
+++ b/crates/core/src/manager/dragresize.rs
@@ -43,8 +43,29 @@ impl WindowManager {
new_geom.y = new_geom.y.clamp(bounds.y, bounds.bottom() - 40);
}
+ // Live, every motion tick - not just once at `end_drag`, which
+ // used to be the only place this got corrected (see its own doc
+ // comment on why `w.monitor` goes stale at all). Between here and
+ // there, `state/geometry.rs::sync_geometry` - called on every one
+ // of these same motion ticks while a drag is active - reads this
+ // exact field to pick which monitor's `scale` converts the
+ // client's real physical size into the logical points `xdg_
+ // toplevel::configure` sends it. Two real monitors at genuinely
+ // different scales (confirmed live: `1.0` and `~0.84`), a window
+ // dragged from one onto the other kept computing every mid-drag
+ // configure against the *origin* monitor's scale for the drag's
+ // entire remaining duration - the client resizing itself to a
+ // logical size that doesn't match the physical footprint the
+ // border/decoration are actually drawing around it, only self-
+ // correcting the instant the button came up. Reported live as a
+ // dragged window "looking very messed up" on the other monitor.
+ let now_on = self.monitors.iter().find(|m| m.geometry.overlaps(&new_geom)).map(|m| m.id);
+
if let Some(w) = self.windows.get_mut(&drag.window) {
w.geometry = new_geom;
+ if let Some(now_on_id) = now_on {
+ w.monitor = now_on_id;
+ }
}
}
@@ -52,17 +73,13 @@ impl WindowManager {
/// near a monitor edge.
pub fn end_drag(&mut self) {
if let Some(drag) = self.drag.take() {
- // `w.monitor` only ever gets set at window creation (or by
- // `set_monitors`, reactively, on the *next* hotplug) - a drag
- // that crossed onto a different monitor leaves it stale
- // pointing at wherever the window *started*, same gap
- // `set_monitors`'s own doc comment already documents for the
- // hotplug-rehoming case. Corrected here, before computing the
- // snap zone below, not after - using the stale value there
- // would check the *wrong* monitor's snap zones (e.g. still
- // snapping against monitor 1's left edge for a window that's
- // now actually sitting near monitor 2's), the same bug this is
- // fixing for maximize/fullscreen one level up.
+ // `update_drag` above already keeps `w.monitor` live on every
+ // motion tick now, so this is normally just confirming what's
+ // already current - kept anyway as the final word before
+ // computing the snap zone below (a drag that starts and ends
+ // between two motion ticks, however unlikely, would otherwise
+ // check the *wrong* monitor's snap zones), the same bug this
+ // was originally fixing for maximize/fullscreen one level up.
if let Some(w) = self.windows.get(&drag.window) {
if let Some(now_on) = self.monitors.iter().find(|m| m.geometry.overlaps(&w.geometry)) {
let now_on_id = now_on.id;
@@ -95,8 +112,18 @@ impl WindowManager {
let Some(r) = &self.resize else { return };
let (dx, dy) = (x - r.start_x, y - r.start_y);
let new_geom = r.edge.apply_delta(r.orig, dx, dy, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
+ // Same live `w.monitor` correction as `update_drag`'s own doc
+ // comment explains - a resize can cross a monitor boundary at
+ // the edge being dragged just as easily as a drag can carry the
+ // whole window across one, and `sync_geometry`'s per-tick scale
+ // lookup doesn't care which kind of geometry change put the
+ // window there.
+ let now_on = self.monitors.iter().find(|m| m.geometry.overlaps(&new_geom)).map(|m| m.id);
if let Some(w) = self.windows.get_mut(&r.window) {
w.geometry = new_geom;
+ if let Some(now_on_id) = now_on {
+ w.monitor = now_on_id;
+ }
}
}
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index 88699de..4739278 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -123,6 +123,33 @@
}
#[test]
+ fn dragging_across_a_monitor_boundary_updates_monitor_live_not_just_at_end() {
+ // Reported live: a window dragged onto a second monitor with a
+ // different scale (confirmed live: 1.0 and ~0.84) "looks very
+ // messed up" - `state/geometry.rs::sync_geometry` reads `w.
+ // monitor` on every drag motion tick to pick which scale converts
+ // the client's physical size into the logical points `xdg_
+ // toplevel::configure` sends it, and `w.monitor` used to only get
+ // corrected once, at `end_drag`, leaving every mid-drag configure
+ // computed against the wrong monitor's scale for the drag's whole
+ // remaining duration.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(1000, 100, 200, 150); // fully on monitor 0
+ wm.add_window(w);
+ wm.window_mut(a).unwrap().monitor = 0;
+ wm.start_drag(a, 1010, 110);
+ assert_eq!(wm.window(a).unwrap().monitor, 0);
+ // Dragged fully onto monitor 1 - checked immediately, before
+ // `end_drag` runs at all.
+ wm.update_drag(1600, 110);
+ assert_eq!(wm.window(a).unwrap().monitor, 1, "monitor must update live during the drag, not only once it ends");
+ }
+
+ #[test]
fn drag_ending_near_edge_snaps_to_half_screen() {
let mut wm = wm_with_monitor();
wm.set_layout(wm.current_workspace(), "tiling");
@@ -154,6 +181,32 @@
}
#[test]
+ fn resizing_across_a_monitor_boundary_updates_monitor_live() {
+ // Same fix as `dragging_across_a_monitor_boundary_updates_monitor_
+ // live_not_just_at_end`'s own doc comment - a resize can carry the
+ // edge being dragged onto a different monitor just as easily as a
+ // move can carry the whole window, and `update_resize` never
+ // corrected `w.monitor` at all before this fix, not even at the
+ // end.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.geometry = Rect::new(1000, 100, 500, 150); // fully on monitor 0, right edge at 1500
+ wm.add_window(w);
+ wm.window_mut(a).unwrap().monitor = 0;
+ wm.start_resize(a, ResizeEdge::Left, 1050, 100);
+ // Drags the left edge from 1000 to 1290 (right edge anchored at
+ // 1500, final width 210 - comfortably above MIN_WINDOW_WIDTH),
+ // landing the whole window past the 1280 boundary on monitor 1.
+ wm.update_resize(1340, 100);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!(g, Rect::new(1290, 100, 210, 150), "sanity: resize must actually have cleared the boundary");
+ assert_eq!(wm.window(a).unwrap().monitor, 1, "monitor must update live during the resize");
+ }
+
+ #[test]
fn ending_a_resize_remembers_the_new_size_for_the_apps_next_window() {
let mut wm = wm_with_monitor();
// Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
diff --git a/docs/TODO.md b/docs/TODO.md
index 1de3cee..7a11584 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -266,6 +266,16 @@ Not fixed generically this session - the immediate, concrete mitigation the user
Second-order finding from a peer session verifying the above: `grim`'s own multi-output capture is *itself* affected by the same physical/logical confusion, independent of anything srdwm's compositor code does - capturing two 1920x1080-logical outputs (one at scale 1.0, one at ~0.843) produced a single stitched image sized `3840x1281`, not `3840x2160`(both at scale) or `3840x1080` cleanly, meaning the capture tool's own per-output stitching mixes physical and logical sizing across outputs of different scale. `srd clients`/`srd monitors` report physical throughout (this compositor's own established convention); a screenshot taken across a sub-1.0-scaled output can't be trusted to line up with those numbers pixel-for-pixel without correcting for whatever `grim` itself did - a testing-infrastructure gap on top of the client-rendering one, not fixable from srdwm's side either. One more argument for clamping the floor at 1.0, alongside the client-compatibility one above.
+## Real bug, root-caused and fixed: a window dragged or resized onto a different-scale monitor rendered "very messed up" for the whole gesture, only correcting itself on release (2026-08-25)
+
+Reported live: moving a window onto the other monitor looks very messed up. This machine's own two real monitors have genuinely different scales (`srd monitors`: `eDP-1` at `1.0`, `HDMI-A-1` at `~0.843`, the same auto-scaled-below-1.0 monitor the 2026-08-21 entries above already have a long history with) - the exact condition needed to expose this.
+
+Root cause in `WindowManager::update_drag`/`update_resize` (`crates/core/src/manager/dragresize.rs`): `w.monitor` used to only get corrected once, at `end_drag` (`update_resize` never corrected it at all, not even at the end) - but `state/geometry.rs::sync_geometry`, called on every single motion tick while a drag or resize is in progress, reads that exact field to pick which monitor's `scale` converts the client's real physical size into the logical points `xdg_toplevel::configure` sends it. Dragging (or resizing) a window from one monitor onto the other kept every mid-gesture configure computed against the *origin* monitor's stale scale for the gesture's entire remaining duration - the client resizing itself to a logical size that doesn't match the physical footprint the border/decoration were actually drawing around it on the *new* monitor, self-correcting only the instant the button came up (which is when `end_drag`'s own existing fixup finally ran).
+
+Fixed at the source, same "close the gap where the field actually goes stale" approach as this session's earlier resize-lag fix: both `update_drag` and `update_resize` now re-derive `w.monitor` from which monitor the window's live geometry actually overlaps, every motion tick, the same `Rect::overlaps`-based lookup `end_drag` already used once at the very end. `end_drag`'s own fixup is left in place as a final-word safety net (a drag that starts and ends between two motion ticks would otherwise skip the correction entirely), now normally just reconfirming what `update_drag` already set.
+
+Does not fully close the family of scale-crossing issues this shares a root cause with - see the 2026-08-21 entries just above: a client that doesn't speak `wp-fractional-scale-v1` will still show a content/frame mismatch once *settled* on the sub-1.0-scaled monitor, independent of this fix, which only closes the *stale-during-the-gesture* half of the problem. Two new tests (`dragging_across_a_monitor_boundary_updates_monitor_live_not_just_at_end`, `resizing_across_a_monitor_boundary_updates_monitor_live`); full workspace test suite (195 core / 124 wayland) and clippy clean; installed, pending a live restart to confirm.
+
## Real bug, root-caused, not yet fixed: `com.canonical.AppMenu.Registrar` is owned by AGS, not srdwm, despite srdwm's own code deliberately trying to claim it (2026-08-21)
Flagged by a peer session (`dotfiles-04`): `busctl --user list` shows the classic Qt/`appmenu-qt5` global-menu registrar name owned by AGS's own `gjs` process, not srdwm - confirmed live (`OwnerUID` traced to the AGS pid). `AppmenuRegistrarState::new()` (`crates/platform/src/appmenu_registrar.rs`) is genuinely constructed at startup (`xwayland.rs`'s `XWaylandEvent::Ready` handler, alongside `EwmhState::connect`) and its own D-Bus name request sets `replace_existing_names(true)` - and srdwm's log has no warning from the `Err` branch that would fire if the connection/name request failed outright, meaning the `zbus` call chain reports success from srdwm's own side despite not actually owning the name afterward.