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

commite58f32e352758b0011c745078665a3c5bfb2fe12
parentab99cf0b17
authorLucas Galante <[email protected]>
date2026-09-12 09:19
feat(camera): focus-follow takes the minimal pan at every distance

Both sites that panned for a focus change carried the same two-branch rule:
centre the window when less than three quarters of it was visible, nudge it
in when it was merely clipped. Both now call policy::camera::pan_into_view,
which is that nudge generalized to any distance (cce-window-manager
8f7f4a3 has the reasoning).

So focusing the window immediately to the right slides the desktop just far
enough to show it whole with a margin, instead of swinging it into the
middle of the screen and taking every other window with it.

The second site is pan_to_virtual_rect, which pans for restore
placeholders; its doc already said it mirrors what windows get, and sharing
one policy call is what keeps that true.

Measured in a headless session, an 800-wide window entirely off the right
edge of a 1280 viewport at vx=1876: before, pan_x 1636, the centre-on value
exactly, window at screen x=240; after, pan_x 1420, the minimal value, window
at screen x=456 with its right edge 24px in. Re-focusing an already-visible
window moves the camera not at all, and focus-left onto a window off the
other edge lands it at screen x=24.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/server/seat.rs           | 33 +++++++--------------------------
 src/server/window_manager.rs | 20 ++++----------------
 2 files changed, 11 insertions(+), 42 deletions(-)

diff --git a/src/server/seat.rs b/src/server/seat.rs
index bf25138..0d9e050 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1079,28 +1079,13 @@ impl Seat {
             // whenever zoom != 1 and mistargeted the pan.
             let vw_w = fw;
             let vw_h = fh;
-            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();
-            } else if let Some(target) = crate::policy::camera::nudge_into_view(
+            // The camera moves as little as the focus demands: enough to
+            // show the whole window with a margin, and no further. A window
+            // half off the edge and one a screen away take the same rule —
+            // `policy::camera::pan_into_view` carries the reasoning, and
+            // `WindowManager::pan_to_virtual_rect` applies it to the
+            // non-window rects (restore placeholders) from the same place.
+            if let Some(target) = crate::policy::camera::pan_into_view(
                 (*window).virtual_x,
                 (*window).virtual_y,
                 vw_w,
@@ -1109,10 +1094,6 @@ impl Seat {
                 viewport_w,
                 viewport_h,
             ) {
-                // Mostly visible but clipped: slide the
-                // clipped edge on-screen instead of
-                // recentering — focusing a window should
-                // never leave part of it hanging off.
                 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 d7946de..360522c 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -735,9 +735,9 @@ impl WindowManager {
         None
     }
 
-    /// Focus-follow camera rules for a bare virtual rect — the same
-    /// center-when-mostly-hidden / nudge-when-clipped behavior windows get,
-    /// for things that are not windows (restore placeholders).
+    /// Focus-follow camera rules for a bare virtual rect — the same minimal
+    /// pan-into-view windows get, for things that are not windows (restore
+    /// placeholders).
     pub unsafe fn pan_to_virtual_rect(&mut self, vx: f64, vy: f64, w: f64, h: f64) {
         let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
         let mut curr_out = (*outputs_list).next;
@@ -753,19 +753,7 @@ impl WindowManager {
         let Some(viewport) = viewport else { return };
         let (vw, vh) = (viewport.width as f64, viewport.height as f64);
         let cam = self.camera();
-        let visible = crate::policy::camera::visible_fraction(vx, vy, w, h, cam, vw, vh);
-        let target = if visible < crate::policy::camera::FOCUS_VISIBLE_THRESHOLD {
-            Some(crate::policy::camera::center_on(
-                vx + w / 2.0,
-                vy + h / 2.0,
-                vw,
-                vh,
-                cam.zoom,
-            ))
-        } else {
-            crate::policy::camera::nudge_into_view(vx, vy, w, h, cam, vw, vh)
-        };
-        if let Some(target) = target {
+        if let Some(target) = crate::policy::camera::pan_into_view(vx, vy, w, h, cam, vw, vh) {
             self.target_desk_pan_x = Some(target.pan_x);
             self.target_desk_pan_y = Some(target.pan_y);
             self.start_panning_animation();