From 9e88d3e3f86a1248b72fdb9bab59d9936a3323a6 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:28:00 +0200 Subject: Let a new window pick its own size instead of forcing a guessed placeholder Root-caused "windows spawn small and square, not remembering placement or size": new_managed_window hardcoded a fresh toplevel's geometry to 800x632 before the client had said anything about its own preferred size, and sync_geometry forced that guess onto the client's very first xdg_toplevel::configure unconditionally. Per xdg-shell, size: None on that first configure is how every mainstream compositor lets a client pick its own natural size instead; this one never did, so every app converged on the same placeholder rectangle regardless of what it would have chosen. Window::size_is_provisional marks a size that really was just the guess (not a remembered geometry, a rule's explicit geometry action, or a maximize/phone-mode fill, none of which are guesses). sync_geometry sends size: None for such a window's first configure; a new adopt_provisional_size, called from the commit handler, adopts the client's own real first size into Window::geometry the moment it commits one, clamping only position so a bigger-than-guessed window can't hang off its monitor's edge. Live-verified in a nested compositor: a zenity dialog now renders at its own compact natural size instead of being stretched to the old guess. --- crates/core/src/manager/mod.rs | 2 ++ crates/core/src/manager/tests.rs | 45 ++++++++++++++++++++++++++++++++++++++ crates/core/src/manager/windows.rs | 12 ++++++++++ crates/core/src/window.rs | 20 +++++++++++++++++ 4 files changed, 79 insertions(+) (limited to 'crates/core/src') diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs index e4ac1c2..d449486 100644 --- a/crates/core/src/manager/mod.rs +++ b/crates/core/src/manager/mod.rs @@ -3,6 +3,8 @@ use crate::layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig}; use crate::monitor::{DisabledMonitor, Monitor, MonitorId, MonitorSplit}; use crate::placement::{PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH}; use crate::rules::WindowRule; +#[cfg(test)] +use crate::rules::{WindowMatch, WindowRuleActions}; use crate::lock_config::LockConfig; use crate::theme::ThemeConfig; use crate::window::{likely_draws_own_titlebar, ResizeEdge, TitlebarHit, Window, WindowId, RESIZE_MARGIN}; diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs index 860af89..f703f25 100644 --- a/crates/core/src/manager/tests.rs +++ b/crates/core/src/manager/tests.rs @@ -25,6 +25,51 @@ assert_eq!(placed.x, wm.placement.cascade_offset); } + #[test] + fn a_smart_placed_window_is_marked_size_provisional() { + let mut wm = wm_with_monitor(); + let id = wm.alloc_window_id(); + let w = Window::new(id, "first"); + wm.add_window(w); + // No remembered geometry, no rule, not maximized - the size that + // just got smart-placed is nothing but `Window::new`'s own default + // guess, so a backend should be free to let the client override it. + assert!(wm.window(id).unwrap().size_is_provisional); + } + + #[test] + fn a_remembered_geometry_is_never_provisional() { + let mut wm = wm_with_monitor(); + wm.set_remembered_geometry("some-app".to_string(), (100, 100, 900, 700)); + let id = wm.alloc_window_id(); + let mut w = Window::new(id, "second"); + w.app_id = "some-app".to_string(); + wm.add_window(w); + let win = wm.window(id).unwrap(); + assert_eq!((win.geometry.width, win.geometry.height), (900, 700)); + assert!(!win.size_is_provisional, "a deliberately remembered size must never be second-guessed by the client's own default"); + } + + #[test] + fn a_rules_explicit_geometry_is_never_provisional() { + let mut wm = wm_with_monitor(); + wm.rules.push(WindowRule { matcher: WindowMatch { class: Some("ruled-app".to_string()), ..Default::default() }, actions: WindowRuleActions { geometry: Some(Rect::new(10, 10, 500, 400)), ..Default::default() } }); + let id = wm.alloc_window_id(); + let mut w = Window::new(id, "third"); + w.app_id = "ruled-app".to_string(); + wm.add_window(w); + assert!(!wm.window(id).unwrap().size_is_provisional, "a rule's own explicit geometry is a deliberate choice, not a guess"); + } + + #[test] + fn phone_mode_maximize_is_never_provisional() { + let mut wm = wm_with_monitor(); + wm.phone_mode = true; + let id = wm.alloc_window_id(); + wm.add_window(Window::new(id, "fourth")); + assert!(!wm.window(id).unwrap().size_is_provisional, "a deliberate full-monitor fill is not a guess needing a client override"); + } + #[test] fn add_window_picks_up_the_configured_default_decoration_mode() { let mut wm = wm_with_monitor(); diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs index 40b4c17..f29d19a 100644 --- a/crates/core/src/manager/windows.rs +++ b/crates/core/src/manager/windows.rs @@ -151,10 +151,17 @@ impl WindowManager { let step = self.next_cascade_step.get(); self.next_cascade_step.set(step.wrapping_add(1)); window.geometry = SmartPlacement::place(monitor, &existing, size, &self.placement, step); + // See `Window::size_is_provisional`'s own doc comment: + // `size` above is whatever a backend hardcoded before this + // window's real content was known, not a genuine + // preference - reset below if a rule or maximize goes on + // to give this window a real, deliberate size instead. + window.size_is_provisional = true; } } if let Some(geometry) = actions.as_ref().and_then(|a| a.geometry) { window.geometry = geometry; + window.size_is_provisional = false; } // `general.phone_mode`'s own real default (see its doc comment on // `WindowManager` for the full "optional phone mode" reasoning): @@ -176,6 +183,11 @@ impl WindowManager { self.restack_pinned(); if maximize { self.toggle_maximize(id); + // Deliberately full-monitor, not a guess - see `Window:: + // size_is_provisional`'s own doc comment. + if let Some(w) = self.windows.get_mut(&id) { + w.size_is_provisional = false; + } } id } diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index 21faeff..cc841b1 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -244,6 +244,25 @@ pub struct Window { /// This window's global-menu D-Bus address, if the client has exported /// one. See [`GlobalMenu`]'s own doc comment. pub global_menu: Option, + /// Set by `WindowManager::add_window` when `geometry`'s size just came + /// from `SmartPlacement`'s own guessed default (`Window::new`'s + /// `640x480`, or whatever a backend hardcodes before a client has said + /// anything about its own preferred size) rather than a deliberate + /// decision - a remembered size, a rule's explicit `geometry` action, + /// or a maximize/phone-mode fill. `false` for all three of those, since + /// there is nothing provisional about a size someone actually chose. + /// + /// A backend reads this once, right after `add_window` returns, to + /// decide whether the *client's own* first real committed size should + /// be allowed to win once it arrives (see `crates/wayland/src/state/ + /// geometry.rs`'s own use of this) - reported live as "windows always + /// spawn small and square, not remembering placement or size": every + /// new toplevel was forced, via its very first `xdg_toplevel.configure`, + /// into this guessed placeholder size regardless of what the + /// application itself would have preferred, which is why every app + /// converged on the same generic footprint instead of its own natural + /// one. + pub size_is_provisional: bool, } impl Window { @@ -281,6 +300,7 @@ impl Window { rules_applied: false, anim_from: None, global_menu: None, + size_is_provisional: false, } } } -- cgit v1.2.3