diff options
| author | srdusr <[email protected]> | 2025-08-29 22:42:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-08-29 22:42:00 +0200 |
| commit | be5c4efa6866b5d23e1e81ecfc3b8d276540860f (patch) | |
| tree | 0732d942bf28d26cc6996549fadd1785e566a637 /crates/core/src/manager/windows.rs | |
| parent | bd7141718901c6f41511137e4b34a3bd9e2705b1 (diff) | |
| download | srdwm-be5c4efa6866b5d23e1e81ecfc3b8d276540860f.tar.gz srdwm-be5c4efa6866b5d23e1e81ecfc3b8d276540860f.zip | |
Core window manager: real fixes plus three new rule/placement primitives
Several independent, real pieces landed in crates/core this shift - see
docs/TODO.md for each one's full root-cause/verification narrative:
- "Primary" monitor is now picked by which head sits at physical (0, 0)
(the user's own configured anchor), not whichever connector DRM
happened to probe first - fixes desktop icons and new-window placement
landing on the wrong monitor depending on hotplug/probe order.
- A new window's target monitor now prioritizes the pointer's own current
monitor over the last-focused window's monitor, which goes stale the
moment the user's attention moves to empty desktop, a panel, or a dock.
- aspect_ratio window-rule action ("W:H") plus ResizeEdge::apply_aspect_
ratio: holds a floating window's aspect ratio through an interactive
resize. The real, scoped "phone monitor" primitive - matches any VM/
emulator/scrcpy window by app_id, nothing Android- or VM-specific here.
- general.phone_mode (WindowManager::phone_mode): a new window defaults
to maximized instead of floating/tiled small, unless a rule explicitly
floats it or sets maximized - the one placement default a phone-shaped
screen actually needs. Exposed read-only via IPC so a shell panel can
adapt its own chrome to the same signal.
- input_pin.rs: the core half of pinning a virtual pointer to a specific
window (Multi-cursor Phase 2) - a backend-agnostic request queue,
same cross-boundary shape output_position_requests/lock_requested
already use, since core has no real Wayland protocol object to reach
into itself.
Full workspace test suite covers all of the above (aspect-ratio resize
math for every edge case, phone-mode default-vs-rule-override behavior,
the pin-input request queue, the monitor-picking fixes).
Diffstat (limited to 'crates/core/src/manager/windows.rs')
| -rw-r--r-- | crates/core/src/manager/windows.rs | 131 |
1 files changed, 98 insertions, 33 deletions
diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs index 7e2046d..ff6c4c8 100644 --- a/crates/core/src/manager/windows.rs +++ b/crates/core/src/manager/windows.rs @@ -28,6 +28,14 @@ impl WindowManager { window.corner_radius = self.theme.default_corner_radius; window.decorated = self.theme.default_decorated && !likely_draws_own_titlebar(&window.app_id); let actions = self.rules.iter().find(|r| r.matcher.matches(&window)).map(|r| r.actions.clone()); + log::warn!( + "DECO-DIAG add_window id={id} app_id={:?} title={:?} rules_count={} actions_found={} decorated_before_actions={}", + window.app_id, + window.title, + self.rules.len(), + actions.is_some(), + window.decorated + ); // See `Window::rules_applied`'s doc comment: a native Wayland window // still has empty title/app_id at this point, so a real (if // inconclusive) match attempt needs to wait for `reapply_rules_if_pending`. @@ -60,52 +68,82 @@ impl WindowManager { if let Some(margin) = a.resize_margin { window.resize_margin = Some(margin); } + if let Some(ratio) = a.aspect_ratio { + window.aspect_ratio = Some(ratio); + } } - // A remembered size (`remembered_sizes`' own doc comment) wins over - // whatever fixed default a backend hardcoded into `window.geometry` - // before calling this - but a rule's explicit `geometry` action - // below still wins over *this*, since that's a deliberate per-app - // override, more specific than "whatever I last resized this app - // to". Clamped to the same minimums a live resize itself can never - // go below, so a corrupted/stale entry can't hand a new window a - // degenerate size. + // A remembered size (`remembered_geometry`'s own doc comment) wins + // over whatever fixed default a backend hardcoded into `window. + // geometry` before calling this - but a rule's explicit `geometry` + // action below still wins over *this*, since that's a deliberate + // per-app override, more specific than "whatever I last left this + // app at". Clamped to the same minimums a live resize itself can + // never go below, so a corrupted/stale entry can't hand a new + // window a degenerate size. Position (see `remembered_position` + // below, computed after `target_monitor` exists) is a separate + // question from size: a size is always safe to reapply verbatim, + // but a *position* needs checking against the monitors that + // actually exist right now before it's safe to reuse. + let mut remembered_position: Option<(i32, i32)> = None; if !window.app_id.is_empty() { - if let Some(&(w, h)) = self.remembered_sizes.get(&window.app_id) { + if let Some((x, y, w, h)) = self.remembered_geometry.get(&window.app_id).copied() { window.geometry.width = w.max(MIN_WINDOW_WIDTH); window.geometry.height = h.max(MIN_WINDOW_HEIGHT); + remembered_position = Some((x, y)); } } // Every new window used to land on the *primary* monitor // unconditionally, regardless of which monitor the user was // actually working on - reported live as "why do all windows only // open on the first monitor" once a second, non-primary monitor - // was actually in use. Placing on the *focused* window's monitor - // instead matches every mainstream desktop's own convention (a new - // window opens where you're currently working, not wherever - // "primary" happens to be), and needs no new state: `self.focused` - // already exists for exactly this kind of "what's the user looking - // at right now" question. + // was actually in use. // - // Falling all the way back to the primary monitor whenever nothing - // is focused was still wrong for a second, later-reported case: - // nothing focused *on the monitor the user is actually at* - an - // empty desktop there, or the last-focused window happening to sit - // on a different monitor than the one just clicked/hovered before - // launching something new - landed the new window on primary - // regardless of which monitor was genuinely in use. `pointer_ - // monitor` (see its own doc comment) is a second, better fallback - // for exactly that gap, checked before giving up to primary - // entirely - which stays the last resort for the one case neither - // signal can answer, a fresh session's very first window before any - // pointer motion has been reported at all. + // This used to check the *focused* window's monitor first and the + // pointer only as a fallback, on the reasoning that focus is the + // stronger "where is the user working" signal. Reported live as + // still wrong the same way: launching an app while the pointer sat + // on a second monitor's bare desktop (nothing focused *there* -- + // the panel/launcher that started it isn't a core-tracked window, + // and the last *focused* window was still whatever had been open on + // the first monitor) put the new window back on the first monitor + // regardless. `self.focused` only changes when a real window is + // focused, so it goes stale the moment the user's attention moves + // to empty desktop, a panel, or a dock - exactly the case that + // matters here. The pointer's own current monitor has no such + // staleness: `set_pointer_monitor` is updated on every motion + // event, so it always reflects where the user physically is right + // now. Checked first for that reason, matching this compositor's + // own mouse-first design (see `docs/DEFAULTS.md`) and the same + // "active output follows the cursor" default every comparable + // dynamic/floating compositor (Hyprland, Mutter/GNOME, sway's + // `focus_follows_mouse`) ships. Falling back to the focused + // window's monitor when the pointer's own is unknown, then all the + // way back to primary as the last resort for the one case neither + // signal can answer - a fresh session's very first window, before + // any pointer motion has been reported at all. let target_monitor = self - .focused - .and_then(|id| self.windows.get(&id)) - .and_then(|w| self.monitors.iter().find(|m| m.id == w.monitor)) - .or_else(|| self.pointer_monitor.and_then(|id| self.monitors.iter().find(|m| m.id == id))) + .pointer_monitor + .and_then(|id| self.monitors.iter().find(|m| m.id == id)) + .or_else(|| self.focused.and_then(|id| self.windows.get(&id)).and_then(|w| self.monitors.iter().find(|m| m.id == w.monitor))) .or_else(|| self.primary_monitor()); - if let Some(monitor) = target_monitor { + // A remembered position (see just above) wins over both the + // pointer/focus-based `target_monitor` heuristic and smart + // placement - real desktop convention (Windows, macOS) is "reopen + // exactly where I left this app", not "wherever the mouse happens + // to be right now", *provided* that position still lands on a + // monitor that actually exists this run - a laptop undocked since + // the position was saved, say, must not place a window off in + // space on a monitor that's no longer there. Checked against every + // current monitor's own *full* geometry (not the exclusive-zone- + // shrunk usable one): a remembered position under where a bar now + // sits is still "a real monitor, just partly covered", not invalid. + let remembered_monitor = remembered_position.and_then(|(x, y)| self.monitors.iter().find(|m| m.full_geometry.contains_point(x, y))); + if let (Some((x, y)), Some(monitor)) = (remembered_position, remembered_monitor) { + window.monitor = monitor.id; + window.geometry.x = x; + window.geometry.y = y; + } else if let Some(monitor) = target_monitor { window.monitor = monitor.id; let layout_name = self.workspace(workspace).map(|w| w.layout.clone()).unwrap_or_default(); if layout_name != "tiling" { @@ -117,7 +155,18 @@ impl WindowManager { if let Some(geometry) = actions.as_ref().and_then(|a| a.geometry) { window.geometry = geometry; } - let maximize = actions.as_ref().and_then(|a| a.maximized).unwrap_or(false); + // `general.phone_mode`'s own real default (see its doc comment on + // `WindowManager` for the full "optional phone mode" reasoning): + // every ordinary new window opens maximized, since a phone-shaped + // screen has no real room for multiple windows side by side. A + // rule's own explicit `maximized` action (`Some(true)` or + // `Some(false)`) always wins regardless - this only supplies the + // *default* `None` would otherwise fall back to. `window.floating` + // already reflects any rule's own `floating` action by this point + // (applied above) - a floating window (a picture-in-picture + // popup, a dialog) is floating *because* it's meant to stay small, + // so phone mode leaves it alone rather than maximizing it anyway. + let maximize = actions.as_ref().and_then(|a| a.maximized).unwrap_or(self.phone_mode && !window.floating); self.windows.insert(id, window); self.order.push(id); @@ -154,9 +203,21 @@ impl WindowManager { pub fn reapply_rules_if_pending(&mut self, id: WindowId) -> bool { let Some(window) = self.windows.get(&id) else { return false }; if window.rules_applied || (window.title.is_empty() && window.app_id.is_empty()) { + log::warn!( + "DECO-DIAG reapply_rules_if_pending id={id} SKIPPED rules_applied={} app_id={:?} title={:?}", + window.rules_applied, + window.app_id, + window.title + ); return false; } let actions = self.rules.iter().find(|r| r.matcher.matches(window)).map(|r| r.actions.clone()); + log::warn!( + "DECO-DIAG reapply_rules_if_pending id={id} app_id={:?} actions_found={} actions_decorated={:?}", + window.app_id, + actions.is_some(), + actions.as_ref().and_then(|a| a.decorated) + ); let Some(window) = self.windows.get_mut(&id) else { return false }; window.rules_applied = true; // `add_window`'s matching fallback only ever sees this once @@ -176,6 +237,7 @@ impl WindowManager { if let Some(decorated) = actions.decorated { window.decorated = decorated; } + log::warn!("DECO-DIAG reapply_rules_if_pending id={id} FINAL window.decorated={}", window.decorated); if let Some(color) = actions.border_color { window.border_color = color; } @@ -191,6 +253,9 @@ impl WindowManager { if let Some(margin) = actions.resize_margin { window.resize_margin = Some(margin); } + if let Some(ratio) = actions.aspect_ratio { + window.aspect_ratio = Some(ratio); + } if let Some(geometry) = actions.geometry { window.geometry = geometry; } |