diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/core/src/manager/dragresize.rs | 8 | ||||
| -rw-r--r-- | crates/core/src/manager/tests.rs | 33 | ||||
| -rw-r--r-- | crates/core/src/manager/windows.rs | 19 | ||||
| -rw-r--r-- | crates/srdwm/src/main.rs | 41 | ||||
| -rw-r--r-- | crates/wayland/src/state/geometry.rs | 6 |
5 files changed, 106 insertions, 1 deletions
diff --git a/crates/core/src/manager/dragresize.rs b/crates/core/src/manager/dragresize.rs index 906e5db..16aa539 100644 --- a/crates/core/src/manager/dragresize.rs +++ b/crates/core/src/manager/dragresize.rs @@ -387,6 +387,14 @@ impl WindowManager { /// Every remembered `app_id` and its geometry - what `window_memory.rs` /// iterates to persist the full table (e.g. on a clean shutdown), not /// just whatever changed most recently. + /// This app's remembered geometry, if any. `None` means nothing has + /// been recorded for it - which is what a window that closed while its + /// size was still a placeholder deliberately leaves behind, so its next + /// launch gets to pick its own size again. + pub fn remembered_geometry_for(&self, app_id: &str) -> Option<(i32, i32, u32, u32)> { + self.remembered_geometry.get(app_id).copied() + } + pub fn all_remembered_geometry(&self) -> impl Iterator<Item = (&str, (i32, i32, u32, u32))> { self.remembered_geometry.iter().map(|(k, &v)| (k.as_str(), v)) } diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs index 152c71d..fdf0517 100644 --- a/crates/core/src/manager/tests.rs +++ b/crates/core/src/manager/tests.rs @@ -915,6 +915,39 @@ } #[test] + fn a_size_the_client_never_chose_is_not_remembered() { + // The poisoning loop: remember a placeholder once and every future + // launch is forced to it, which looks like "every window spawns the + // same shape". + let mut wm = wm_with_monitor(); + let id = wm.alloc_window_id(); + let mut w = Window::new(id, "w"); + w.app_id = "someapp".into(); + wm.add_window(w); + assert!(wm.window(id).unwrap().size_is_provisional, "no remembered size, so the guess is provisional"); + wm.remove_window(id); + assert!(wm.remembered_geometry_for("someapp").is_none(), "a guess must never be remembered"); + } + + #[test] + fn a_size_the_client_did_choose_is_remembered() { + let mut wm = wm_with_monitor(); + let id = wm.alloc_window_id(); + let mut w = Window::new(id, "w"); + w.app_id = "someapp".into(); + wm.add_window(w); + // What the backend does once the client commits a real buffer. + if let Some(w) = wm.window_mut(id) { + w.size_is_provisional = false; + w.geometry.width = 1389; + w.geometry.height = 933; + } + wm.remove_window(id); + let remembered = wm.remembered_geometry_for("someapp").expect("a real choice must be remembered"); + assert_eq!((remembered.2, remembered.3), (1389, 933)); + } + + #[test] fn a_dialog_opens_centered_not_cascaded_into_the_corner() { let mut wm = wm_with_monitor(); let a = wm.alloc_window_id(); diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs index 0186679..1e8a6cb 100644 --- a/crates/core/src/manager/windows.rs +++ b/crates/core/src/manager/windows.rs @@ -369,8 +369,25 @@ impl WindowManager { // simply never consulted again on the read side once `layout_name // == "tiling"`, so remembering it anyway is harmless, not wasted // work worth a special case. + // + // `size_is_provisional` still set means the client never actually + // chose a size: the window closed while still carrying the + // backend's placeholder guess. Remembering that guess poisons this + // table permanently, and does so in a way that hides itself: + // + // 1. a window closes early, the placeholder is remembered + // 2. next launch finds a remembered size, so it is NOT provisional + // 3. the client is therefore forced to the placeholder instead of + // being asked to pick, and looks identical to every other + // poisoned app + // 4. on close the same placeholder is written back + // + // Reported as windows "spawning in squares" - every app coming out + // the same shape whatever it is. Five of eleven entries in the live + // store had been captured this way, including Firefox, whose real + // remembered size earlier the same day had been 1389x933. if let Some(w) = &window { - if !w.app_id.is_empty() { + if !w.app_id.is_empty() && !w.size_is_provisional { self.remembered_geometry.insert(w.app_id.clone(), (w.geometry.x, w.geometry.y, w.geometry.width, w.geometry.height)); } } diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index 9772e65..3f4fe34 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -104,6 +104,40 @@ const RELOAD_COMBO_LITERAL: &str = "Mod4+Ctrl+r"; /// `general.config_reload_on_write` is on. See `config_mtime`. const CONFIG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); +/// Publishes srdwm's own titlebar button placement to the toolkits that +/// draw their own decorations, so one setting governs every window. +/// +/// GTK never negotiates decoration with the compositor - it creates no +/// `xdg_toplevel_decoration` object at all, measured directly - so the +/// protocol cannot reach its buttons. What GTK *does* read is the desktop's +/// own button-layout preference: GTK4 through `xdg-desktop-portal` +/// (`org.freedesktop.portal.Settings`, key +/// `org.gnome.desktop.wm.preferences` `button-layout`), GTK3 through the +/// `gtk-decoration-layout` setting. Both resolve from the same GSettings +/// key on an ordinary system. +/// +/// So this writes that key to match `button_side`. It is exactly what KDE +/// does - `kde-gtk-config` exists for no other reason than to keep GTK's +/// own setting in step with KWin's decoration - and it is what makes +/// "set the decoration side once and every application follows" true rather +/// than only true of srdwm's own titlebars. +/// +/// Best-effort by design: `gsettings` missing, or no dconf to write to, is +/// not an error worth interrupting startup for. The compositor's own +/// titlebars are already correct either way; this only brings the +/// self-decorating clients into line. +fn publish_gtk_button_layout(wm: &Rc<RefCell<WindowManager>>) { + let layout = if wm.borrow().theme.buttons_left { "close,minimize,maximize:" } else { ":minimize,maximize,close" }; + match std::process::Command::new("gsettings") + .args(["set", "org.gnome.desktop.wm.preferences", "button-layout", layout]) + .status() + { + Ok(status) if status.success() => log::info!("published GTK button-layout {layout:?}"), + Ok(status) => log::debug!("gsettings exited {status} publishing button-layout; self-decorating clients keep their own"), + Err(e) => log::debug!("gsettings unavailable ({e}); self-decorating clients keep their own button layout"), + } +} + /// Copies the loaded config's key bindings into the `WindowManager`, where /// the IPC layer can serve them (`srd keybindings`). /// @@ -720,6 +754,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { publish_keybindings(&engine, &wm); apply_workspace_count(&engine, &wm); apply_general_settings(&engine, &wm); + // After `apply_general_settings`, which is what copies the config's + // `button_side` into the theme - publishing before it would broadcast + // the built-in default rather than the user's choice. + publish_gtk_button_layout(&wm); apply_default_layout(&engine, &wm); let running = engine.running_flag(); // `general.config_reload_on_write` - on by default. A programmable @@ -829,6 +867,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { } apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); + publish_gtk_button_layout(&wm); // After `apply_general_settings`, which rebuilds the // theme from the config file - see // `WindowManager::live_settings` for why a hand-made @@ -852,6 +891,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { } apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); + publish_gtk_button_layout(&wm); srdwm_platform::replay_live_settings(&wm); // After the reload, so a handler edited in the config since // startup is the one that runs. @@ -870,6 +910,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { } apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); + publish_gtk_button_layout(&wm); srdwm_platform::replay_live_settings(&wm); } else if !engine.dispatch_keybinding(&combo) { log::debug!("no binding for '{combo}'"); diff --git a/crates/wayland/src/state/geometry.rs b/crates/wayland/src/state/geometry.rs index b8629bc..1910334 100644 --- a/crates/wayland/src/state/geometry.rs +++ b/crates/wayland/src/state/geometry.rs @@ -556,6 +556,12 @@ impl CompState { let width = ((content.size.w as f64 * scale).round() as u32).max(srdwm_core::placement::MIN_WINDOW_WIDTH); let height = ((content.size.h as f64 * scale).round() as u32).max(srdwm_core::placement::MIN_WINDOW_HEIGHT) + band; let Some(w) = wm.window_mut(id) else { return }; + // The client has now made a real choice, so this size is no longer + // a guess. Clearing the core flag is what lets `remove_window` + // remember it: that path deliberately refuses to remember a size + // that was never chosen, and without this every window would look + // provisional forever and nothing would ever be remembered again. + w.size_is_provisional = false; w.geometry.width = width; w.geometry.height = height; if let Some(monitor) = monitor_geometry { |