Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
refactor: dispatch camera actions through the Policy/Compositor traits
execute_action now builds an ActionCtx snapshot (build_action_ctx:
first-enabled-output viewport, cursor output box, hovered window via
scene hit-test, focused window, per-window rects + expose eligibility)
and asks policy::actions::DefaultPolicy first; the returned commands
run through the new `impl Compositor for WindowManager` applier. The
five migrated arms — View1-4, SetViewport1-4, Zoom*, Pan*, and the
~180-line Expose arm — are deleted; unclaimed actions fall through to
the remaining legacy arms unchanged. Command application preserves the
per-action update paths (Relayout vs the idle fast-path RefreshCamera)
and the Expose-exit focus event post.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/window_manager.rs | 463 +++++++++++++++++--------------------------
1 file changed, 184 insertions(+), 279 deletions(-)
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index bc441e6..8b85103 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -673,6 +673,114 @@ impl WindowManager {
}
}
+ /// Snapshot for `Policy::action`: seat- and scene-dependent facts
+ /// (cursor output, hovered window, focus) resolved up front, the
+ /// arrange-pass convention.
+ unsafe fn build_action_ctx(&mut self) -> crate::policy::api::ActionCtx {
+ use crate::policy::api::{ActionCtx, ActionWindow, Rect, WindowId};
+
+ // First enabled output: the legacy viewport for zooms and View jumps.
+ let (mut viewport_w, mut viewport_h) = (1920.0, 1080.0);
+ let (mut first_x, mut first_y) = (0.0, 0.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;
+ first_x = wlr_box.x as f64;
+ first_y = wlr_box.y as f64;
+ break;
+ }
+ curr_out = (*curr_out).next;
+ }
+
+ let mut cursor_viewport = Rect {
+ x: first_x as i32,
+ y: first_y as i32,
+ width: viewport_w as i32,
+ height: viewport_h as i32,
+ };
+ let mut has_cursor = false;
+ let (mut cursor_x, mut cursor_y) = (0.0, 0.0);
+ let mut hovered = None;
+ let mut focused = None;
+ if let Some(seat) = self.first_seat() {
+ has_cursor = true;
+ cursor_x = (*seat).cursor.x();
+ cursor_y = (*seat).cursor.y();
+ let wlr_output = (*self.server).om.output_at(cursor_x, cursor_y);
+ if !wlr_output.is_null() {
+ let mut output_box = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
+ ffi::wlr_output_layout_get_box((*self.server).om.output_layout, wlr_output, &mut output_box);
+ cursor_viewport = Rect {
+ x: output_box.x,
+ y: output_box.y,
+ width: output_box.width,
+ height: output_box.height,
+ };
+ }
+ if let Some(result) = (*self.server).scene.at(cursor_x, cursor_y) {
+ if let crate::scene_node_data::SceneNodeDataVal::Window(w) = result.data {
+ if !(*w).is_status_bar() && !(*w).is_wallpaper() {
+ hovered = Some(WindowId((*w).ref_key));
+ }
+ }
+ }
+ if let crate::seat::Focus::Window(fw) = (*seat).focused {
+ if !fw.is_null() && !(*fw).closed {
+ focused = Some(WindowId((*fw).ref_key));
+ }
+ }
+ }
+
+ let mut windows = Vec::new();
+ for &w in self.windows.iter() {
+ if w.is_null() || (*w).closed {
+ continue;
+ }
+ let app_id = (*w).get_app_id_string();
+ let is_status = app_id.as_deref().map_or(false, |id| id.starts_with("cce-status"));
+ let is_wallpaper = app_id.as_deref() == Some("cce-wallpaper");
+ let visible = !matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init);
+ let mode = self.get_mode_for_window(w);
+ let expose_eligible = !(*w).minimized
+ && !is_status
+ && !is_wallpaper
+ && visible
+ && mode != crate::tiling::TilingMode::Popup
+ && mode != crate::tiling::TilingMode::Overlay;
+ windows.push(ActionWindow {
+ id: WindowId((*w).ref_key),
+ x: (*w).virtual_x,
+ y: (*w).virtual_y,
+ w: if (*w).box_geom.width > 0 { (*w).box_geom.width as f64 } else { 800.0 },
+ h: if (*w).box_geom.height > 0 { (*w).box_geom.height as f64 } else { 600.0 },
+ expose_eligible,
+ });
+ }
+
+ ActionCtx {
+ camera: self.camera(),
+ overview: self.mode == WindowManagerMode::Overview,
+ pan_target_x: self.target_desk_pan_x,
+ pan_target_y: self.target_desk_pan_y,
+ viewport_w,
+ viewport_h,
+ cursor_viewport,
+ has_cursor,
+ cursor_x,
+ cursor_y,
+ hovered,
+ focused,
+ grid_period: self.layout.desktop_grid_scale.max(5.0)
+ + self.layout.desktop_gap_width.max(0) as f64,
+ windows,
+ }
+ }
+
/// Record the edge auto-pan velocity (screen px/s) and arm its 16ms tick
/// when nonzero. A zero velocity just parks: the armed tick sees it and
/// stops itself without re-arming.
@@ -1920,6 +2028,23 @@ impl WindowManager {
pub unsafe fn execute_action(&mut self, action: &crate::config::Action, command: Option<&str>) {
use crate::config::Action;
self.stop_panning_animation();
+
+ // Snapshot → policy → commands: the camera actions (zoom, pan, view
+ // jumps, overview) are decided in the policy crate. An empty command
+ // list means the policy doesn't claim the action, and the legacy
+ // arms below handle it.
+ {
+ use crate::policy::api::{Compositor, Policy};
+ let ctx = self.build_action_ctx();
+ let cmds = crate::policy::actions::DefaultPolicy.action(&ctx, *action);
+ if !cmds.is_empty() {
+ for cmd in &cmds {
+ self.apply(cmd);
+ }
+ return;
+ }
+ }
+
match action {
Action::None => {}
Action::Spawn => {
@@ -2207,110 +2332,6 @@ impl WindowManager {
}
}
}
- Action::View1 | Action::View2 | Action::View3 | Action::View4 => {
- let (target_x, target_y) = match action {
- Action::View1 => (0.0, 0.0),
- Action::View2 => (2000.0, 0.0),
- Action::View3 => (0.0, 2000.0),
- Action::View4 => (2000.0, 2000.0),
- _ => (0.0, 0.0),
- };
-
- let (mut viewport_w, mut viewport_h) = (1920.0, 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 cam = crate::policy::camera::center_on(target_x, target_y, viewport_w, viewport_h, self.desk_zoom);
- self.desk_pan_x = cam.pan_x;
- self.desk_pan_y = cam.pan_y;
- self.dirty_windowing();
- }
- Action::SetViewport1 | Action::SetViewport2 | Action::SetViewport3 | Action::SetViewport4 => {
- let (target_x, target_y) = match action {
- Action::SetViewport1 => (0.0, 0.0),
- Action::SetViewport2 => (2000.0, 0.0),
- Action::SetViewport3 => (0.0, 2000.0),
- Action::SetViewport4 => (2000.0, 2000.0),
- _ => (0.0, 0.0),
- };
- if let Some(seat) = self.first_seat() {
- if let crate::seat::Focus::Window(fw) = (*seat).focused {
- let w = if (*fw).box_geom.width > 0 { (*fw).box_geom.width as f64 } else { 800.0 };
- let h = if (*fw).box_geom.height > 0 { (*fw).box_geom.height as f64 } else { 600.0 };
- (*fw).virtual_x = target_x - w / 2.0;
- (*fw).virtual_y = target_y - h / 2.0;
- self.dirty_windowing();
- }
- }
- }
- Action::ZoomIn | Action::ZoomOut | Action::ZoomReset => {
- let (mut viewport_w, mut viewport_h) = (1920.0, 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;
- }
-
- // Keyed zooms pivot about the viewport center.
- let dir = match action {
- Action::ZoomIn => 1.0,
- Action::ZoomOut => -1.0,
- _ => 0.0,
- };
- let new_zoom = crate::policy::camera::keyed_zoom(self.desk_zoom, dir);
- let cam = crate::policy::camera::zoom_about_anchor(
- self.camera(),
- viewport_w / 2.0,
- viewport_h / 2.0,
- new_zoom,
- );
- self.desk_pan_x = cam.pan_x;
- self.desk_pan_y = cam.pan_y;
- self.desk_zoom = cam.zoom;
- self.mode = if crate::policy::camera::is_overview(cam.zoom) { WindowManagerMode::Overview } else { WindowManagerMode::Normal };
- self.dirty_windowing();
- }
- Action::PanLeft | Action::PanRight | Action::PanUp | Action::PanDown => {
- // Keyed pans move cell-by-cell and land aligned: the target
- // is the adjacent pan offset that puts the viewport origin on
- // a grid-period boundary, eased in by the pan animation.
- // Basing each step on the pending target (not the current
- // offset) lets rapid presses queue one cell apiece.
- let period = self.layout.desktop_grid_scale.max(5.0)
- + self.layout.desktop_gap_width.max(0) as f64;
- let (dx, dy) = match action {
- Action::PanLeft => (-1.0, 0.0),
- Action::PanRight => (1.0, 0.0),
- Action::PanUp => (0.0, -1.0),
- _ => (0.0, 1.0),
- };
- if dx != 0.0 {
- let base = self.target_desk_pan_x.unwrap_or(self.desk_pan_x);
- self.target_desk_pan_x = Some(crate::policy::pan::aligned_step(base, period, dx));
- }
- if dy != 0.0 {
- let base = self.target_desk_pan_y.unwrap_or(self.desk_pan_y);
- self.target_desk_pan_y = Some(crate::policy::pan::aligned_step(base, period, dy));
- }
- self.start_panning_animation();
- }
Action::OverlayLeft => {
self.layout.overlay_position = "left".to_string();
self.dirty_windowing();
@@ -2319,181 +2340,6 @@ impl WindowManager {
self.layout.overlay_position = "right".to_string();
self.dirty_windowing();
}
- Action::Expose => {
- if self.mode == WindowManagerMode::Overview {
- let mut viewport_w = 1920.0;
- let mut viewport_h = 1080.0;
- let mut phys_x = 0.0;
- let mut phys_y = 0.0;
- let mut output_found = false;
-
- if let Some(seat) = self.first_seat() {
- let lx = (*seat).cursor.x();
- let ly = (*seat).cursor.y();
- let wlr_output = (*self.server).om.output_at(lx, ly);
- if !wlr_output.is_null() {
- let mut output_box = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
- ffi::wlr_output_layout_get_box((*self.server).om.output_layout, wlr_output, &mut output_box);
- viewport_w = output_box.width as f64;
- viewport_h = output_box.height as f64;
- phys_x = output_box.x as f64;
- phys_y = output_box.y as f64;
- output_found = true;
- }
- }
-
- if !output_found {
- 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;
- phys_x = wlr_box.x as f64;
- phys_y = wlr_box.y as f64;
- break;
- }
- curr_out = (*curr_out).next;
- }
- }
-
- if let Some(seat) = self.first_seat() {
- let lx = (*seat).cursor.x();
- let ly = (*seat).cursor.y();
-
- let mut hovered_win: *mut crate::window::Window = std::ptr::null_mut();
- 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).is_status_bar() && !(*hovered_win).is_wallpaper() {
- (*seat).focus(crate::seat::Focus::Window(hovered_win));
- if !(*seat).object.is_null() && !(*hovered_win).object.is_null() {
- ffi::wl_resource_post_event((*seat).object, 4, (*hovered_win).object);
- }
-
- let win_w = if (*hovered_win).box_geom.width > 0 { (*hovered_win).box_geom.width as f64 } else { 800.0 };
- let win_h = if (*hovered_win).box_geom.height > 0 { (*hovered_win).box_geom.height as f64 } else { 600.0 };
- let center_x = (*hovered_win).virtual_x + win_w / 2.0;
- let center_y = (*hovered_win).virtual_y + win_h / 2.0;
- let cam = crate::policy::camera::center_on(center_x, center_y, viewport_w, viewport_h, 1.0);
- self.desk_zoom = cam.zoom;
- self.mode = WindowManagerMode::Normal;
- self.desk_pan_x = cam.pan_x;
- self.desk_pan_y = cam.pan_y;
- self.stop_panning_animation();
- if matches!(self.state, WindowManagerState::Idle) {
- self.update_viewport_local();
- } else {
- self.dirty_windowing();
- }
- return;
- } else {
- let vx = self.desk_pan_x + (lx - phys_x) / self.desk_zoom;
- let vy = self.desk_pan_y + (ly - phys_y) / self.desk_zoom;
- let cam = crate::policy::camera::center_on(vx, vy, viewport_w, viewport_h, 1.0);
- self.desk_zoom = cam.zoom;
- self.mode = WindowManagerMode::Normal;
- self.desk_pan_x = cam.pan_x;
- self.desk_pan_y = cam.pan_y;
- self.stop_panning_animation();
- if matches!(self.state, WindowManagerState::Idle) {
- self.update_viewport_local();
- } else {
- self.dirty_windowing();
- }
- return;
- }
- }
-
- self.desk_zoom = 1.0;
- self.mode = WindowManagerMode::Normal;
- self.desk_pan_x = 0.0;
- self.desk_pan_y = 0.0;
- self.stop_panning_animation();
- if matches!(self.state, WindowManagerState::Idle) {
- self.update_viewport_local();
- } else {
- self.dirty_windowing();
- }
- } else {
- 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().map_or(false, |id| id.starts_with("cce-status"));
- let is_wallpaper = app_id.as_deref() == Some("cce-wallpaper");
- if is_status_bar || is_wallpaper {
- 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::Overlay {
- 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 cam = crate::policy::camera::fit_bounds(min_vx, min_vy, max_vx, max_vy, viewport_w, viewport_h);
- self.desk_zoom = cam.zoom;
- // Overview by fiat even when the fit lands at zoom 1
- // (a desktop smaller than the screen): the next
- // Expose must exit, not re-enter.
- self.mode = WindowManagerMode::Overview;
- self.desk_pan_x = cam.pan_x;
- self.desk_pan_y = cam.pan_y;
- if matches!(self.state, WindowManagerState::Idle) {
- self.update_viewport_local();
- } else {
- self.dirty_windowing();
- }
- }
- }
- }
_ => {}
}
}
@@ -4006,6 +3852,65 @@ pub(crate) unsafe extern "C" fn handle_viewport_settle_tick(data: *mut std::ffi:
0
}
+/// The mechanism half of the trait boundary: apply one policy `Command`
+/// against the scene/seat world. Command order matters — the apply loop is a
+/// flat sequence, mirroring the arrange-plan convention.
+impl crate::policy::api::Compositor for WindowManager {
+ fn apply(&mut self, cmd: &crate::policy::api::Command) {
+ use crate::policy::api::Command;
+ unsafe {
+ match *cmd {
+ Command::SetCamera { camera, overview } => {
+ self.desk_pan_x = camera.pan_x;
+ self.desk_pan_y = camera.pan_y;
+ self.desk_zoom = camera.zoom;
+ if let Some(overview) = overview {
+ self.mode = if overview { WindowManagerMode::Overview } else { WindowManagerMode::Normal };
+ }
+ }
+ Command::PanTo { x, y } => {
+ if let Some(x) = x {
+ self.target_desk_pan_x = Some(x);
+ }
+ if let Some(y) = y {
+ self.target_desk_pan_y = Some(y);
+ }
+ self.start_panning_animation();
+ }
+ Command::StopPanAnimation => self.stop_panning_animation(),
+ Command::Focus(id) => {
+ if let Some(&win) = self.windows.get(id.0) {
+ if !win.is_null() && !(*win).closed {
+ if let Some(seat) = self.first_seat() {
+ (*seat).focus(crate::seat::Focus::Window(win));
+ if !(*seat).object.is_null() && !(*win).object.is_null() {
+ ffi::wl_resource_post_event((*seat).object, 4, (*win).object);
+ }
+ }
+ }
+ }
+ }
+ Command::MoveWindow { id, x, y } => {
+ if let Some(&win) = self.windows.get(id.0) {
+ if !win.is_null() && !(*win).closed {
+ (*win).virtual_x = x;
+ (*win).virtual_y = y;
+ }
+ }
+ }
+ Command::Relayout => self.dirty_windowing(),
+ Command::RefreshCamera => {
+ if matches!(self.state, WindowManagerState::Idle) {
+ self.update_viewport_local();
+ } else {
+ self.dirty_windowing();
+ }
+ }
+ }
+ }
+ }
+}
+
/// 16ms edge auto-pan tick: scroll the desktop by the current velocity, then
/// re-run the seat op at its last cursor position so the dragged window keeps
/// tracking the (pinned) cursor — the op's pan-delta term turns the scroll