Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
refactor: route all camera math through policy::camera
The six inline pan/zoom computations move onto the policy crate's new
camera module: keyed ZoomIn/Out/Reset (zoom_about_anchor at the
viewport center + keyed_zoom steps), wheel zoom in handle_axis
(wheel_zoom + zoom_about_anchor at the cursor), View1-4 jumps
(center_on), Expose exit (center_on at zoom 1 for both the
hovered-window and cursor-point paths), Expose enter (fit_bounds; mode
stays Overview by fiat even when the fit lands at zoom 1, so the next
Expose exits), and the focus-follow pan in seat.rs (visible_fraction +
FOCUS_VISIBLE_THRESHOLD + center_on). WindowManager::camera() is the
plain-data snapshot helper. One deliberate correction: focus-follow now
converts the window's screen-px box to virtual units (fw/zoom) for both
the visibility test and the center target — identical at zoom 1, where
the old code's mixed units happened to cancel.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/cursor.rs | 19 +++++++----
src/server/seat.rs | 58 ++++++++++++++-----------------
src/server/window_manager.rs | 81 +++++++++++++++++++++++---------------------
3 files changed, 81 insertions(+), 77 deletions(-)
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index ade4be9..5ff6ca0 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1571,9 +1571,8 @@ unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std
if delta != 0.0 {
let wm = &mut (*seat.server).wm;
wm.stop_panning_animation();
- let zoom_factor = 1.005_f64.powf(-delta);
let old_zoom = wm.desk_zoom;
- let new_zoom = (old_zoom * zoom_factor).clamp(0.1, 10.0);
+ let new_zoom = crate::policy::camera::wheel_zoom(old_zoom, delta);
if new_zoom != old_zoom {
let cx = cursor.x();
let cy = cursor.y();
@@ -1585,10 +1584,18 @@ unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std
} else {
(0.0, 0.0)
};
- wm.desk_pan_x += (cx - phys_x) * (1.0 / old_zoom - 1.0 / new_zoom);
- wm.desk_pan_y += (cy - phys_y) * (1.0 / old_zoom - 1.0 / new_zoom);
- wm.desk_zoom = new_zoom;
- wm.mode = if (new_zoom - 1.0).abs() > 0.001 { crate::window_manager::WindowManagerMode::Overview } else { crate::window_manager::WindowManagerMode::Normal };
+ // Wheel zoom pivots about the cursor: the virtual point
+ // under it stays put on screen.
+ let cam = crate::policy::camera::zoom_about_anchor(
+ wm.camera(),
+ cx - phys_x,
+ cy - phys_y,
+ new_zoom,
+ );
+ wm.desk_pan_x = cam.pan_x;
+ wm.desk_pan_y = cam.pan_y;
+ wm.desk_zoom = cam.zoom;
+ wm.mode = if crate::policy::camera::is_overview(cam.zoom) { crate::window_manager::WindowManagerMode::Overview } else { crate::window_manager::WindowManagerMode::Normal };
if matches!(wm.state, crate::window_manager::WindowManagerState::Idle) {
wm.update_viewport_local();
} else {
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 1b4bc5c..d037139 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -493,39 +493,31 @@ impl Seat {
};
let wm = &mut (*self.server).wm;
- let scale = wm.desk_zoom;
-
- // Calculate window visibility percentage in viewport
- let v_w = viewport_w / scale;
- let v_h = viewport_h / scale;
- let v_left = wm.desk_pan_x;
- let v_right = v_left + v_w;
- let v_top = wm.desk_pan_y;
- let v_bottom = v_top + v_h;
-
- let w_left = (*window).virtual_x;
- let w_top = (*window).virtual_y;
- let w_right = w_left + fw;
- let w_bottom = w_top + fh;
-
- let i_left = w_left.max(v_left);
- let i_right = w_right.min(v_right);
- let i_top = w_top.max(v_top);
- let i_bottom = w_bottom.min(v_bottom);
-
- let i_w = (i_right - i_left).max(0.0);
- let i_h = (i_bottom - i_top).max(0.0);
- let i_area = i_w * i_h;
- let w_area = fw * fh;
-
- let visible_percent = if w_area > 0.0 { i_area / w_area } else { 0.0 };
-
- if visible_percent < 0.75 {
- let target_x = (*window).virtual_x + (fw / 2.0 - viewport_w / 2.0) / scale;
- let target_y = (*window).virtual_y + (fh / 2.0 - viewport_h / 2.0) / scale;
-
- wm.target_desk_pan_x = Some(target_x);
- wm.target_desk_pan_y = Some(target_y);
+ let cam = wm.camera();
+ // fw/fh are screen px; the window's virtual
+ // footprint is that over zoom.
+ let vw_w = fw / cam.zoom;
+ let vw_h = fh / cam.zoom;
+ let visible = crate::policy::camera::visible_fraction(
+ (*window).virtual_x,
+ (*window).virtual_y,
+ vw_w,
+ vw_h,
+ cam,
+ viewport_w,
+ viewport_h,
+ );
+
+ if visible < crate::policy::camera::FOCUS_VISIBLE_THRESHOLD {
+ let target = crate::policy::camera::center_on(
+ (*window).virtual_x + vw_w / 2.0,
+ (*window).virtual_y + vw_h / 2.0,
+ viewport_w,
+ viewport_h,
+ cam.zoom,
+ );
+ wm.target_desk_pan_x = Some(target.pan_x);
+ wm.target_desk_pan_y = Some(target.pan_y);
wm.start_panning_animation();
}
}
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index c590a44..bc441e6 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -664,6 +664,15 @@ impl WindowManager {
self.target_desk_pan_y = None;
}
+ /// The current camera as the policy crate's plain-data snapshot.
+ pub fn camera(&self) -> crate::policy::camera::Camera {
+ crate::policy::camera::Camera {
+ pan_x: self.desk_pan_x,
+ pan_y: self.desk_pan_y,
+ zoom: self.desk_zoom,
+ }
+ }
+
/// 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.
@@ -2221,8 +2230,9 @@ impl WindowManager {
curr_out = (*curr_out).next;
}
- self.desk_pan_x = target_x - (viewport_w / 2.0) / self.desk_zoom;
- self.desk_pan_y = target_y - (viewport_h / 2.0) / self.desk_zoom;
+ 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 => {
@@ -2258,20 +2268,23 @@ impl WindowManager {
curr_out = (*curr_out).next;
}
- let cx = self.desk_pan_x + (viewport_w / 2.0) / self.desk_zoom;
- let cy = self.desk_pan_y + (viewport_h / 2.0) / self.desk_zoom;
-
- let new_zoom = match action {
- Action::ZoomIn => (self.desk_zoom * 1.1).min(10.0),
- Action::ZoomOut => (self.desk_zoom / 1.1).max(0.1),
- Action::ZoomReset => 1.0,
- _ => self.desk_zoom,
+ // Keyed zooms pivot about the viewport center.
+ let dir = match action {
+ Action::ZoomIn => 1.0,
+ Action::ZoomOut => -1.0,
+ _ => 0.0,
};
-
- self.desk_pan_x = cx - (viewport_w / 2.0) / new_zoom;
- self.desk_pan_y = cy - (viewport_h / 2.0) / new_zoom;
- self.desk_zoom = new_zoom;
- self.mode = if (new_zoom - 1.0).abs() > 0.001 { WindowManagerMode::Overview } else { WindowManagerMode::Normal };
+ 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 => {
@@ -2367,10 +2380,11 @@ impl WindowManager {
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;
- self.desk_zoom = 1.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 = center_x - viewport_w / 2.0;
- self.desk_pan_y = center_y - viewport_h / 2.0;
+ 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();
@@ -2381,10 +2395,11 @@ impl WindowManager {
} 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;
- self.desk_zoom = 1.0;
+ 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 = vx - viewport_w / 2.0;
- self.desk_pan_y = vy - viewport_h / 2.0;
+ 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();
@@ -2463,24 +2478,14 @@ impl WindowManager {
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;
+ 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 = center_x - (viewport_w / 2.0) / new_zoom;
- self.desk_pan_y = center_y - (viewport_h / 2.0) / new_zoom;
+ 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 {