Add a setting for window-by-window snapping instead of continuous scrolling#281

opened7d agobySSamanyu Das

Problem

Sam wants to navigate the strip one window at a time instead of scrolling continuously. The existing settling-after-a-swipe specification describes continuous movement followed by settling on a column when the gesture ends. That does not provide a setting for discrete window-by-window movement. This is a feature request, not a report of a reproduced defect.

Proposed behavior

Add a scrolling mode setting with two choices: Continuous and Window-by-window snapping. In snapping mode, scrolling advances through adjacent windows in discrete steps, with each step landing at a stable position that shows the destination window. For stacked windows that share one horizontal position, advance by column and preserve the existing rule for choosing the focused window within the stack. Keep continuous scrolling available and preserve the existing choice for current users. Persist the setting across app restarts.

Requested by

Sam, on 3 November 2026.

Release

v2, as requested by Sam on 3 November 2026. The current v1 scope says settling is always on and has no setting. For v2, update the scope documentation to distinguish end-of-gesture settling from the proposed scrolling mode.

Acceptance criteria

  • Settings exposes Continuous and Window-by-window snapping with a clear explanation of each mode.
  • In snapping mode, left and right scrolling move through adjacent windows or columns in discrete steps instead of leaving the strip at arbitrary intermediate offsets.
  • Trackpad scrolling and modifier-scroll with a mouse wheel both honor the setting.
  • Gesture momentum does not turn a single intended step into an uncontrolled sequence of window changes.
  • At either end of the strip, snapping stops at a valid position without leaving empty space beside an overflowing strip.
  • Mixed window widths and stacked windows land at valid positions and follow the existing focus rules.
  • Each display and native Space retains its independent strip position.
  • Switching back to Continuous restores continuous scrolling with its existing settling behavior.
  • The selected mode survives restarting Miri.

Verification

Verify both modes in the running app with a real trackpad and mouse wheel. Check slow movement, quick flicks, direction reversals, mixed window widths, stacks, strip endpoints, and two displays. Confirm that a deliberate step feels predictable at normal interaction speed.

#212 controls which strip receives a swipe. This request controls how that strip moves.

Note

🤖 Claude Opus 5.5 responding on behalf of samanyudas

Triage result: the engine already has both snapping modes, so this issue is mostly a Settings control, docs, and checks. I added needs-info, because criterion 4 has no measurable limit and three product choices are open. No code changed.

Why the strip scrolls continuously today

The pinned engine (a20f56e, on main at ffefedfa) has two step modes, and both are off by default:

  • [swipe.gesture] paging = true turns a strip swipe into focus steps, one adjacent window per page_distance_cm of finger travel (default 2 cm). See paging_gesture and GestureOptions.
  • [swipe.scroll] window_step = true turns each modifier-scroll wheel input into one focus step. See handle_scroll_wheel.

Upstream Paneru added these modes in d7080ec on 16 October 2026. Miri's Settings writes fingers_count, direction, and vertical in [swipe.gesture], but never paging or window_step (Settings.swift). So the engine defaults apply, and the strip scrolls freely, then settles.

What each criterion needs

Evidence types differ below. "CI" means the engine unit tests passed in the Engine tests on macOS 27 job of CI run 42179065759 on ffefedfa. "Code" means I read the code and did not run it. Nothing below was checked in the running app.

CriterionState in the engineWork left
1. Settings shows both modesNot built.A Settings control that writes both keys, plus rows in decisions/configuration.md.
2. Discrete stepsBuilt. Each step is the keyboard focus command, so the strip moves only as far as needed to show the column. CI: paging_waits_for_distance_then_focuses_without_free_scrolling.Live check.
3. Trackpad and wheelBuilt as two separate keys. CI: 14 paging tests and 5 wheel tests passed.One Miri setting must write both keys.
4. MomentumWheel: momentum events never step, and same-direction repeats within 180 ms are dropped. CI: wheel_momentum_does_not_select_a_window_or_delay_the_next_input. Trackpad: steps come from finger travel only, so momentum cannot add steps. But a long swipe earns one step per 2 cm, and those steps still play out after the fingers lift. CI: paging_fast_swipe_keeps_each_step_and_signed_remainder.A measurable limit. See question 1.
5. Strip endsBuilt. Focus stops at the end column. CI: paging_respects_reversed_direction_and_strip_edges, wheel_steps_focus_adjacent_windows_and_stop_at_strip_edges.Live check with an overflowing strip.
6. Mixed widths and stacksCode: steps call the same focus command as the keyboard, with focus history, so a stack follows the keyboard focus rule.Live check. AXFixture is fixed at 380 pt, so mixed widths need another window or a --width option, as noted in #305.
7. Per display and SpaceCode: steps move focus, so each strip keeps its own offset. A step acts on the focused window's strip, the same as a swipe today. #212 covers the strip under the pointer.Live check on two displays.
8. Switching backCode: a config change resets the paging state.Live check.
9. Survives restartThe setting lives in the config file.Swift test plus a relaunch check.

Agents cannot send real trackpad gestures, so the trackpad parts of criteria 2 to 8 and the "feels predictable" check are hand checks for Sam. The wheel parts can run with synthetic modifier-scroll events.

Open questions for Sam

  1. Long swipes. Today a 10 cm swipe moves 5 windows. Should snapping keep one step per 2 cm of travel, or allow exactly one step per gesture? My recommendation is one step per 2 cm, because it matches the engine and lets a long swipe travel far. Criterion 4 would then read: "A trackpad step needs 2 cm of finger travel, and travel after the fingers lift adds no step. Wheel momentum never adds a step."
  2. Haptics. The engine pulses the trackpad on each step by default (haptics). Should Miri keep that on with no setting, turn it off, or add a setting? My recommendation is on, with no setting.
  3. Step distance. Should page_distance_cm stay fixed at 2 cm, or be a setting? My recommendation is fixed at 2 cm for now.

Release: the work left is small, but Sam set v2, and decisions/release-scope.md says settling has no setting in v1. I did not change the scope.