Files
PanosandClaude Opus 5 9fe2e4fa58 fix(view): hold ZOOMFACTOR to its own range, finish the exponential zoom
The exponential camera step is the right call. `0.9 ^ delta` is strictly
positive for every real delta, in and out cancel exactly where the
subtraction lost a percent of the view on every round trip, and the file's
own header has promised an "exponential feel" since it was written. Three
surfaces were left behind it, and the wider range exposed a fourth thing.

The range of the system variable. 3 to 100 is the range ZOOMFACTOR has, and
the User Preferences page was built against it; `SETVAR ZOOMFACTOR` accepts
that and nothing else again. A faster wheel is the Options window's business
instead: the slider covers the variable's own range, the field beside it
reaches 500, and a read-out says what a notch actually does ("10.0% a
notch") so the number being set is readable rather than implied. A value
above 100 therefore lives only in this application's own configuration file,
where it is nobody else's to read, and a bare `SETVAR ZOOMFACTOR` still
reports it. The bounds and the clamp live in one place now
(`settings::clamp_zoom_factor`): four literal copies of `3, 100` are what
let a surface be missed in the first place.

A floating viewport in MSPACE. It zooms its own view through
`zoom_active_viewport`, which kept subtracting: the factor reached zero at
6.67 steps, so from ZOOMFACTOR 360 up every notch inside a viewport bottomed
out on the clamp — a tenfold jump a notch, and the top third of the slider
doing nothing at all. Exponential there too, from a base of 0.85 so the
default notch still moves a viewport the 15% it always has. Each surface
keeps its own long-standing feel rather than inheriting the other's.

The trackpad pinch. `pinch_zoom_steps` was calibrated against the
subtraction (`1 - step / 10 = 1 / (1 + m)`) so that a pinch of m leaves the
view exactly 1 + m times closer — the property docs/trackpad.md documents
and the reason the gesture tracks the fingers at all. Inverting the zoom law
by hand is what let the two drift apart, so the step is asked of the camera
now (`Camera::zoom_steps_for_ratio`), which cannot drift by construction.
`pinch_is_one_to_one` recomputed the old formula rather than the camera's,
so it kept passing through all of this; it zooms a real camera and measures
the distance it lands at instead.

Also: `zoom` had a floor and no ceiling, and zooming out multiplies. At
ZOOMFACTOR 500 a notch out is x2.4, so about ninety notches — a couple of
seconds of scrolling — ran the distance past what an f32 far plane
(`distance * 1000`) can hold, and the viewport went black on an infinite
projection. A degenerate pinch magnification of -1 or beyond reached the
same place through the other side. Both are bounded now.

Also: two doc comments quoted the linear law (`zoom_view_at_cursor` and the
one on `pinch_zoom_steps`), and docs/trackpad.md stated the pinch identity
and claimed the tests covered it against the camera. All updated, with what
a notch does and where each surface's range comes from written down.

Tests, each confirmed against the behaviour it describes: the zoom law at
every factor the application can hold (positive, monotonic in the setting,
in and out cancelling), the ceiling under a long scroll out in both
projections, the published inverse and what it does with a nonsense ratio,
the viewport factor and a real viewport zoomed three notches at the top of
the range, the SETVAR range including a value the Options field set, the
Options field (above 100, above the ceiling, cleared, not a number), its
agreement with the slider, Close putting both the value and the field back,
and the read-out's percentages. 1882 lib tests pass.

The two new Options strings are not in the locale catalog yet, so they
render in English until it is regenerated.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-02 10:51:26 +03:00

4.8 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.

ZOOMFACTOR scales what one notch does. The system variable takes 3 to 100 — the range it has everywhere — and SETVAR ZOOMFACTOR accepts nothing outside it. Options → User Preferences reaches 500 for anyone who wants a faster wheel: the slider covers the variable's own range, the field beside it takes any value up to that ceiling, and both show what a notch does as a percentage. A value above 100 therefore lives only in this application's own configuration, and a bare SETVAR ZOOMFACTOR still reports it.

A notch multiplies the camera distance by 0.9 ^ (ZOOMFACTOR / 60), so the default 60 brings the view a tenth closer, a notch in and a notch out cancel exactly, and no setting can drive the distance to zero or through it. A floating viewport in MSPACE zooms its own view the same way, from a base of 0.85 — the 15% a notch has always moved a viewport.

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. A pinch of m leaves the view exactly 1 + m times closer: pinch_zoom_steps asks the camera for the step that applies that ratio (Camera::zoom_steps_for_ratio) instead of inverting the zoom law here, which is what let the two drift apart when the law stopped being a subtraction.
  • 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 mapping by zooming a real Camera and measuring the distance it ends at, so it is checked against the camera's own zoom law and not against a second copy of it. 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.