srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/core/src/manager/dragresize.rs8
-rw-r--r--crates/core/src/manager/tests.rs33
-rw-r--r--crates/core/src/manager/windows.rs19
-rw-r--r--crates/srdwm/src/main.rs41
-rw-r--r--crates/wayland/src/state/geometry.rs6
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 {