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

commite03f878a1878948044657b27745a5df746fc45d7
parenta9f40ab927
authorLucas Galante <[email protected]>
date2026-06-19 03:18
Implement expose mode border highlight and pointer focus transitions

 src/server/cursor.rs         | 51 +++++++++++++++++++++++++
 src/server/window.rs         |  7 ++++
 src/server/window_manager.rs | 89 +++++++++++++++++++++++++++++++++-----------
 3 files changed, 126 insertions(+), 21 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 3a1b302..fdbe3a1 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -458,8 +458,17 @@ impl Cursor {
             }
 
             if let SceneNodeDataVal::Window(window) = result.data {
+                if (*server).wm.expose_active {
+                    let old_hovered = (*server).wm.expose_hovered_window;
+                    if old_hovered != window {
+                        (*server).wm.expose_hovered_window = window;
+                        (*server).wm.dirty_windowing();
+                    }
+                }
+
                 if (*window).tiling_mode != crate::tiling::TilingMode::Popup
                     && (*window).tiling_mode != crate::tiling::TilingMode::Fullscreen
+                    && !(*server).wm.expose_active
                 {
                     match get_border_zone(window, lx, ly) {
                         BorderZone::Resize(edges) => {
@@ -560,6 +569,48 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
             return;
         }
 
+        // --- EXPOSE MODE LEFT CLICK HANDLING ---
+        if (*(*seat).server).wm.expose_active && (*event).button == 0x110 {
+            let lx = cursor.x();
+            let ly = cursor.y();
+            let server = seat.server;
+            let mut clicked_win: *mut crate::window::Window = std::ptr::null_mut();
+            if let Some(result) = (*server).scene.at(lx, ly) {
+                if let SceneNodeDataVal::Window(window) = result.data {
+                    clicked_win = window;
+                }
+            }
+
+            // Exit expose mode
+            (*server).wm.expose_active = false;
+            log::info!("Expose mode exited via left click");
+
+            if !clicked_win.is_null() {
+                seat.focus(Focus::Window(clicked_win));
+                if !seat.object.is_null() && !(*clicked_win).object.is_null() {
+                    ffi::wl_resource_post_event(seat.object, 4, (*clicked_win).object);
+                }
+            } else {
+                let initial = (*server).wm.expose_initial_focus;
+                if (*server).wm.window_is_valid(initial) {
+                    seat.focus(Focus::Window(initial));
+                    if !seat.object.is_null() && !(*initial).object.is_null() {
+                        ffi::wl_resource_post_event(seat.object, 4, (*initial).object);
+                    }
+                } else {
+                    seat.focus(Focus::None);
+                }
+            }
+
+            (*server).wm.expose_hovered_window = std::ptr::null_mut();
+            (*server).wm.expose_initial_focus = std::ptr::null_mut();
+            (*server).wm.dirty_windowing();
+
+            // Insert into pressed so we handle the release correctly (standard wayland behavior)
+            cursor.pressed.insert((*event).button, None);
+            return;
+        }
+
         let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
         let modifiers = if !wlr_keyboard.is_null() {
             ffi::wlr_keyboard_get_modifiers(wlr_keyboard)
diff --git a/src/server/window.rs b/src/server/window.rs
index 8417584..02c484a 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -669,6 +669,13 @@ impl Window {
             curr = next;
         }
 
+        if (*(*window).server).wm.expose_hovered_window == window {
+            (*(*window).server).wm.expose_hovered_window = std::ptr::null_mut();
+        }
+        if (*(*window).server).wm.expose_initial_focus == window {
+            (*(*window).server).wm.expose_initial_focus = std::ptr::null_mut();
+        }
+
         // Destroy decorations
         for decorations in [&mut (*window).decorations_above as *mut ffi::wl_list, &mut (*window).decorations_below as *mut ffi::wl_list] {
             let list_head = decorations as *mut WlList;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index f33e1a8..fce5e77 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -71,6 +71,8 @@ pub struct WindowManager {
     pub input_rules: Vec<crate::config::InputDeviceConfigRule>,
     pub input_config: crate::config::InputConfig,
     pub expose_active: bool,
+    pub expose_hovered_window: *mut Window,
+    pub expose_initial_focus: *mut Window,
     pub last_status_update: std::cell::RefCell<Option<crate::status_server::StatusUpdate>>,
 }
 
@@ -129,6 +131,8 @@ impl WindowManager {
         self.input_rules = Vec::new();
         self.input_config = crate::config::InputConfig::default();
         self.expose_active = false;
+        self.expose_hovered_window = std::ptr::null_mut();
+        self.expose_initial_focus = std::ptr::null_mut();
         self.last_status_update = std::cell::RefCell::new(None);
 
         ffi::wl_list_init(&mut self.sent.outputs);
@@ -920,32 +924,47 @@ impl WindowManager {
                 }
 
                 let is_focused = win_ptr == focused_window;
-                let (r, g_val, b, mut a) = if is_focused {
-                    (
-                        self.layout.border_r,
-                        self.layout.border_g,
-                        self.layout.border_b,
-                        self.layout.border_a,
-                    )
-                } else {
-                    let mut tiled_stack = tiled_windows.clone();
-                    tiled_stack.sort_by_key(|&w| stack_order.iter().position(|&x| x == w).unwrap_or(usize::MAX));
-                    let pos = tiled_stack.iter().position(|&w| w == win_ptr).unwrap_or(0);
-                    let depth = n_tiled - 1 - pos as i32;
-                    let mut factor = 1.0_f64;
-                    for _ in 0..depth {
-                        factor *= 0.70; // UNFOCUSED_DEPTH_FACTOR
+                let (win_bw, r, g_val, b, a) = if self.expose_active {
+                    if self.window_is_valid(self.expose_hovered_window) && win_ptr == self.expose_hovered_window {
+                        (
+                            bw as u32,
+                            self.layout.border_r,
+                            self.layout.border_g,
+                            self.layout.border_b,
+                            self.layout.border_a,
+                        )
+                    } else {
+                        (0, 0, 0, 0, 0)
                     }
-                    let r = blend_channel(self.layout.background_r, self.layout.border_r, factor);
-                    let g = blend_channel(self.layout.background_g, self.layout.border_g, factor);
-                    let b = blend_channel(self.layout.background_b, self.layout.border_b, factor);
-                    let a = blend_channel(self.layout.background_a, self.layout.border_a, factor);
-                    (r, g, b, a)
+                } else {
+                    let (r, g_val, b, a) = if is_focused {
+                        (
+                            self.layout.border_r,
+                            self.layout.border_g,
+                            self.layout.border_b,
+                            self.layout.border_a,
+                        )
+                    } else {
+                        let mut tiled_stack = tiled_windows.clone();
+                        tiled_stack.sort_by_key(|&w| stack_order.iter().position(|&x| x == w).unwrap_or(usize::MAX));
+                        let pos = tiled_stack.iter().position(|&w| w == win_ptr).unwrap_or(0);
+                        let depth = n_tiled - 1 - pos as i32;
+                        let mut factor = 1.0_f64;
+                        for _ in 0..depth {
+                            factor *= 0.70; // UNFOCUSED_DEPTH_FACTOR
+                        }
+                        let r = blend_channel(self.layout.background_r, self.layout.border_r, factor);
+                        let g = blend_channel(self.layout.background_g, self.layout.border_g, factor);
+                        let b = blend_channel(self.layout.background_b, self.layout.border_b, factor);
+                        let a = blend_channel(self.layout.background_a, self.layout.border_a, factor);
+                        (r, g, b, a)
+                    };
+                    (bw as u32, r, g_val, b, a)
                 };
 
                 (*win_ptr).rendering_requested.border = crate::window::Border {
                     edges: crate::window::Edges { top: true, bottom: true, left: true, right: true },
-                    width: bw as u32,
+                    width: win_bw,
                     r,
                     g: g_val,
                     b,
@@ -1169,6 +1188,13 @@ impl WindowManager {
         std::ptr::null_mut()
     }
 
+    pub unsafe fn window_is_valid(&self, win: *mut Window) -> bool {
+        if win.is_null() {
+            return false;
+        }
+        self.windows.iter().any(|&w| w == win)
+    }
+
     pub unsafe fn focused_layer_surface(&self) -> *mut ffi::wlr_surface {
         let seats_list = &(*self.server).input_manager.seats as *const ffi::wl_list as *const WlList as *mut WlList;
         let mut curr_seat = (*seats_list).next;
@@ -1594,6 +1620,27 @@ impl WindowManager {
             Action::Expose => {
                 self.expose_active = !self.expose_active;
                 log::info!("Expose mode toggled: {}", self.expose_active);
+                if self.expose_active {
+                    self.expose_initial_focus = self.focused_window();
+                    let mut hovered_win = std::ptr::null_mut();
+                    if let Some(seat) = self.first_seat() {
+                        let cursor = &(*seat).cursor;
+                        let lx = cursor.x();
+                        let ly = cursor.y();
+                        if let Some(result) = (*self.server).scene.at(lx, ly) {
+                            if let crate::scene_node_data::SceneNodeDataVal::Window(window) = result.data {
+                                hovered_win = window;
+                            }
+                        }
+                    }
+                    if hovered_win.is_null() {
+                        hovered_win = self.expose_initial_focus;
+                    }
+                    self.expose_hovered_window = hovered_win;
+                } else {
+                    self.expose_hovered_window = std::ptr::null_mut();
+                    self.expose_initial_focus = std::ptr::null_mut();
+                }
                 self.dirty_windowing();
             }
             _ => {}