git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitd212f61f502b5e58dade4ef003f2ee069c7a2165
parentb2305d0399
authorLucas Galante <[email protected]>
date2026-06-25 12:56
Restore focused window state on startup and prevent focus stealing race condition

 src/server/window.rs         | 32 +++++++++++++++++++++++++-------
 src/server/window_manager.rs | 18 +++++++++++++++++-
 2 files changed, 42 insertions(+), 8 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 7368a58..421a796 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -210,6 +210,7 @@ pub struct Window {
     pub mode_locked: bool,
     pub is_new: bool,
     pub restored: bool,
+    pub restored_focused: bool,
     pub closed: bool,
     pub has_parent: bool,
     pub minimized: bool,
@@ -366,6 +367,7 @@ impl Window {
             mode_locked: false,
             is_new: true,
             restored: false,
+            restored_focused: false,
             closed: false,
             has_parent: false,
             minimized: false,
@@ -664,6 +666,7 @@ impl Window {
             }
 
             self.restored = true;
+            self.restored_focused = saved.focused;
         }
     }
 
@@ -693,13 +696,28 @@ impl Window {
         if is_status_bar {
             self.tiling_mode = crate::tiling::TilingMode::Status;
         } else {
-            let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
-            let mut curr = (*seats).next;
-            while curr != seats {
-                let next = (*curr).next;
-                let seat = crate::container_of!(curr, crate::seat::Seat, link);
-                (*seat).focus(crate::seat::Focus::Window(self as *mut Window));
-                curr = next;
+            let mut should_focus = true;
+            if self.restored {
+                if self.restored_focused {
+                    (*self.server).wm.restored_focused_window_mapped = true;
+                    log::info!("[FocusRestore] Restored focused window mapped: {:?}", self.get_title());
+                } else {
+                    if (*self.server).wm.has_restored_focused_window && (*self.server).wm.restored_focused_window_mapped {
+                        log::info!("[FocusRestore] Blocking focus to non-focused restored window {:?} because restored focused window is already mapped", self.get_title());
+                        should_focus = false;
+                    }
+                }
+            }
+
+            if should_focus {
+                let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+                let mut curr = (*seats).next;
+                while curr != seats {
+                    let next = (*curr).next;
+                    let seat = crate::container_of!(curr, crate::seat::Seat, link);
+                    (*seat).focus(crate::seat::Focus::Window(self as *mut Window));
+                    curr = next;
+                }
             }
         }
 
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 2117bdb..3ff04b8 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -53,6 +53,8 @@ pub struct SavedWindowState {
     pub width: u32,
     pub height: u32,
     pub cmdline: String,
+    #[serde(default)]
+    pub focused: bool,
 }
 
 #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
@@ -101,6 +103,8 @@ pub struct WindowManager {
     pub target_desk_pan_x: Option<f64>,
     pub target_desk_pan_y: Option<f64>,
     pub animation_timer: *mut ffi::wl_event_source,
+    pub has_restored_focused_window: bool,
+    pub restored_focused_window_mapped: bool,
 }
 
 impl WindowManager {
@@ -153,6 +157,8 @@ impl WindowManager {
         self.shutting_down = false;
         self.layout = crate::config::Layout::default();
         self.output_scale = 1.0;
+        self.has_restored_focused_window = false;
+        self.restored_focused_window_mapped = false;
         self.mode_rules = Vec::new();
         self.keybinds = Vec::new();
         self.pointer_binds = Vec::new();
@@ -211,7 +217,13 @@ impl WindowManager {
                 self.desk_zoom = state.desk_zoom;
                 self.global_layout = state.global_layout;
                 self.restore_queue = state.windows;
-                log::info!("State loaded successfully. {} windows in restore queue.", self.restore_queue.len());
+                self.has_restored_focused_window = self.restore_queue.iter().any(|w| w.focused);
+                self.restored_focused_window_mapped = false;
+                log::info!(
+                    "State loaded successfully. {} windows in restore queue, has_restored_focused_window={}.",
+                    self.restore_queue.len(),
+                    self.has_restored_focused_window
+                );
             } else {
                 log::error!("Failed to parse state JSON from {}", path);
             }
@@ -230,6 +242,7 @@ impl WindowManager {
         };
         log::debug!("Saving state to {}", path_str);
         
+        let focused_win = self.focused_window();
         let mut saved_wins = Vec::new();
         for &w in self.windows.iter() {
             if w.is_null() || (*w).closed || matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init) {
@@ -256,6 +269,8 @@ impl WindowManager {
                 cmdline = app_id.clone();
             }
 
+            let is_focused = w == focused_win;
+
             saved_wins.push(SavedWindowState {
                 app_id,
                 title,
@@ -267,6 +282,7 @@ impl WindowManager {
                 width: (*w).box_geom.width as u32,
                 height: (*w).box_geom.height as u32,
                 cmdline,
+                focused: is_focused,
             });
         }