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

commit4055b1d3d4f3fbfc75aae91b8bc1283f4181eef3
parent47c24736be
authorLucas Galante <[email protected]>
date2026-06-24 22:18
Remove expose mode and replace with viewport zoom-out/zoom-in gestures

 src/server/config.rs         |   1 -
 src/server/cursor.rs         |  63 ++++++++++----------
 src/server/output.rs         |   2 +-
 src/server/tiling.rs         |  51 ----------------
 src/server/window.rs         |  14 +----
 src/server/window_manager.rs | 139 ++++++++++++++++++++++++++++++++++---------
 6 files changed, 146 insertions(+), 124 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 3565fed..98b9497 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -433,7 +433,6 @@ pub fn parse_tiling_mode(s: &str) -> TilingMode {
         "fullscreen" => TilingMode::Fullscreen,
         "popup" => TilingMode::Popup,
         "sidepanel" | "side_panel" | "side-panel" | "pinned" => TilingMode::Pinned,
-        "expose" => TilingMode::Expose,
         "status" => TilingMode::Status,
         _ => TilingMode::Cascade,
     }
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index f7909bb..fab8ddb 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -458,18 +458,10 @@ 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
                     && (*window).tiling_mode != crate::tiling::TilingMode::Status
-                    && !(*server).wm.expose_active
+                    && (*server).wm.desk_zoom >= 0.999
                 {
                     match get_border_zone(window, lx, ly) {
                         BorderZone::Resize(edges) => {
@@ -570,8 +562,8 @@ 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 {
+        // --- ZOOMED OUT CLICK HANDLING ---
+        if (*event).button == 0x110 && (*(*seat).server).wm.desk_zoom < 0.999 {
             let lx = cursor.x();
             let ly = cursor.y();
             let server = seat.server;
@@ -582,34 +574,43 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                 }
             }
 
-            // Exit expose mode
-            (*server).wm.expose_active = false;
-            log::info!("Expose mode exited via left click");
-
             if !clicked_win.is_null() && !(*clicked_win).is_status_bar() {
                 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);
+
+                let mut viewport_w = 1920.0;
+                let mut viewport_h = 1080.0;
+                let outputs_list = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+                let mut curr_out = (*outputs_list).next;
+                while curr_out != outputs_list {
+                    let output = crate::container_of!(curr_out, crate::output::Output, link);
+                    if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+                        let wlr_box = (*output).sent.box_layout();
+                        viewport_w = wlr_box.width as f64;
+                        viewport_h = wlr_box.height as f64;
+                        break;
                     }
-                } else {
-                    seat.focus(Focus::None);
+                    curr_out = (*curr_out).next;
                 }
-            }
 
-            (*server).wm.expose_hovered_window = std::ptr::null_mut();
-            (*server).wm.expose_initial_focus = std::ptr::null_mut();
-            (*server).wm.dirty_windowing();
+                (*server).wm.desk_zoom = 1.0;
 
-            // Insert into pressed so we handle the release correctly (standard wayland behavior)
-            cursor.pressed.insert((*event).button, None);
-            return;
+                let win_w = if (*clicked_win).box_geom.width > 0 { (*clicked_win).box_geom.width as f64 } else { 800.0 };
+                let win_h = if (*clicked_win).box_geom.height > 0 { (*clicked_win).box_geom.height as f64 } else { 600.0 };
+
+                let center_x = (*clicked_win).virtual_x + win_w / 2.0;
+                let center_y = (*clicked_win).virtual_y + win_h / 2.0;
+
+                (*server).wm.desk_pan_x = center_x - viewport_w / 2.0;
+                (*server).wm.desk_pan_y = center_y - viewport_h / 2.0;
+
+                (*server).wm.dirty_windowing();
+
+                cursor.pressed.insert((*event).button, None);
+                return;
+            }
         }
 
         let wlr_keyboard = ffi::river_wlr_seat_get_keyboard(seat.wlr_seat);
@@ -1422,7 +1423,7 @@ pub enum BorderZone {
 }
 
 pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f64) -> BorderZone {
-    if (*(*window).server).wm.expose_active {
+    if (*(*window).server).wm.desk_zoom < 0.999 {
         return BorderZone::None;
     }
     if (*window).tiling_mode == crate::tiling::TilingMode::Popup
diff --git a/src/server/output.rs b/src/server/output.rs
index a99a15c..986c586 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -431,7 +431,7 @@ impl Output {
             return Ok(());
         }
 
-        // Re-apply scale to all windows in expose mode right before rendering
+        // Re-apply scale to all windows whose scale is not 1.0 right before rendering
         let wm = &(*self.server).wm;
         for &window in wm.windows.iter() {
             if !window.is_null() && (*window).scale != 1.0 {
diff --git a/src/server/tiling.rs b/src/server/tiling.rs
index 59d927a..05e5c28 100644
--- a/src/server/tiling.rs
+++ b/src/server/tiling.rs
@@ -8,7 +8,6 @@ pub enum TilingMode {
     Fullscreen,
     Popup,
     Pinned,
-    Expose,
     Status,
 }
 
@@ -21,7 +20,6 @@ impl TilingMode {
             TilingMode::Fullscreen => "Fullscreen",
             TilingMode::Popup => "Popup",
             TilingMode::Pinned => "Pinned",
-            TilingMode::Expose => "Expose",
             TilingMode::Status => "Status",
         }
     }
@@ -89,55 +87,6 @@ pub fn tile_grid(
     (x, y, width, height)
 }
 
-/// Tile a window in expose mode.
-pub fn tile_expose(
-    screen_w: i32,
-    screen_h: i32,
-    gap: i32,
-    gap_top: i32,
-    gap_left: i32,
-    gap_right: i32,
-    gap_bottom: i32,
-    bw: i32,
-    dec_h: i32,
-    bar_height: i32,
-    n_expose: i32,
-    idx: i32,
-) -> (i32, i32, i32, i32) {
-    if n_expose == 1 {
-        // Center the single window and scale to 70% of the screen
-        let usable_h = screen_h - bar_height - gap_top - gap_bottom;
-        let width = (screen_w * 7) / 10 - 2 * bw;
-        let height = (usable_h * 7) / 10 - (dec_h + bw);
-        let width = if width < 1 { 1 } else { width };
-        let height = if height < 1 { 1 } else { height };
-        
-        let x = gap_left + (screen_w - gap_left - gap_right - width - 2 * bw) / 2;
-        let y = bar_height + gap_top + (usable_h - height - (dec_h + bw)) / 2 + dec_h;
-        (x, y, width, height)
-    } else {
-        // Tile in a square-root layout grid with a 60px outer margin
-        let outer_margin = 60;
-        let inner_w = screen_w - 2 * outer_margin;
-        let inner_h = screen_h - bar_height - 2 * outer_margin;
-        
-        let cols = (n_expose as f32).sqrt().ceil() as i32;
-        let rows = (n_expose + cols - 1) / cols;
-        
-        let row = idx / cols;
-        let col = idx % cols;
-        
-        let width = (inner_w - gap_left - gap_right - (cols - 1) * gap) / cols - 2 * bw;
-        let height = (inner_h - gap_top - gap_bottom - (rows - 1) * gap) / rows - (dec_h + bw);
-        let width = if width < 1 { 1 } else { width };
-        let height = if height < 1 { 1 } else { height };
-        
-        let x = outer_margin + gap_left + bw + col * (width + 2 * bw + gap);
-        let y = bar_height + outer_margin + gap_top + dec_h + row * (height + (dec_h + bw) + gap);
-        (x, y, width, height)
-    }
-}
-
 /// Tile a window in fullscreen mode.
 pub fn tile_fullscreen(
     screen_w: i32,
diff --git a/src/server/window.rs b/src/server/window.rs
index dd814b5..7768709 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -728,12 +728,7 @@ impl Window {
             self.wlr_toplevel_handle = std::ptr::null_mut();
         }
 
-        if (*self.server).wm.expose_hovered_window == self as *mut Window {
-            (*self.server).wm.expose_hovered_window = std::ptr::null_mut();
-        }
-        if (*self.server).wm.expose_initial_focus == self as *mut Window {
-            (*self.server).wm.expose_initial_focus = std::ptr::null_mut();
-        }
+
     }
 
     pub unsafe fn close(&mut self) {
@@ -777,12 +772,7 @@ 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] {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index dedc992..497ddab 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -95,9 +95,6 @@ pub struct WindowManager {
     pub output_scale: f32,
     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>>,
     pub restore_queue: Vec<SavedWindowState>,
     pub shutting_down: bool,
@@ -166,9 +163,6 @@ impl WindowManager {
         self.status_sender = None;
         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);
@@ -756,9 +750,7 @@ impl WindowManager {
             return crate::tiling::TilingMode::Popup;
         }
 
-        if self.expose_active {
-            return crate::tiling::TilingMode::Expose;
-        }
+
 
         if (*win).mode_locked {
             return (*win).tiling_mode;
@@ -1629,43 +1621,134 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                 self.dirty_windowing();
             }
             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 self.desk_zoom < 0.999 {
+                    let mut viewport_w = 1920.0;
+                    let mut viewport_h = 1080.0;
+                    let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
+                    let mut curr_out = (*outputs_list).next;
+                    while curr_out != outputs_list {
+                        let output = crate::container_of!(curr_out, crate::output::Output, link);
+                        if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+                            let wlr_box = (*output).sent.box_layout();
+                            viewport_w = wlr_box.width as f64;
+                            viewport_h = wlr_box.height as f64;
+                            break;
+                        }
+                        curr_out = (*curr_out).next;
+                    }
+
                     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 let crate::seat::Focus::Window(fw) = (*seat).focused {
+                            if self.window_is_valid(fw) {
+                                let win_w = if (*fw).box_geom.width > 0 { (*fw).box_geom.width as f64 } else { 800.0 };
+                                let win_h = if (*fw).box_geom.height > 0 { (*fw).box_geom.height as f64 } else { 600.0 };
+                                let center_x = (*fw).virtual_x + win_w / 2.0;
+                                let center_y = (*fw).virtual_y + win_h / 2.0;
+                                self.desk_zoom = 1.0;
+                                self.desk_pan_x = center_x - viewport_w / 2.0;
+                                self.desk_pan_y = center_y - viewport_h / 2.0;
+                                self.dirty_windowing();
+                                return;
                             }
                         }
                     }
-                    if hovered_win.is_null() {
-                        hovered_win = self.expose_initial_focus;
-                    }
-                    self.expose_hovered_window = hovered_win;
+                    self.desk_zoom = 1.0;
+                    self.desk_pan_x = 0.0;
+                    self.desk_pan_y = 0.0;
+                    self.dirty_windowing();
                 } else {
-                    self.expose_hovered_window = std::ptr::null_mut();
-                    self.expose_initial_focus = std::ptr::null_mut();
+                    let mut min_vx = f64::MAX;
+                    let mut max_vx = f64::MIN;
+                    let mut min_vy = f64::MAX;
+                    let mut max_vy = f64::MIN;
+                    let mut has_visible_windows = false;
+
+                    for &win_ptr in self.windows.iter() {
+                        if win_ptr.is_null() || (*win_ptr).closed || (*win_ptr).minimized {
+                            continue;
+                        }
+
+                        let app_id = (*win_ptr).get_app_id_string();
+                        let is_status_bar = app_id.as_deref() == Some("cce-status-interface");
+                        if is_status_bar {
+                            continue;
+                        }
+
+                        let visible = !matches!((*win_ptr).state, crate::window::WindowState::Closing | crate::window::WindowState::Init);
+                        if !visible {
+                            continue;
+                        }
+
+                        let mode = self.get_mode_for_window(win_ptr);
+                        if mode == crate::tiling::TilingMode::Popup || mode == crate::tiling::TilingMode::Pinned {
+                            continue;
+                        }
+
+                        let win_w = if (*win_ptr).box_geom.width > 0 { (*win_ptr).box_geom.width as f64 } else { 800.0 };
+                        let win_h = if (*win_ptr).box_geom.height > 0 { (*win_ptr).box_geom.height as f64 } else { 600.0 };
+
+                        let vx = (*win_ptr).virtual_x;
+                        let vy = (*win_ptr).virtual_y;
+
+                        if vx < min_vx { min_vx = vx; }
+                        if vx + win_w > max_vx { max_vx = vx + win_w; }
+                        if vy < min_vy { min_vy = vy; }
+                        if vy + win_h > max_vy { max_vy = vy + win_h; }
+                        has_visible_windows = true;
+                    }
+
+                    if has_visible_windows {
+                        let mut viewport_w = 1920.0;
+                        let mut viewport_h = 1080.0;
+                        let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
+                        let mut curr_out = (*outputs_list).next;
+                        while curr_out != outputs_list {
+                            let output = crate::container_of!(curr_out, crate::output::Output, link);
+                            if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+                                let wlr_box = (*output).sent.box_layout();
+                                viewport_w = wlr_box.width as f64;
+                                viewport_h = wlr_box.height as f64;
+                                break;
+                            }
+                            curr_out = (*curr_out).next;
+                        }
+
+                        let box_w = max_vx - min_vx;
+                        let box_h = max_vy - min_vy;
+
+                        let margin = 100.0;
+                        let avail_w = (viewport_w - 2.0 * margin).max(200.0);
+                        let avail_h = (viewport_h - 2.0 * margin).max(200.0);
+
+                        let zoom_x = avail_w / box_w.max(1.0);
+                        let zoom_y = avail_h / box_h.max(1.0);
+                        let new_zoom = zoom_x.min(zoom_y).min(1.0).max(0.05);
+
+                        let center_x = min_vx + box_w / 2.0;
+                        let center_y = min_vy + box_h / 2.0;
+
+                        self.desk_zoom = new_zoom;
+                        self.desk_pan_x = center_x - (viewport_w / 2.0) / new_zoom;
+                        self.desk_pan_y = center_y - (viewport_h / 2.0) / new_zoom;
+                        self.dirty_windowing();
+                    }
                 }
-                self.dirty_windowing();
             }
             _ => {}
         }
     }
 
     pub unsafe fn process_ipc_command(&mut self, cmd: &str) -> String {
-        self.stop_panning_animation();
         let parts: Vec<&str> = cmd.split_whitespace().collect();
         if parts.is_empty() {
+            self.stop_panning_animation();
             return "error: empty command\n".to_string();
         }
         
         let action = parts[0];
+        if action != "focus-window" {
+            self.stop_panning_animation();
+        }
         match action {
             "view" => {
                 if parts.len() < 2 { return "error: missing tag\n".to_string(); }