Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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 {