Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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 {