git.lucas.co / cce-window-manager
window management library
git clone https://git.lucas.co/cce-window-manager.git

commita43b0b66ec1c550e0e554c9c500346fe862687d0
parentecad2ac902
authorLucas Galante <[email protected]>
date2026-08-20 10:04
camera: add pinch_zoom for continuous touchpad pinch

libinput reports pinch `scale` as absolute finger spread relative to the
gesture's begin, not a per-event delta — so the map is start_zoom * scale
off the zoom captured at pinch begin, clamped to the usual bounds.
Mapping off the *current* zoom would compound every update into runaway
growth. Consumed by the compositor's pinch handlers to zoom the desktop
camera when the gesture starts over the background.

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

 src/camera.rs | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/camera.rs b/src/camera.rs
index b57b843..117e977 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -47,6 +47,14 @@ pub fn wheel_zoom(zoom: f64, delta: f64) -> f64 {
     (zoom * WHEEL_ZOOM_BASE.powf(-delta)).clamp(ZOOM_MIN, ZOOM_MAX)
 }
 
+/// Continuous pinch zoom. libinput reports `scale` as the absolute finger
+/// spread relative to the gesture's begin (not a per-event delta), so the
+/// whole gesture maps off the zoom captured at pinch begin — never the
+/// current zoom, which would compound every update into runaway growth.
+pub fn pinch_zoom(start_zoom: f64, scale: f64) -> f64 {
+    (start_zoom * scale).clamp(ZOOM_MIN, ZOOM_MAX)
+}
+
 /// Change zoom while keeping the virtual point under an output-local anchor
 /// (`ax`, `ay` px from the output's top-left) fixed on screen — the wheel
 /// zooms about the cursor, keyed zooms about the viewport center.
@@ -239,6 +247,18 @@ mod tests {
         assert_eq!(keyed_zoom(3.7, 0.0), 1.0);
     }
 
+    #[test]
+    fn pinch_zoom_maps_off_the_begin_zoom_and_clamps() {
+        // Absolute-scale semantics: spreading to 2x from zoom 1.5 lands on
+        // 3.0 no matter how many intermediate updates arrived.
+        assert_eq!(pinch_zoom(1.5, 2.0), 3.0);
+        assert_eq!(pinch_zoom(1.5, 1.0), 1.5); // begin-scale identity
+        assert_eq!(pinch_zoom(1.0, 0.5), 0.5);
+        // Clamped at both ends.
+        assert_eq!(pinch_zoom(8.0, 4.0), ZOOM_MAX);
+        assert_eq!(pinch_zoom(0.4, 0.1), ZOOM_MIN);
+    }
+
     #[test]
     fn zoom_about_anchor_pins_the_anchored_point() {
         // Virtual point under the anchor before == after. Anchor (960, 540),