git.lucas.co / cce-window-manager
window management library
git clone https://git.lucas.co/cce-window-manager.git

commita4850bd7110efde33ef93f25a63e326fb16e1c29
parent3a2a15cf76
authorLucas Galante <[email protected]>
date2026-07-28 14:02
feat: overlay-side command + next-visible-focus rule

OverlayLeft/Right join DefaultPolicy via Command::SetOverlayPosition
(new OverlaySide enum). focus::next_visible_focus is the refocus rule
behind close/minimize/unmap: the most recently focused eligible window,
else — a preserved mechanism quirk — the LAST eligible window in window
order, else nothing (focus clears). Eligibility stays the mechanism's
judgment, passed in on FocusCandidate.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/actions.rs |  8 +++++++-
 src/api.rs     |  9 +++++++++
 src/focus.rs   | 45 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/src/actions.rs b/src/actions.rs
index 0fec670..c29dfae 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -4,7 +4,7 @@
 // execute_action arms; an action this policy doesn't claim returns an empty
 // vec and the mechanism's remaining legacy arms handle it.
 
-use crate::api::{Action, ActionCtx, Command, Policy, WindowId};
+use crate::api::{Action, ActionCtx, Command, OverlaySide, Policy, WindowId};
 use crate::camera::{self, Camera};
 use crate::focus;
 use crate::pan;
@@ -35,6 +35,12 @@ impl Policy for DefaultPolicy {
             Action::Fullscreen => fullscreen(ctx),
             Action::ModeNext => mode_next(ctx),
             Action::ModeNextShared => mode_next_shared(ctx),
+            Action::OverlayLeft => {
+                vec![Command::SetOverlayPosition(OverlaySide::Left), Command::Relayout]
+            }
+            Action::OverlayRight => {
+                vec![Command::SetOverlayPosition(OverlaySide::Right), Command::Relayout]
+            }
             _ => Vec::new(),
         }
     }
diff --git a/src/api.rs b/src/api.rs
index 5d9ee0c..5111521 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -243,6 +243,13 @@ pub struct GridSpec {
     pub fade_mode: GridFadeMode,
 }
 
+/// Which side of the viewport the overlay column docks on.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum OverlaySide {
+    Left,
+    Right,
+}
+
 /// Shape of a cell's inward edge fade.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum GridFadeMode {
@@ -355,6 +362,8 @@ pub enum Command {
     /// Set a window's tiling mode; `locked` pins it against viewport-mode
     /// resolution.
     SetWindowMode { id: WindowId, mode: TilingMode, locked: bool },
+    /// Dock the overlay column on the given side.
+    SetOverlayPosition(OverlaySide),
     /// Reposition a window in virtual space.
     MoveWindow { id: WindowId, x: f64, y: f64 },
     /// Full re-arrange (the mechanism's `dirty_windowing`).
diff --git a/src/focus.rs b/src/focus.rs
index ccd87b2..e27f4c3 100644
--- a/src/focus.rs
+++ b/src/focus.rs
@@ -86,9 +86,54 @@ pub fn directional_focus(centers: &[(f64, f64)], focused: Option<usize>, dir: Di
     best.map(|(i, _)| i)
 }
 
+/// A candidate for the next-visible-focus rule. `eligible` is the
+/// mechanism's judgment (mapped, not minimized, not a status bar or
+/// background surface).
+#[derive(Debug, Clone, Copy)]
+pub struct FocusCandidate {
+    pub id: super::api::WindowId,
+    pub eligible: bool,
+}
+
+/// Which window takes focus when the focused one goes away (close,
+/// minimize, unmap): the most recently focused eligible window, else — a
+/// preserved mechanism quirk — the LAST eligible window in window order,
+/// else nothing (focus clears). `history` is most-recent-first.
+pub fn next_visible_focus(
+    history: &[FocusCandidate],
+    windows: &[FocusCandidate],
+) -> Option<super::api::WindowId> {
+    history
+        .iter()
+        .find(|c| c.eligible)
+        .or_else(|| windows.iter().filter(|c| c.eligible).last())
+        .map(|c| c.id)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::api::WindowId;
+    use crate::slotmap::Key;
+
+    fn fc(index: u32, eligible: bool) -> FocusCandidate {
+        FocusCandidate { id: WindowId(Key { generation: 0, index }), eligible }
+    }
+
+    #[test]
+    fn next_visible_prefers_history_then_last_in_window_order() {
+        let history = [fc(3, false), fc(7, true), fc(1, true)];
+        let windows = [fc(7, true), fc(3, false), fc(1, true)];
+        // Most recent eligible history entry wins.
+        assert_eq!(next_visible_focus(&history, &windows), Some(fc(7, true).id));
+        // No eligible history: LAST eligible window in window order.
+        let history = [fc(3, false)];
+        assert_eq!(next_visible_focus(&history, &windows), Some(fc(1, true).id));
+        // Nothing eligible anywhere: focus clears.
+        let none = [fc(1, false)];
+        assert_eq!(next_visible_focus(&history, &none), None);
+        assert_eq!(next_visible_focus(&[], &[]), None);
+    }
 
     // A 2x2-ish layout (y grows downward):
     //   0:(100,100)   1:(500,100)