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

commitfcc51dabb827a9f8d7caddab3827dccadcaff79a
parent9c80d3ba99
authorLucas Galante <[email protected]>
date2026-06-16 09:26
feat: implement dedicated Expose tiling mode with visual window scaling

 src/server/config.rs         |   1 +
 src/server/cursor.rs         |   3 ++
 src/server/status_server.rs  |   4 +-
 src/server/tiling.rs         |  51 ++++++++++++++++++++++
 src/server/window.rs         | 102 +++++++++++++++++++++++++++++++++++++++++--
 src/server/window_manager.rs |  74 ++++++++++++++++++++++++-------
 6 files changed, 215 insertions(+), 20 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 3200652..46ecd1d 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -392,6 +392,7 @@ pub fn parse_tiling_mode(s: &str) -> TilingMode {
         "fullscreen" => TilingMode::Fullscreen,
         "popup" => TilingMode::Popup,
         "sidepanel" | "side_panel" | "side-panel" => TilingMode::SidePanel,
+        "expose" => TilingMode::Expose,
         _ => TilingMode::Cascade,
     }
 }
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 4cb4af3..7af1589 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1326,6 +1326,9 @@ 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 {
+        return BorderZone::None;
+    }
     if (*window).tiling_mode != crate::tiling::TilingMode::Floating
         && (*window).tiling_mode != crate::tiling::TilingMode::Cascade
     {
diff --git a/src/server/status_server.rs b/src/server/status_server.rs
index 49d2e1d..a7cf54c 100644
--- a/src/server/status_server.rs
+++ b/src/server/status_server.rs
@@ -246,7 +246,9 @@ pub unsafe fn build_status_update(wm: &crate::window_manager::WindowManager) ->
         &wm.layout.status_normal_color,
     );
 
-    let layout_text = if !focused_window.is_null() {
+    let layout_text = if wm.expose_active {
+        "Expose".to_string()
+    } else if !focused_window.is_null() {
         (*focused_window).tiling_mode.as_str().to_string()
     } else {
         wm.global_layout.as_str().to_string()
diff --git a/src/server/tiling.rs b/src/server/tiling.rs
index 339e06f..a82c104 100644
--- a/src/server/tiling.rs
+++ b/src/server/tiling.rs
@@ -8,6 +8,7 @@ pub enum TilingMode {
     Fullscreen,
     Popup,
     SidePanel,
+    Expose,
 }
 
 impl TilingMode {
@@ -19,6 +20,7 @@ impl TilingMode {
             TilingMode::Fullscreen => "Fullscreen",
             TilingMode::Popup => "Popup",
             TilingMode::SidePanel => "Side Panel",
+            TilingMode::Expose => "Expose",
         }
     }
 }
@@ -85,6 +87,55 @@ 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,
+    bar_height: i32,
+    n_expose: i32,
+    idx: i32,
+) -> (i32, i32, i32, i32) {
+    let dec_h = std::cmp::max(bw, 16);
+    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 ccaf0a1..6d48849 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -220,6 +220,7 @@ pub struct Window {
     pub anim_opacity: Option<f64>,
     pub circular: bool,
     pub blur: bool,
+    pub scale: f64,
 
     pub wm_scheduled: WmScheduledState,
     pub wm_sent: WmSentState,
@@ -334,6 +335,7 @@ impl Window {
             anim_opacity: None,
             circular: false,
             blur: false,
+            scale: 1.0,
             wm_scheduled: WmScheduledState {
                 dimensions_hint: DimensionsHint { min_width: 0, min_height: 0, max_width: 0, max_height: 0 },
                 decoration_hint: ffi::river_window_v1_decoration_hint_RIVER_WINDOW_V1_DECORATION_HINT_ONLY_SUPPORTS_CSD,
@@ -1317,6 +1319,50 @@ impl Window {
                 Some(set_corner_radius_iterator),
                 &radius as *const i32 as *mut std::ffi::c_void,
             );
+
+            struct ScaleData {
+                scale: f64,
+            }
+
+            unsafe extern "C" fn set_expose_scale_iterator(
+                buffer: *mut ffi::wlr_scene_buffer,
+                sx: i32,
+                sy: i32,
+                user_data: *mut std::ffi::c_void,
+            ) {
+                let data = &*(user_data as *const ScaleData);
+                let node = buffer as *mut ffi::wlr_scene_node;
+
+                if data.scale == 1.0 {
+                    ffi::wlr_scene_buffer_set_dest_size(buffer, 0, 0);
+                    ffi::wlr_scene_node_set_position(node, sx, sy);
+                } else {
+                    let surface = ffi::river_scene_node_get_surface(node);
+                    if !surface.is_null() {
+                        let w = ffi::river_wlr_surface_get_width(surface);
+                        let h = ffi::river_wlr_surface_get_height(surface);
+                        let dest_w = (w as f64 * data.scale) as i32;
+                        let dest_h = (h as f64 * data.scale) as i32;
+                        ffi::wlr_scene_buffer_set_dest_size(buffer, dest_w, dest_h);
+
+                        let dest_x = (sx as f64 * data.scale) as i32;
+                        let dest_y = (sy as f64 * data.scale) as i32;
+                        ffi::wlr_scene_node_set_position(node, dest_x, dest_y);
+                    }
+                }
+            }
+
+            let scale_data = ScaleData { scale: self.scale };
+            ffi::wlr_scene_node_for_each_buffer(
+                self.surfaces.tree as *mut ffi::wlr_scene_node,
+                Some(set_expose_scale_iterator),
+                &scale_data as *const ScaleData as *mut std::ffi::c_void,
+            );
+            ffi::wlr_scene_node_for_each_buffer(
+                self.popup_tree as *mut ffi::wlr_scene_node,
+                Some(set_expose_scale_iterator),
+                &scale_data as *const ScaleData as *mut std::ffi::c_void,
+            );
         }
 
         self.box_geom.width = self.rendering_sent.width as i32;
@@ -1424,7 +1470,9 @@ impl Window {
             (border.a as f64 / u32::MAX as f64) as f32,
         ];
         ffi::wlr_scene_node_set_position(self.window_background as *mut ffi::wlr_scene_node, 0, 0);
-        ffi::wlr_scene_rect_set_size(self.window_background, self.box_geom.width, self.box_geom.height);
+        let bg_width = (self.box_geom.width as f64 * self.scale) as i32;
+        let bg_height = (self.box_geom.height as f64 * self.scale) as i32;
+        ffi::wlr_scene_rect_set_size(self.window_background, bg_width, bg_height);
         ffi::wlr_scene_rect_set_color(self.window_background, bg_color.as_ptr());
         ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, !requested.hidden);
 
@@ -1503,8 +1551,12 @@ impl Window {
                     **edge_box = clip_intersect;
                 }
                 ffi::wlr_scene_node_set_enabled(*rect as *mut ffi::wlr_scene_node, *enabled);
-                ffi::wlr_scene_node_set_position(*rect as *mut ffi::wlr_scene_node, (*edge_box).x, (*edge_box).y);
-                ffi::wlr_scene_rect_set_size(*rect, (*edge_box).width, (*edge_box).height);
+                let scaled_x = ((*edge_box).x as f64 * self.scale) as i32;
+                let scaled_y = ((*edge_box).y as f64 * self.scale) as i32;
+                let scaled_w = ((*edge_box).width as f64 * self.scale) as i32;
+                let scaled_h = ((*edge_box).height as f64 * self.scale) as i32;
+                ffi::wlr_scene_node_set_position(*rect as *mut ffi::wlr_scene_node, scaled_x, scaled_y);
+                ffi::wlr_scene_rect_set_size(*rect, scaled_w, scaled_h);
                 ffi::wlr_scene_rect_set_color(*rect, color.as_ptr());
             }
         }
@@ -2318,7 +2370,49 @@ impl Decoration {
 
         ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, self.rendering_requested.blur);
 
-        ffi::wlr_scene_node_set_position(self.tree as *mut ffi::wlr_scene_node, self.rendering_requested.offset_x, self.rendering_requested.offset_y);
+        let scale = (*self.window).scale;
+        let scaled_x = (self.rendering_requested.offset_x as f64 * scale) as i32;
+        let scaled_y = (self.rendering_requested.offset_y as f64 * scale) as i32;
+        ffi::wlr_scene_node_set_position(self.tree as *mut ffi::wlr_scene_node, scaled_x, scaled_y);
+
+        struct ScaleData {
+            scale: f64,
+        }
+
+        unsafe extern "C" fn set_expose_scale_iterator(
+            buffer: *mut ffi::wlr_scene_buffer,
+            sx: i32,
+            sy: i32,
+            user_data: *mut std::ffi::c_void,
+        ) {
+            let data = &*(user_data as *const ScaleData);
+            let node = buffer as *mut ffi::wlr_scene_node;
+
+            if data.scale == 1.0 {
+                ffi::wlr_scene_buffer_set_dest_size(buffer, 0, 0);
+                ffi::wlr_scene_node_set_position(node, sx, sy);
+            } else {
+                let surface = ffi::river_scene_node_get_surface(node);
+                if !surface.is_null() {
+                    let w = ffi::river_wlr_surface_get_width(surface);
+                    let h = ffi::river_wlr_surface_get_height(surface);
+                    let dest_w = (w as f64 * data.scale) as i32;
+                    let dest_h = (h as f64 * data.scale) as i32;
+                    ffi::wlr_scene_buffer_set_dest_size(buffer, dest_w, dest_h);
+
+                    let dest_x = (sx as f64 * data.scale) as i32;
+                    let dest_y = (sy as f64 * data.scale) as i32;
+                    ffi::wlr_scene_node_set_position(node, dest_x, dest_y);
+                }
+            }
+        }
+
+        let scale_data = ScaleData { scale };
+        ffi::wlr_scene_node_for_each_buffer(
+            self.surfaces.tree as *mut ffi::wlr_scene_node,
+            Some(set_expose_scale_iterator),
+            &scale_data as *const ScaleData as *mut std::ffi::c_void,
+        );
 
         let children_head = ffi::river_scene_tree_get_children(self.surfaces.tree) as *mut WlList;
         if (*children_head).next != children_head {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 03b5747..2dfda72 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -565,7 +565,7 @@ impl WindowManager {
         }
 
         if self.expose_active {
-            return crate::tiling::TilingMode::Grid;
+            return crate::tiling::TilingMode::Expose;
         }
 
         if (*win).mode_locked {
@@ -715,7 +715,9 @@ impl WindowManager {
                 (*win_ptr).rendering_requested.hidden = false;
 
                 let mode = self.get_mode_for_window(win_ptr);
-                (*win_ptr).tiling_mode = mode;
+                if !self.expose_active {
+                    (*win_ptr).tiling_mode = mode;
+                }
 
                 // Apply ModeRule SSD configuration if defined and not locked
                 if !(*win_ptr).mode_locked {
@@ -784,6 +786,8 @@ impl WindowManager {
                 self.global_layout
             };
 
+            log::info!("arrange_views: n_tiled = {}, current_layout = {:?}, expose_active = {}", n_tiled, current_layout, self.expose_active);
+
             if current_layout == crate::tiling::TilingMode::Cascade {
                 tiled_windows.sort_by_key(|&w| stack_order.iter().position(|&x| x == w).unwrap_or(usize::MAX));
             }
@@ -795,7 +799,7 @@ impl WindowManager {
             let gap_bottom = self.layout.gap_bottom;
             let bw = match current_layout {
                 crate::tiling::TilingMode::Cascade => self.layout.cascade_border_width,
-                crate::tiling::TilingMode::Grid => self.layout.grid_border_width,
+                crate::tiling::TilingMode::Grid | crate::tiling::TilingMode::Expose => self.layout.grid_border_width,
                 crate::tiling::TilingMode::Fullscreen => self.layout.fullscreen_border_width,
                 _ => self.layout.border_width,
             };
@@ -816,6 +820,12 @@ impl WindowManager {
                             bw, bar_height, n_tiled, idx as i32
                         )
                     }
+                    crate::tiling::TilingMode::Expose => {
+                        crate::tiling::tile_expose(
+                            tiled_usable_w, usable_h, gap, gap_top, gap_left, gap_right, gap_bottom,
+                            bw, bar_height, n_tiled, idx as i32
+                        )
+                    }
                     crate::tiling::TilingMode::Fullscreen => {
                         crate::tiling::tile_fullscreen(
                             tiled_usable_w, usable_h, gap_top, gap_left, gap_right, gap_bottom,
@@ -827,21 +837,55 @@ impl WindowManager {
                     }
                 };
 
-                let final_x = tiled_usable_x + x;
-                let final_y = usable_y + y;
+                let mut final_x = tiled_usable_x + x;
+                let mut final_y = usable_y + y;
+
+                if self.expose_active {
+                    let orig_w = (*win_ptr).box_geom.width;
+                    let orig_h = (*win_ptr).box_geom.height;
+                    let scale = if orig_w > 0 && orig_h > 0 {
+                        let scale_x = w as f64 / orig_w as f64;
+                        let scale_y = h as f64 / orig_h as f64;
+                        scale_x.min(scale_y).min(1.0)
+                    } else {
+                        1.0
+                    };
+                    (*win_ptr).scale = scale;
+
+                    let visual_w = orig_w as f64 * scale;
+                    let visual_h = orig_h as f64 * scale;
+                    let offset_x = (w as f64 - visual_w) / 2.0;
+                    let offset_y = (h as f64 - visual_h) / 2.0;
+
+                    final_x += offset_x as i32;
+                    final_y += offset_y as i32;
+                } else {
+                    (*win_ptr).scale = 1.0;
+                }
+
+                log::info!("arrange_views: tiled window index {}, title = {:?}, app_id = {:?}, geom_box = (x={}, y={}, w={}, h={}), target_box = (x={}, y={}, w={}, h={}), scale = {}",
+                    idx,
+                    (*win_ptr).get_title_string(),
+                    (*win_ptr).get_app_id_string(),
+                    (*win_ptr).box_geom.x, (*win_ptr).box_geom.y, (*win_ptr).box_geom.width, (*win_ptr).box_geom.height,
+                    final_x, final_y, w, h, (*win_ptr).scale
+                );
 
                 (*win_ptr).rendering_requested.x = final_x;
                 (*win_ptr).rendering_requested.y = final_y;
-                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
-                    width: w as u32,
-                    height: h as u32,
-                });
-                (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
-                    width: w as u32,
-                    height: h as u32,
-                };
-                (*win_ptr).wm_requested.tiled = 1 | 2 | 4 | 8;
-                (*win_ptr).wm_requested.ssd = true;
+
+                if !self.expose_active {
+                    (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
+                        width: w as u32,
+                        height: h as u32,
+                    });
+                    (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
+                        width: w as u32,
+                        height: h as u32,
+                    };
+                    (*win_ptr).wm_requested.tiled = 1 | 2 | 4 | 8;
+                    (*win_ptr).wm_requested.ssd = true;
+                }
 
                 let is_focused = win_ptr == focused_window;
                 let (r, g_val, b, mut a) = if is_focused {