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

commitc37e0743d7e527e6db548c6c0b2b612e1e6e66da
parentf6bed9a0b2
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 14:24
fix(overview): launching an app keeps overview, panning only to show it

A world window mapping during overview flew the camera to zoom 1 on it
and left overview. It now keeps the zoom and pans just far enough to
show the whole window (pan_to_virtual_rect), whatever center_on_spawn
says, and not while an overview flight still owns the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/server/window.rs         | 17 +++++++++-------
 src/server/window_manager.rs | 46 +++++++++-----------------------------------
 2 files changed, 19 insertions(+), 44 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 76a6fafc..8a45ad9c 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2269,12 +2269,15 @@ impl Window {
                 }
             }
 
-            // A WORLD window spawning during overview pulls the session
-            // out of it, landing at zoom 1 on the new window — the user
-            // asked for it (launcher pick, spawn keybind). Chrome
-            // (Popup/Overlay), status, wallpaper and the grid spawn without
-            // disturbing the overview. Before the focus loop, so the
-            // focus-follow pan sees the settled zoom-1 camera and no-ops.
+            // A WORLD window spawning during overview stays in overview:
+            // the camera keeps its zoom and only pans, as little as it
+            // must, to show the whole new window. Until 2026-10-05 it
+            // flew out to zoom 1 on the window, so launching from the
+            // overview left it. Chrome (Popup/Overlay), status, wallpaper
+            // and the grid spawn without touching the camera. Here rather
+            // than left to the focus loop's focus-follow pan, which skips
+            // a first focus unless `center_on_spawn` allows it — the exit
+            // this replaced always moved the camera.
             if should_focus
                 && (*self.server).wm.mode == crate::window_manager::WindowManagerMode::Overview
                 && !self.is_grid()
@@ -2286,7 +2289,7 @@ impl Window {
                     resolved,
                     crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
                 ) {
-                    (*self.server).wm.exit_overview_to_window(self as *mut Window);
+                    (*self.server).wm.pan_overview_to_window(self as *mut Window);
                 }
             }
 
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index f8892d7d..dc519745 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3365,47 +3365,19 @@ impl WindowManager {
 
 
 
-    /// Snap the camera out of overview and onto `win`: zoom 1, centered,
-    /// mode Normal. The overview click-release path in cursor.rs does the
-    /// same dance inline (plus its focus/seat-event bookkeeping); this is
-    /// the map-time variant for windows SPAWNED during overview.
-    pub unsafe fn exit_overview_to_window(&mut self, win: *mut Window) {
-        if win.is_null() {
+    /// Pan the overview camera, at its current zoom, just far enough to
+    /// show all of `win` — the map-time treatment for a window SPAWNED
+    /// during overview, which leaves the mode alone. A camera ramp (an
+    /// overview enter still flying) owns the camera, and its mid-flight
+    /// sample would give a stale target, so the window is left to land
+    /// wherever the flight shows it.
+    pub unsafe fn pan_overview_to_window(&mut self, win: *mut Window) {
+        if win.is_null() || self.camera_ramp_anim.is_some() {
             return;
         }
-        let (mut viewport_w, mut viewport_h) = (1920.0_f64, 1080.0_f64);
-        let outputs_list = &(*self.server).om.outputs as *const 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 win_w = if (*win).box_geom.width > 0 { (*win).box_geom.width as f64 } else { 800.0 };
         let win_h = if (*win).box_geom.height > 0 { (*win).box_geom.height as f64 } else { 600.0 };
-        let center_x = (*win).virtual_x + win_w / 2.0;
-        let center_y = (*win).virtual_y + win_h / 2.0;
-        // Same animated flight as the Overview toggle's exit: SetCamera
-        // owns the ramp/target bookkeeping and flips the mode by fiat.
-        self.stop_panning_animation();
-        crate::policy::api::Compositor::apply(
-            self,
-            &crate::policy::api::Command::SetCamera {
-                camera: crate::policy::camera::Camera {
-                    pan_x: center_x - viewport_w / 2.0,
-                    pan_y: center_y - viewport_h / 2.0,
-                    zoom: 1.0,
-                },
-                overview: Some(false),
-                animate: true,
-            },
-        );
-        crate::policy::api::Compositor::apply(self, &crate::policy::api::Command::RefreshCamera);
+        self.pan_to_virtual_rect((*win).virtual_x, (*win).virtual_y, win_w, win_h);
     }
 
     /// Issue grid_patch events to grid clients whose current patch no