cmux NEXT: a grabbed divider stays under the pointer during a spring, also while its column slides (FRAME OWNER S2B rule D)

Merged
LA
2d ago
+863-242

FRAME OWNER REFACTOR S2B, rule D (the refactor owner's decision, 2026-11-04): a drag that starts while panes still spring holds the dragged edge on the pixel where it is drawn. The pointer alone moves it, 1:1, also while its column slides, and a still pointer holds it. This also fixes a red that #35036's merged suite has shown on feat since KEYS #34123.

Takes back #35036: rule C's catch, which made the grabbed line take its anchor's motion. The films showed that the line then slides ahead of a still pointer (20 pt at rest). The refactor owner replaced it with rule D (2026-11-04). The rest of #35036 stays: the switch, the move rule, the release seed, the cancel resend and the negative-size check.

Why

  • Films (pixel-perfect, S2B head 3fbd40a, C3T column edge, x0.125): under rule C, the grabbed edge kept its column's slide. It drifted ahead of a still pointer, by 20 pt at rest (+2 px at 25 ms, +16 px at 200 ms, +28 px at 390 ms). With OFF it jumps by the same amount at the press. Rule C's clause "only translation springs stay" had been approved without a film of this case.
  • Tip red: SharedEdgeDuringSpringTests has been red on feat since KEYS #34123 (4f13a238f24), 4 of 10 cases, 1.00 pt off the pointer for the whole drag.
    • Bisected on scratch merges of the S2B head 3fbd40a: with #34123's parent it is green (step 254ec9a6b66c86694748e6b4); with #34123 it is red (step 530ed5a7dcc4f4272dd39f74).
    • #34123 makes a key press trade width, so B's left edge springs too. The catch measured the line on the springs' unrounded values, so it caught the line up to a pixel off the drawn one.
    • My process fault: the green evidence for #35036 was at 3fbd40a, but cmux-bot's feat merge after the label made the final head 6aad7d8406b, which already failed. From now on, green evidence is taken at the final head.

Rule D

Behind motion.frameOwner.sharedEdges (Debug, default on), latched per drag.

  • The press (DividerDrag.catchShown, only while the screen moves): the drag's value (a ratio, a width) goes to the value that rests the line on its drawn pixel. That value is measured from its anchor's REST place, the edge across the pane or span it resizes.

    • The pass that lays the value out keeps every frame where it shows.
    • Each edge then gets its share of the change of speed (minus the line's velocity), by how far it moves with the line (Influence). The edges that move as the line does also get the change of place onto the drawn pixel. The anchor and the edges before it keep their springs.
    • The drag measures the pointer from the drawn line (the grab offset and Option's press point).
  • The clamp: where the anchor's rest place leaves the pane less than its minimum, the layout clamps the line there.

    • The line springs to that place from where it is drawn, and no further.
    • It never shows behind the pointer: on each frame, SharedEdgeClamp.follow drops the part of that spring that the pointer has passed (the same part of distance and speed). Past the line's target, it drops all of it.
    • The probes for Influence go around where the layout rests the line, so a clamped press keeps rule D.
  • The negative-size check (a press whose drag could shrink a pane that its spring takes far under its rest size past zero takes the old drag) now also follows each pane's size without its share of a clamp's leftover. The springs are linear, so this covers every time and every part the pointer can drop. A clamped press during a slide bigger than a pane's minimum takes the old drag, as an unclamped press during the same slide already did.

  • Removed from rule C: the anchor ride (the line took its anchor's velocity) and the 0.5 pt clamp fallback (rule D holds a clamped line by design).

  • Kept: the cancel resend, the per-edge release seed, and the move rule (each retargeted edge moves by its target's change and keeps its spring).

  • Known limit (the camera): rule D holds the line still in the strip. A reveal or center request during the drag moves the strip camera (a tab opened beside a chat, an automation's reveal, Center Column). The held line then moves on screen with the camera, until the next pointer event puts it under the pointer again (within the split's limits, as for any drag). A focus change does not move the camera during a drag: the scroll reducer defers it to the drag's end. A press while the camera already moves takes the old drag. SharedEdgeCameraLimitTests shows the limit. The filmed cases do not hit it. I own the follow-up.

  • Known limit (the clamp, a short undershoot): in a clamp, the press holds the line while the anchor (the edge across the pane) is still on its spring toward the line. So the pane between them goes under its minimum for a short time, until the line's spring to the clamp catches up. In the clamp film (ON) b1 is at least 196.0 pt against its 200 pt minimum, from 40 ms to about 900 ms after the press at x0.125 (about 0.11 s at x1). OFF never goes under 200 pt. No size goes under zero (the press check covers that), and the shared edges stay together. A spring model of the film's set-up gives the same: 3.8 pt under for a press at 70 % of the step. The fix (follow-up, mine): measure the clamp from the anchor's live place, so the line never leaves the pane under its minimum. The unit tests check the dragged edge against the clamp, not the pane's width while its anchor moves; the follow-up adds that check.

  • Known limit (Command on a column edge, from before S2B): with Command, every scrolling column takes the dragged column's width (StripStart.uniform), and columnTargets measures that width from the column's start place. Columns before the dragged one grow too, so its line moves 2 pt per pointer point in a strip that fits, and a Command press can move it under a still pointer. Those are the mode's own targets: the switch OFF and feat show the same line (frames snap to those targets). Rule D does not change them, and its clamp drop never moves an edge past its target. The Command tests drag only the first column. I own the follow-up (a red test with the second column's edge, in a strip that fits and in one that scrolls).

Frames

  • Frames: divider and column edge. Before (feat, rule C), during a drag that starts while a column slides, the grabbed line keeps the slide and shows ahead of a still pointer, by 20 pt at rest. Since KEYS #34123 it also shows 1 pt off the pointer during a key spring. After, the line shows on the pixel where it was drawn at the press, moves with the pointer 1:1 in every frame, and stays still with a still pointer. In a clamp, it moves toward the clamp only, never behind the pointer, and stops at the clamp.
  • Frames: panes (terminal, page, agent chat). The pane edges on the line move with the line in every frame. The anchor's edge and the edges before it go on with their own springs. A shared edge shows at one place in both panes in every frame (shell-gap edges on the final build: C2ST 1210 OFF, 0 ON; C3T 408 OFF, 0 ON). Pane content follows its frame as before. The drag's surface hold (A3) is unchanged, and it is the same with OFF.
  • No content stops following its pane in this PR.
  • Known effect (pixel-perfect, films of the final build 07444af96423f1534e5d8a1f): ON has more terminal content-edge frames than OFF (C3T 441 against 437, C2ST 434 against 328). By phase, the counts are equal before the press and during the drag: the difference is only after the mouse-up. The interim films of 813ac157880 showed the same split by phase (after the mouse-up: C3T 332 against 290, C2ST 344 against 245). OFF snaps every frame at the press, so its springs end early. ON keeps the key spring, so the terminals' drag hold (A3) and key hold (S4, OFF in that build) last until the springs land. This is the S4 and A3 holds, not rule D, and rule C showed the same. The S4 flip films check it again.

Changelog

  • Fixed: a divider or column edge grabbed while panes still animate stays under the pointer for the whole drag, also while its column slides.

Tests

On the fleet: cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --arg=Packages/macOS/CmuxNext --env CMUX_SWIFT_TEST_SUITES=SharedEdgeDuringSpringTests,SharedEdgeCatchFallbackTests,SharedEdgeCameraLimitTests

  • SharedEdgeDuringSpringTests, 18 cases. The dragged edge is read as the screen draws it, against the press plus the pointer's travel, in view space. Before, it was read from its anchor, which slides with it, so the test could not see the drift. New cases:
    • a whole column slides while B's edge or its split is grabbed and held still;
    • A's edge springs into B past b1's minimum while b1's split is held (the clamp), held still for 1 s: it must come to rest at the clamp;
    • the same, with the pointer past the clamp in one move before the line lands;
    • the same, with the pointer moving toward the clamp faster than the line springs there, on the split and on B's column edge: the line must stay between the pointer and the clamp;
    • a top-and-bottom split grabbed during an equalize;
    • a right dock's inner edge grabbed while the dock grows.
  • SharedEdgeCatchFallbackTests: a press while the strip camera moves, a clamped press whose drop could show a pane under zero (a 300 pt strip slide), and a press whose spring speed could take a pane under zero all take the old drag.
  • SharedEdgeCameraLimitTests: the camera limit above.

The commit order is not red-first for the first red: that proof is the tests-only branch fix/frame-owner-s2b-rule-d-red-nomerge 6d8d3c8eea6 (feat + the test). The clamp reds are commits on this branch.

RunRefStepResult
red: rule D tests only, on feat6d8d3c8eea65d0545000103659ff1f111788 of 15 cases fail (drift of 19-45 pt from a still pointer during a slide or a key spring)
red: Codex round 2 (clamp spring kept)c3fffa1d6262a56ae90b3dfab8ce6274883pointerPassesTheClampBeforeItLands fails: "drag frame 1: the dragged edge shows at 810.00 pt, the pointer put it at 900.00 pt", still moving 12 frames later; 15 of 16 pass
red: Codex round 3 (negative size, line behind the pointer)3d742a3b716eb71b3265789f8ddba14d76dthe 3 new tests fail: aClampedPressWhoseReleaseCouldShowAPaneUnderZeroTakesTheOldDrag (the press kept rule D, then "drag frame 1: b2 shows a negative size -85.35 x 700.00"), pointerChasesTheClamp ("drag frame 1: the dragged edge shows at 780.00 pt, outside the pointer's 790.00 pt and the clamp 870.00 pt"), pointerChasesTheColumnEdgeClamp (1005.00 pt against the pointer's 1013.00 pt); the other 16 cases pass
green: the 3 suites + the 26 S2 suitesddf0ac45b40053f30b2c8e194a9d8cacff9pass: 18/18 cases with 0 misses, 3/3 fallback tests, the camera test; 28 suites pass, PaneMotionWorkTests skips (a bench: CMUX_PANE_BENCH=1)
32 more layout suitesddf0ac45b40814cf11b2816df0b4071fbd931 pass; OutOfSightFramesTests skips (it needs CMUX_TEST_REQUIRE_GUI=1)
final head (feat 4546c99a6c1 merged): the 3 suites + the 26 S2 suites95c38f3bfed93808595241549931f0e4a7epass: 18/18 cases with 0 misses; 28 suites pass, PaneMotionWorkTests skips
final head: 32 more layout suites95c38f3bfed4243b592443e98d12bb0cc0931 pass; OutOfSightFramesTests skips
final head, display fleet run (gui-session-v1 --display, console session on a display Mac): the divider-drag suites95c38f3bfed3396f6f199dd1c4687c809a8ci-step s2bd-rule-d-display-95c38f3bfed-v1 exited 0: 15/15 suites pass (SharedEdgeDuringSpringTests 18/18 cases with 0 misses, SharedEdgeCatchFallbackTests, SharedEdgeCameraLimitTests, DividerDragFollowsPointerTests, DividerDragFrameTickTests, DividerCatcherMotionTests, DividerClickKeepsRatioTests, DividerDragTerminalResizeCountTests, DividerDragTerminalSteadyTests, DividerModifierDragTests, DividerSnapTests, DragSendsOneIntentTests, PaneResizeGestureTests, DividerFocusLayoutTests, SplitSpringMotionTests), 56 tests, 0 skipped

Typing

Nothing runs per key, or per layout pass outside a drag:

  • SharedEdgeMotion.swift: the catch computation, Influence.apply and the negative-size check, at a press. place() and its per-pass guard are unchanged.
  • DividerDrag+Catch.swift: catchShown, at a press.
  • SharedEdgeClamp.swift: follow, on each frame tick of a drag whose press was clamped (O(the frames the line moves)), and nothing else.
  • MotionTunables.swift: doc and help text only.

Films

pixel-perfect, cmux22, x0.125, OFF and ON in one launch, on two builds: ad4ae2c4 (813ac157880) and the final build 07444af96423f1534e5d8a1f (ddf0ac45b40):

  • C2ST (the split dragged 120 pt down during a key spring): PASS on both builds (round ab-s2bdf2-ddf0ac4 on the final one). Both arms grab the split; at the mouse-up it is exactly at the pointer (drift 0.00 pt, from the app trace); the press moves it 2.0 pt at most (one pointer step). Shell-gap edges at the shared edges on the final build: 1210 OFF, 0 ON (1173 OFF, 0 ON at 813ac157880).
  • C3T (the column edge dragged 150 pt during a key spring): PASS on both builds (round ab-s2bdf3-ddf0ac4 on the final one; kit c50def74536, whose press grabs the p1|p2 edge). Both arms grab; drift 0.00 pt at the mouse-up; the press moves the edge 2.4 pt at most. Shell-gap edges at the shared edges on the final build: 408 OFF, 0 ON (the p0|p1 edge at 813ac157880: 432 OFF, 0 ON).
  • Clamp (round ab-s2bdc2-ddf0ac4, a 2-column layout A | B with split x and b1 6 pt above its minimum, the split held still while a key step pushes A's edge into B). Every press is inside the hit band (1.4 to 1.9 pt from x) and focuses no pane.
    • ON: x stops at the press (0.5 pt the tick before, 0 after), springs one way to the clamp and stops exactly there (904.5 pt: A's rest edge + 200 - 3). It never shows behind the pointer.
    • OFF: x keeps its slide and ends 1 pt past the clamp.
  • Clamp chase (the same, with the pointer 30 pt to the right in 0.6 s, past the clamp): ON ends exactly at the pointer (906.0 pt), at most 0.54 pt behind it on the way. OFF ends 19.5 pt past the pointer.
  • An earlier clamp PASS (on 813ac157880) is withdrawn: 3 of its 4 presses missed the split by 25 to 26 pt (the hit band is 12 pt), and a missed press shows the same frames as a correct clamp. The films above use a grab check at the mouse-down.
  • The films count for 95c38f3bfed: that commit only merges feat, and no layout or motion source changes (pixel-perfect checked the merge diff too).

Review

  • Codex round 1 at 813ac157880: FIX (a view offset that still moves at the press). Fixed: such a press takes the old drag.
  • Codex round 2 at f163471: FIX (the clamp spring stayed after the pointer passed the clamp). Fixed in 83583cac2a6.
  • Codex round 3 at 83583cac2a6: FIX (that release could show the far-side pane under zero; also mode and camera coverage). Fixed in 3429bc82b80 and ddf0ac45b40.
  • Codex round 4 at ddf0ac45b40: FIX (Command on a non-first column edge moves the line 2:1: the mode's own targets, the same with OFF; disclosed above as a known limit, follow-up mine). Round 5 at ddf0ac45b40: SHIP (receipt on this PR).

Observed / Not run

  • Observed: the fleet runs above (green at the final head 95c38f3bfed), the prepush gate, check-no-godfiles and check-motion, the Codex rounds (SHIP at ddf0ac45b40, carried to 95c38f3bfed: that commit only merges feat, and the PR's own diff is unchanged), and pixel-perfect's films (C2ST, C3T, the clamp and the clamp chase, all PASS on the final build).
  • Filmed since: the clamp drop (the clamp chase above). The unit tests cover it too.
  • Not tested: a pane made during a clamped drag (for example a split by an automation) is not in the clamp's recorded frames, so it gets no part of a drop. A key spring started during a drag is also untested. Codex round 5 named both as verification limits and found no failing path. The films of the final head are pending (above). If the bot merges feat again after the label, I run the suites again at that head.

No capture: frame timing during motion; the films from the refactor owner are the evidence.