Files
OneCAD-Draft/docs/trackpad.md
T
Alexey Bagno 279d77a950 docs(trackpad): document the gestures and why a pinch bypasses iced
The pinch is read from AppKit rather than through iced, which is the kind of
decision that reads as arbitrary without the reason next to it. The file also
records what the pixel/notch split costs: a smooth-scrolling mouse pans with a
trackpad.
2026-09-21 23:47:32 +03:00

3.6 KiB

Trackpad

The macOS desktop app reads trackpad gestures directly: two fingers pan, a pinch zooms. The web build and the Linux desktop build keep zooming on every scroll delta and have no pinch; the reasons are in Implementation below.

A mouse is unaffected. The wheel zooms, the middle button pans, and Shift together with the middle button orbits, exactly as before.

Navigation

Gesture Behavior
Two-finger scroll Pan the active view, both axes, the drawing following the fingers.
Pinch Zoom about the cursor, at the scale of the fingers' own spread.
Wheel (mouse) Zoom about the cursor. ZOOMFACTOR scales a notch; ZOOMWHEEL reverses it.
Middle-button drag Pan. Shift together with the middle button orbits the model view.
PAN / P Pan with the left button until Escape.

Panning covers the same ground whichever way it starts: the active model tile, a paper sheet, or the model view inside a floating viewport in MSPACE. A pan dragged out of a two-finger gesture and one dragged with the middle button move through the same code, so they agree on speed, on the tile they pan by, and on keeping an open selection box attached to the drawing underneath.

A pinch is not affected by ZOOMWHEEL. The direction of a pinch is the direction of the fingers, not a convention about a device.

On macOS a mouse that scrolls smoothly, such as a Magic Mouse, reports precise pixel deltas like a trackpad does, so it pans instead of zooming. That follows from the split described below; it is not a separate setting.

Implementation

  • src/input/trackpad.rs: the pinch. iced's winit layer converts only the window events that have an iced equivalent, so winit's PinchGesture is dropped before the app sees it. The gesture is read from AppKit with a local NSEvent monitor instead, installed on the main thread where iced builds subscriptions. The monitor's block runs on the main thread during AppKit's own dispatch and only publishes the magnification into a sink; the app receives it through an iced subscription shaped like the SpaceMouse bridge (src/input/spacemouse/). The monitor token is never released, which does not uninstall the monitor — removeMonitor does, and the app has one window for its whole life.
  • src/app/update/viewport.rs: scroll_intent decides what a scroll delta means. A wheel reports notches and zooms; a precise-scrolling device reports pixels and pans. There is no modifier and no setting in that decision. pan_active_view and zoom_view_at_cursor are the bodies shared by the mouse and the gestures, and they apply a pinch as 1 - step / 10 = 1 / (1 + m) so a pinch of m leaves the view exactly 1 + m times closer.
  • src/app/mod.rs carries one Message::TrackpadPinch(f32) per gesture event.

Pixel deltas pan only where they can only mean a trackpad. The web build receives every wheel notch as a pixel delta from the browser, so there — and on Linux, where a smooth-scrolling mouse reports them the same way — they keep zooming. A pinch exists nowhere else either: winit reports PinchGesture on macOS and iOS only, its PanGesture is iOS-only, and its Wayland backend does not bind the pointer-gesture protocol at all.

Validation

cargo test --locked --lib scroll_intent

The tests cover the decision between a notch and a pixel, and the 1:1 mapping of a pinch onto the camera. The gestures themselves are checked by hand: macOS offers no way to synthesize a magnify event, so a pinch cannot be driven by a test. Two-finger pan and pinch are exercised in model space, in a tiled view, on a paper sheet, and inside a floating viewport in MSPACE.