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