srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/window_memory.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-09-10 11:24:00 +0200
committersrdusr <[email protected]>2025-09-10 11:24:00 +0200
commit5ee627928d0cb017e59d796c71793c35d5b65d33 (patch)
tree3285a7df82a04b178fb41fae220d60fe1dd38bd7 /crates/wayland/src/window_memory.rs
parent4a55182ce17fd4474f8be1e8819a41b7ccd11045 (diff)
downloadsrdwm-5ee627928d0cb017e59d796c71793c35d5b65d33.tar.gz
srdwm-5ee627928d0cb017e59d796c71793c35d5b65d33.zip
Wayland backend: desktop icons v2, menu rebuild, layer-shell/scale fixes,
XWayland stability, GPU rendering, and multi-cursor Phase 2 The bulk of a multi-session shift's real work landed in crates/wayland. Full root-cause/verification narrative for every item below lives in docs/TODO.md (each has its own dated entry); this is the summary: Desktop shell: - Real desktop icons v2 (state/desktop_icons.rs, desktop_icons.rs): fixed origin-baked-before-the-bar-connects, fixed-icon sort order, and a proper Rename/Delete-to-Trash menu (window_memory.rs backs the rename-persistence side). Rubber-band marquee multi-select. - icon_theme.rs: real freedesktop icon-theme lookup (inherits chain, hicolor fallback) rendering actual theme SVGs via resvg/tiny-skia, replacing the hand-drawn placeholder glyphs. - Context/desktop menus (decoration.rs, desktop_menu.rs, state/menu.rs) rebuilt to match the project's own AGS panel styling: rounded floating panel, tinted-fill row highlight, real separators, a much fuller titlebar window-menu action set. Layer-shell / multi-monitor: - Layer-shell hit-testing and render positioning (input/pointer.rs, udev/render.rs's element placement) now correctly convert LayerMap's logical geometry into physical pixels on a fractionally-scaled output - root cause of a bottom-anchored dock being unclickable and unpainted while a top-anchored bar on the same output worked. udev/outputs.rs's relayout_outputs gained the same physical/logical split for cross-output positioning, now backed by a real unit test (next_logical_x) built from the original measured incident numbers. - state/geometry.rs: a window's border/decoration no longer briefly clips when moved between differently-scaled monitors mid-drag. XWayland / stability: - xwayland.rs, udev/session.rs, udev/platform.rs: fixed a 100%- reproducible cold-start XKEYBOARD crash-loop (XWayland's own stdin inherited a real, already-owned VT; env passthrough and idle-callback spawn timing were both real, independent gaps) that had silently taken down all X11-app support and the global-menu registrar every session. - state/toplevel.rs, state/lifecycle.rs: XWayland dialog detection via WM_TRANSIENT_FOR, not just a native xdg_toplevel parent. Rendering: - udev/render.rs, decoration.rs: real GPU (GBM+EGL+DrmCompositor) window-content and cursor rendering on the udev backend, falling back to the untouched Pixman path automatically on any init failure. - decoration/tests.rs, state/mod.rs: rounded-corner/border fixes for interactive resize lag and cross-monitor moves. Multi-cursor Phase 2 (virtual_pointer.rs, new; state/mod.rs, udev/ platform.rs, winit/nested_platform.rs): pins a zwlr_virtual_pointer_ unstable_v1 object to a specific window, bypassing the shared seat/ focus/pointer_pos path entirely via hand-rolled wl_pointer.enter/motion/ button/frame/leave against every WlPointer the target client has bound (PointerHandle::client_pointers). Lets an agent operate one window while a human uses another, genuinely simultaneously, with zero client cooperation and no second wl_seat (confirmed a dead end: real clients only ever bind the first seat advertised). Full workspace build/test/clippy clean.
Diffstat (limited to 'crates/wayland/src/window_memory.rs')
-rw-r--r--crates/wayland/src/window_memory.rs124
1 files changed, 124 insertions, 0 deletions
diff --git a/crates/wayland/src/window_memory.rs b/crates/wayland/src/window_memory.rs
new file mode 100644
index 0000000..a92c57c
--- /dev/null
+++ b/crates/wayland/src/window_memory.rs
@@ -0,0 +1,124 @@
+//! Persists `WindowManager::remembered_geometry` (per-`app_id` last
+//! floating position+size) across a restart - see that field's own doc
+//! comment in `srdwm_core` for what it is and why it's read at window-map
+//! time. Same load/save-at-the-platform-layer split, same JSON-file-under-
+//! `$XDG_STATE_HOME/srd` shape, and same atomic tmp-then-rename write this
+//! project already established twice (`monitor_layout.rs`, `desktop_icons_
+//! state.rs`) for exactly this kind of small, rarely-written, must-survive-
+//! a-crash state - deliberately not sharing code with either, matching
+//! this codebase's own "a few duplicated lines beats a shared abstraction
+//! for three near-identical small stores" precedent.
+
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
+pub(crate) struct PersistedGeometry {
+ pub(crate) x: i32,
+ pub(crate) y: i32,
+ pub(crate) width: u32,
+ pub(crate) height: u32,
+}
+
+#[derive(Serialize, Deserialize, Default)]
+struct PersistedWindowMemory {
+ /// Keyed by `app_id` - the same identifier `remembered_geometry`
+ /// itself is keyed by, and the one thing guaranteed stable across a
+ /// restart that a per-session `WindowId` is not.
+ apps: HashMap<String, PersistedGeometry>,
+}
+
+fn state_dir() -> PathBuf {
+ if let Ok(p) = std::env::var("SRDWM_STATE_PATH") {
+ return PathBuf::from(p);
+ }
+ if let Ok(xdg) = std::env::var("XDG_STATE_HOME") {
+ return PathBuf::from(xdg).join("srd");
+ }
+ if let Ok(home) = std::env::var("HOME") {
+ return PathBuf::from(home).join(".local/state/srd");
+ }
+ PathBuf::from("state/srd")
+}
+
+fn memory_path() -> PathBuf {
+ state_dir().join("window-memory.json")
+}
+
+/// Every remembered app's geometry, by `app_id`. Empty (not an error) if
+/// the file doesn't exist yet or is present but unreadable/corrupt - a
+/// bad state file degrades to "nothing remembered yet", not a startup
+/// failure.
+pub(crate) fn load() -> HashMap<String, PersistedGeometry> {
+ let path = memory_path();
+ let Ok(bytes) = std::fs::read(&path) else { return HashMap::new() };
+ match serde_json::from_slice::<PersistedWindowMemory>(&bytes) {
+ Ok(memory) => memory.apps,
+ Err(e) => {
+ log::warn!("window_memory: couldn't parse {path:?} ({e}); starting with nothing remembered");
+ HashMap::new()
+ }
+ }
+}
+
+/// Overwrites the whole persisted table from `entries` - called after
+/// every drag/resize-end (see `input/pointer.rs`'s call site), which are
+/// rare, real user actions, not a per-frame event, so writing the whole
+/// small file each time costs nothing and needs no separate dirty-tracking
+/// story (the same reasoning `monitor_layout::save_output` and `desktop_
+/// icons_state`'s own saver already settled on for the identical shape of
+/// problem).
+pub(crate) fn save_all<'a>(entries: impl Iterator<Item = (&'a str, (i32, i32, u32, u32))>) {
+ let apps: HashMap<String, PersistedGeometry> =
+ entries.map(|(app_id, (x, y, width, height))| (app_id.to_string(), PersistedGeometry { x, y, width, height })).collect();
+ let memory = PersistedWindowMemory { apps };
+ let dir = state_dir();
+ if let Err(e) = std::fs::create_dir_all(&dir) {
+ log::warn!("window_memory: couldn't create {dir:?} ({e}); this session's window positions/sizes won't survive a restart");
+ return;
+ }
+ let Ok(bytes) = serde_json::to_vec_pretty(&memory) else { return };
+ let path = memory_path();
+ let tmp = path.with_extension("json.tmp");
+ if let Err(e) = std::fs::write(&tmp, &bytes) {
+ log::warn!("window_memory: couldn't write {tmp:?} ({e}); this session's window positions/sizes won't survive a restart");
+ return;
+ }
+ if let Err(e) = std::fs::rename(&tmp, &path) {
+ log::warn!("window_memory: couldn't rename {tmp:?} to {path:?} ({e}); this session's window positions/sizes won't survive a restart");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // Only the pure JSON round-trip is exercised here - `state_dir()`/
+ // `load()`/`save_all()` all touch real environment variables and the
+ // filesystem, which parallel `cargo test` execution can't safely share
+ // - same reasoning `monitor_layout.rs`'s own tests give for staying
+ // off real env vars.
+ #[test]
+ fn a_persisted_table_survives_a_json_round_trip() {
+ let mut apps = HashMap::new();
+ apps.insert("alacritty".to_string(), PersistedGeometry { x: 100, y: 100, width: 800, height: 600 });
+ apps.insert("firefox".to_string(), PersistedGeometry { x: -500, y: 0, width: 1280, height: 900 });
+ let memory = PersistedWindowMemory { apps };
+ let bytes = serde_json::to_vec(&memory).unwrap();
+ let parsed: PersistedWindowMemory = serde_json::from_slice(&bytes).unwrap();
+ assert_eq!(parsed.apps.get("alacritty"), Some(&PersistedGeometry { x: 100, y: 100, width: 800, height: 600 }));
+ assert_eq!(parsed.apps.get("firefox"), Some(&PersistedGeometry { x: -500, y: 0, width: 1280, height: 900 }));
+ }
+
+ #[test]
+ fn corrupt_json_falls_back_to_an_empty_table_not_an_error() {
+ let result = serde_json::from_slice::<PersistedWindowMemory>(b"not valid json");
+ assert!(result.is_err(), "sanity: this fixture must actually fail to parse");
+ // `load()` itself can't be called here (touches the real
+ // filesystem/env) - this locks in the *shape* of the fallback
+ // `load()` relies on: a parse error, not a panic, is what lets it
+ // degrade to `HashMap::new()` instead of taking the compositor down.
+ }
+}