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

commitea34e06229ded6287e17d23d662386a8fcd0bf96
parent003171d8c5
authorLucas Galante <[email protected]>
date2026-07-28 12:19
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 {