Let a trackpad swipe move the strip under the pointer, with a setting for the focused strip#212

opened9d agobySSamanyu Das

Note

🤖 Claude Opus 5.5 responding on behalf of Sam

Draft type: feature. Source: the doc audit in PR #209, which found that this accepted v1 behavior has no issue. Release scope: v1, as Multiple monitors records.

What to build

Trackpad scrolling moves the strip on the display under the pointer by default. A setting lets the user choose the focused window's strip instead. Keyboard commands keep acting on the focused strip.

Current behavior

From the engine code at 589b44b, swipe_gesture in src/ecs/scroll.rs moves the strip on the display whose menu bar is active. Nobody has checked this on hardware. Settings has no control for the target, and MiriSettings has no key for it.

Acceptance criteria

  • With two displays, a swipe moves the strip on the display under the pointer, and the other strip keeps its focus and scroll position.
  • Settings > Gestures offers the focused-strip target, and Miri writes it to the configuration file.
  • With the focused-strip target, a swipe moves the focused window's strip wherever the pointer is.
  • Each display's strip still moves independently.
  • Update decisions/configuration.md, docs/limitations.md, and check S-1 in docs/acceptance-ledger.md.

Verification

Engine tests cover both targets with two displays. Sam checks both targets with a real trackpad, which is acceptance check S-1.

Note

🤖 Claude Opus 5.5 responding on behalf of Sam

Triage on main at ffefedfa, engine a20f56e. The issue's description of the current behavior holds in the code. One acceptance criterion is ambiguous, so I added needs-info with one question for Sam below.

Root cause

A swipe can only ever move one strip, the strip that ActiveWorkspaceMarker marks, and that marker follows the menu bar display, not the pointer. I read this from the code. I did not observe it on hardware, and I could not run an engine probe test, because the Mac's data volume had 196 MB free.

  1. The event tap drops the pointer location. Event::Swipe carries only delta, fingers, and distance_cm (input.rs#L734). Event::Scroll, the modifier-wheel path, carries only delta (input.rs#L652).
  2. swipe_gesture takes Single<SwipedStrip, With<ActiveWorkspaceMarker>> and ActiveDisplay (scroll.rs#L329). Both swipes and modifier-wheel scrolls go through it.
  3. ActiveWorkspaceMarker is on exactly one strip across all displays (workspace.rs#L537).
  4. ActiveDisplayMarker moves only on the activeDisplayDidChange: notification (display.rs#L443), and active_display_id is the menu bar display (manager.rs#L140). With focus_follows_mouse off, the v1 default, moving the pointer does not move the menu bar.

So with focus on display A and the pointer on display B, a swipe moves A's strip. The fault is the target choice, not one line.

What the change touches

The whole swipe pipeline assumes the swiped strip is the active strip on the active display. Each of these reads ActiveWorkspaceMarker or ActiveDisplay:

  • swiping_timeout, settle_strip, apply_snap_force, scrolling_integrator, and apply_scrolling_constraints in src/ecs/scroll.rs. They take the viewport width from the active display, which is wrong for a strip on a display of another size.
  • finish_settle_on_deactivate in src/ecs/scroll.rs lands a settle at once when the strip loses the active marker.
  • window_frame_update in src/ecs/systems.rs checks for a running swipe on the active strip only.

My proposed shape, which the builder can change, is to choose the target strip once, on TouchpadDown or the first wheel tick, and mark that strip with a component. The pointer target uses the strip for the current Space of the display under the pointer. The focused target uses ActiveWorkspaceMarker, as today. The swipe systems then read the marked strip and its parent display, not the active strip and active display. The tap can read the pointer from the gesture event's location, and the mock cursor_position lets engine tests place it.

Question for Sam

After a swipe ends, settle_strip focuses the last-focused window in the column it lands on (scroll.rs#L718). With the pointer target, that moves keyboard focus and the menu bar to the swiped display. Criterion 1 says the other strip "keeps its focus", which can mean either option below.

  • Option 1 (recommended). The swipe focuses a window on the swiped display, the same way a swipe on one display focuses its landing column today. The other strip keeps its scroll position and its last-focused column, so focusing back returns to the same window. I recommend this because it keeps one swipe behavior on every display.
  • Option 2. A swipe on a display without focus only scrolls, and focus stays where it was. The swiped strip then shows a column whose window does not have focus.

Proposed wording for criterion 1 under option 1: "With two displays, a swipe moves the strip on the display under the pointer and focuses its landing column. The other strip keeps its scroll position and its last-focused column."

Defaults I would apply unless Sam says otherwise

  • The target setting covers modifier-wheel scrolling too, because both inputs share swipe_gesture and release scope gives them the same settle.
  • Keyboard commands keep acting on the focused strip, as the issue says.

How each criterion gets checked

CriterionCheck
1, 3, 4Engine tests with two displays and a mock pointer, then Sam's real-trackpad check S-1. Synthetic swipes do not act like real fingers, so an agent cannot drive these.
2verify-miri opens Settings > Gestures, picks the focused-strip target, and reads the key back from config.toml.
5Docs review in the PR.

Note

🤖 T3 Tots responding on behalf of Sam

Scope decision (v1 readiness audit, 6 Nov 2026): keep #212 in v1. Labelled v1 blocker. The focus-after-swipe choice from triage (option 1 or 2) is still open.