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

commitc18b6bafff28124bee84c6742dbbe94eea070a1a
parent3e66bd158b
authorLucas Galante <[email protected]>
date2026-08-04 20:23
feat: eased camera for animated overview transitions

SetCamera { animate: true } (the Expose enter/exit paths) now routes
through the pan-animation targets plus a new target_desk_zoom, eased on
the same 16ms tick. Zoom steps geometrically (exponential approach in
log space) — a linear step would leap multiple-x per frame at the small
end of an overview exit, while a constant per-frame ratio reads as
uniform motion. Instant SetCamera writes now also cancel any easing in
flight so stale targets can't drag the camera back.

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

 src/server/window_manager.rs | 49 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 7 deletions(-)

diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 99bc071..8651966 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -121,6 +121,9 @@ pub struct WindowManager {
     pub shutting_down: bool,
     pub target_desk_pan_x: Option<f64>,
     pub target_desk_pan_y: Option<f64>,
+    /// Eased alongside the pan targets by the animation tick — the overview
+    /// enter/exit transition (`SetCamera { animate: true }`).
+    pub target_desk_zoom: Option<f64>,
     pub animation_timer: *mut ffi::wl_event_source,
     /// Edge auto-pan velocity during an interactive move/resize, in SCREEN
     /// px/s (the tick divides by zoom). Written by `Seat::update_edge_pan`
@@ -205,6 +208,7 @@ impl WindowManager {
         self.desk_pan_y = 0.0;
         self.target_desk_pan_x = None;
         self.target_desk_pan_y = None;
+        self.target_desk_zoom = None;
         self.animation_timer = std::ptr::null_mut();
         self.edge_pan_vx = 0.0;
         self.edge_pan_vy = 0.0;
@@ -838,6 +842,7 @@ impl WindowManager {
     pub unsafe fn stop_panning_animation(&mut self) {
         self.target_desk_pan_x = None;
         self.target_desk_pan_y = None;
+        self.target_desk_zoom = None;
     }
 
     /// The current camera as the policy crate's plain-data snapshot.
@@ -1009,9 +1014,10 @@ impl WindowManager {
         }
     }
 
-    /// Arm the pan animation timer (creating it on first use): every 16ms
+    /// Arm the camera animation timer (creating it on first use): every 16ms
     /// `handle_panning_animation_tick` eases `desk_pan_x/y` toward
-    /// `target_desk_pan_x/y`. Callers set the targets first.
+    /// `target_desk_pan_x/y` and `desk_zoom` toward `target_desk_zoom` (the
+    /// overview enter/exit transition). Callers set the targets first.
     pub unsafe fn start_panning_animation(&mut self) {
         if self.animation_timer.is_null() {
             let event_loop = ffi::wl_display_get_event_loop((*self.server).wl_server);
@@ -4018,13 +4024,27 @@ impl crate::policy::api::Compositor for WindowManager {
                 Command::Spawn(ref cmdline) => {
                     self.execute_action(&crate::config::Action::Spawn, Some(cmdline));
                 }
-                Command::SetCamera { camera, overview } => {
-                    self.desk_pan_x = camera.pan_x;
-                    self.desk_pan_y = camera.pan_y;
-                    self.desk_zoom = camera.zoom;
+                Command::SetCamera { camera, overview, animate } => {
+                    // The mode flips immediately either way, so a re-toggle
+                    // mid-flight exits/enters rather than re-entering.
                     if let Some(overview) = overview {
                         self.mode = if overview { WindowManagerMode::Overview } else { WindowManagerMode::Normal };
                     }
+                    if animate {
+                        self.target_desk_pan_x = Some(camera.pan_x);
+                        self.target_desk_pan_y = Some(camera.pan_y);
+                        self.target_desk_zoom = Some(camera.zoom);
+                        self.start_panning_animation();
+                    } else {
+                        self.desk_pan_x = camera.pan_x;
+                        self.desk_pan_y = camera.pan_y;
+                        self.desk_zoom = camera.zoom;
+                        // An instant write cancels any easing in flight — the
+                        // stale targets would otherwise drag the camera back.
+                        self.target_desk_pan_x = None;
+                        self.target_desk_pan_y = None;
+                        self.target_desk_zoom = None;
+                    }
                 }
                 Command::PanTo { x, y } => {
                     if let Some(x) = x {
@@ -4200,7 +4220,22 @@ pub(crate) unsafe extern "C" fn handle_panning_animation_tick(data: *mut std::ff
             (*wm).target_desk_pan_y = None;
         }
     }
-    
+
+    // Zoom eases geometrically (exponential approach in log space): a linear
+    // step would leap multiple-x per frame at the small end of an overview
+    // exit, while a constant per-frame RATIO reads as uniform motion.
+    if let Some(target_zoom) = (*wm).target_desk_zoom {
+        let cur = (*wm).desk_zoom.max(1e-6);
+        let log_delta = (target_zoom / cur).ln();
+        if log_delta.abs() > 0.001 {
+            (*wm).desk_zoom = cur * (log_delta * factor).exp();
+            done = false;
+        } else {
+            (*wm).desk_zoom = target_zoom;
+            (*wm).target_desk_zoom = None;
+        }
+    }
+
     if matches!((*wm).state, WindowManagerState::Idle) {
         (*wm).update_viewport_local();
     } else {