srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/windows.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-29 22:42:00 +0200
committersrdusr <[email protected]>2025-08-29 22:42:00 +0200
commitbe5c4efa6866b5d23e1e81ecfc3b8d276540860f (patch)
tree0732d942bf28d26cc6996549fadd1785e566a637 /crates/core/src/manager/windows.rs
parentbd7141718901c6f41511137e4b34a3bd9e2705b1 (diff)
downloadsrdwm-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.rs131
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;
}