cmux-next: a divider drag follows the pointer in every frame; Shift lands it on a snap at the release

Merged
LA
3d ago
+394-69

Summary

A split divider drag held the divider on a snap (1/3, 1/2, 2/3 or its chain's equal share) while the pointer was within 8 pt of it, then jumped 8 pt to the pointer. A slow drag that started on an even split stood still for 91 to 99 ms at 120 Hz, then lurched (RESIZE KEYS films, #32988). A release with speed also landed on a snap the pointer never reached, and a Shift drag jumped from snap to snap while the pointer moved.

Now the divider is under the pointer in every frame, with no modifier and with Shift, as in Ghostty and Chrome (lead decision, STATUS 8923; the refactor owner's revised rule at 14:1xZ: no hold-then-jump in any mode).

  • No modifier: no soft snap and no fling snap. The divider stays where it is let go.
  • Shift: the drag follows the pointer. The release lands on the nearest snap (a split) or preset width (a column edge), at any distance, on the pane spring through the Motion gate: a split divider's from the pointer's speed (releasedDivider, SplitMotion.seed), a column edge's from rest; with animations off it lands in one frame. While Shift is held, the alignment haptic plays when the snap the release would land on changes. Letting go of Shift before the release lands under the pointer. An Option push (Option-Shift too) lands where the pointer is, as the modifier map says, and forgets the Shift landing.
  • Control, Option and Command are unchanged.
  • Both drag paths: the event path, and the frame-tick path of S2 (#33725, motion.frameOwner.timing; #33935 turns it on by default). The drag code applies one value per event or per frame either way; this change is in that value.

What's New: release-notes/next/highlights/divider-drag-follows-pointer.md.

Removed with the fling snap: DividerDragState.snapDistance / snapReach, Motion.dividerReleaseProjection and its Debug tunable motion.divider.releaseProjection (it had no other reader; schemas/defaults/cmux-next.json still names it until the base autofix regenerates it). Specs: the modifier map in plans/cmux-next/resize-modifiers.md (rows "none" and "Shift") and plans/cmux-next/motion.md. No user string changes (the Shift hint "⇧ Snap: halves and thirds only" is still true).

Supersedes #32988 (RESIZE KEYS, draft). Its DividerDragStartSnapTests case "a drag that starts on a snap follows the pointer from its first point" is kept in the new file, with Shift too, on both drag paths. Its two other cases (a no-modifier drag snaps back onto the half; snaps onto it from 4 pt away) test the soft snap this PR removes, so they are not kept.

Testing

New test file, committed before the fix: DividerDragFollowsPointerTests drives the real DividerHandleView (press, moves, release with and without Shift) on a split screen and a columns screen. It reads the divider's shown position after every move (after the next display frame on the frame-tick path), where each release lands, and every frame of Shift's landing spring with a manual 120 Hz tick. The follow and landing cases run with motion.frameOwner.timing pinned on and off per root (frameTimingOverride).

Command (red and green):

cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref <sha> \
  --arg=suite --arg=Packages/macOS/CmuxNext --arg=DividerDragFollowsPointerTests[,...]
  • Red at 3d6457c55bbe (the test, no fix): step a06fd383bc05cf16d11aac05 (aws-m4pro-2) FAILED as expected, 8 of 9 tests. Examples: a drag from the half by 3 pt showed the divider 1.5 and 3 pt from the pointer (held on the snap); a slow drag with Shift was up to 39 pt from the pointer; a fling released 25 pt short of the half landed on the half. lettingGoOfShiftBeforeTheReleaseLandsUnderThePointer already passed on the base.
  • Red for the frame-tick cases, at a scratch commit with feat 4ca639b0caed (which has S2) and the final test only (0beae67cedf3): step 9887ebab2e9b84a2fe5cc1f8 (aws-m4pro-7) FAILED as expected: every follow and landing case failed on both paths; lettingGoOfShiftBeforeTheReleaseLandsUnderThePointer passed.
  • Green at 3d6a0f480cff (the fix): step fffc56f5e25b0bd53ae6d4e2 (aws-m4pro-4) PASSED, 10 of 10 suites.
  • Green at b0d3c92c55d2 (merge of feat, MotionTunables.swift conflict with S2's switch resolved): step 7d5b8f228c6e14c9971ece22 (aws-m4pro-9) PASSED, 12 of 12 suites.
  • Green at 96b3ed228367 (the frame-tick cases): step 6e8092fe4db61d9573f8f069 (aws-m4pro-5) PASSED, 12 of 12 suites. GUI proof there: step 184d4184b7eace8a8a80aef8 (cmux10s, --display --label gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, 8 suites) PASSED.
  • Review round (Codex FIX and the Claude review at 96b3ed228367): an Option push kept the Shift landing, so Shift pressed again after a push did not play the alignment haptic; the follow assertion allowed 1 pt. Red at c04d1044d5dc (aPushForgetsTheSnapAShiftReleaseWouldLandOn, the follow check at half a device pixel, frame-tick moves must wait for the tick): step 97ea91b53548f81f83b2d49a (aws-m4pro) FAILED as expected, only the push case (snapped stayed 0.5).
  • Green at the head 6be522e66a87: step ce9cd59a361f8a35e96c16c1 (aws-m4pro-4) PASSED, 12 of 12 suites: the new one, every suite that drags a divider (DividerSnapTests, DividerModifierDragTests, DividerResizeMathTests, DragSendsOneIntentTests, PaneResizeGestureTests, DividerClickKeepsRatioTests, DividerCatcherMotionTests, DividerFocusLayoutTests, SplitSpringMotionTests) and S2's DividerDragFrameTickTests and MotionClockFrameTimeTests.
  • GUI proof at the head 6be522e66a87, on a display worker: step a45e35eddac8f6ec2afbfe0a (cmux14, --display --label gui-session-v1, CMUX_TEST_REQUIRE_GUI=1, 8 suites) PASSED: 8 of 8 suites, 0 skipped.

DividerSnapTests changed with the behavior: its snap cases are Shift releases now, and a new case checks that a release 4 pt from the half without Shift lands under the pointer.

Films (the refactor owner, pixel-perfect, STATUS 9858): A/B on cmux22, base FEAT DOGFOOD BUILD 8e149f2e (feat b08cc6f4d398) against the PR build 35cf71ad (3d6a0f480cff), px-col-drag-2 at x0.25, x0.5, x1, x2, x4 and Reduce Motion, 1 px border fixture. Verdict GREEN:

  • Drag-start lurch: base 2 at every speed, PR 0.
  • Largest divider step: base 8.5 pt, PR 1.5 to 2.5 pt.
  • Largest distance from the divider to the pointer: base 9.1 to 9.9 pt, PR 2.5 to 3.9 pt.
  • The PR's px-row-drag-3r film did not record (a ScreenCaptureKit start stall); it is a control (row edges have no snap).
  • The film build is 3d6a0f480cff. The head adds the merge of feat, tests, the push fix (a landing state only the haptic reads), docs and the highlight. Between this branch's base 0c5230241ae2 and the film base b08cc6f4d398 the drag code changed once, in #33725, behind motion.frameOwner.timing (off in both builds), so both arms drag on the same path.

TYPING UNDER LOAD (the refactor owner, typing-load-l6 on cmux18), key-to-commit ms, p50 / p95 / p99 / max:

  • base FEAT DOGFOOD BUILD 8e149f2e: 3.16 / 9.12 / 13.51 / 33.1
  • PR build 35cf71ad: 3.40 / 9.27 / 15.12 / 97.4
  • The p99 gap is inside today's noise band (10 runs: p99 13.39 to 16.91 ms). The PR changes only drag code, which the typing journey never runs, and the two builds also differ in their feat base.

Reviews: the Claude review subagent passed 96b3ed228367 (SHIP, minor notes: the column-edge landing starts from rest, a highlight was missing, the 1 pt tolerance); Codex (a different model) said FIX at 96b3ed228367 and SHIP at the head 6be522e66a87 (receipt in the PR comments).

Observed / Not run

Observed: the red, green and GUI steps above. scripts/cmux-next/prepush-gate.py and scripts/cmux-next/check-motion.sh pass.

Not run: no film of the head itself (see above); the haptic was not checked on hardware; macOS 14 and 15 were not run.

Area owner told: the Performance Lead (Motion gate: Motion.dividerReleaseProjection and its tunable are removed).

Changelog

Changed: A divider drag follows the pointer with no snap; hold Shift to land the divider on a half or a third when you let go

Proof

No capture: the change is in where a dragged divider shows on each frame; the refactor owner's 120 Hz films of this head are the evidence (verdict added here before ready-to-land).