cmux NEXT: a Command column-edge drag keeps the edge under the pointer; the strip camera no longer springs back after a change it cannot keep
Merged
LA
15h ago+510+510+012345678901234567890123456789-3-3-0123456789
A Command drag of a column edge keeps the edge under the pointer, also for a column that is not the first. The strip camera no longer takes a shift it cannot keep, so it no longer springs back after a layout change.
pixel-perfect's decision in the #35775 review, "your proposal (1 + 2)": (a) the slide was refused (the edge slid 26 pt under a still pointer, which breaks rule D), and so was (b) (a lag, then a catch-up).
Why
- Command (all columns), second column, strip that fits: on feat (step
a73b39aa5294dcc2a520401b), one frame after a 30 pt move of c1's edge, the edge shows 31 pt from its start. Then it slides on under the still pointer: 33, 36, 39 ... 56 pt by frame 11.- Command gives every column the dragged column's width, measured from the dragged column's start. The columns before it grow too and move its left edge.
- The camera keeps the dragged column in place (
ScreenDividers.dragAnchorColumn): the anchor step adds the shift to the camera's value and target. In a strip that fits, the target is then clamped back to 0 while the value stays shifted, so the camera springs back.
- Command, second column, strip that scrolls: the drag measured the pointer in strip space at the camera that the last move's anchor shift had already moved. So each move after the first added that shift to the width (in the test: 45 pt for a 30 pt move).
- The same spring-back, for any anchored sync: when the new strip leaves the camera no room (a strip that fits, its ends), the value takes the whole anchor shift, the target is clamped, and the camera springs back. That covers a key trade in a fitting strip, a width preset and a window resize.
Fix
- Reducer (Swift
ColumnScrollReducerand its 1:1 Rust portcmux-layout-reducerscroll/state.rs): the anchor step gives the camera only the shift its target can take in the new strip (anchorShift/anchor_shift). That is all of the shift while the shifted target stays in the strip, up to the strip's end otherwise, and never a shift the other way. The value and the target move together, so nothing springs back. A live trackpad gesture owns its target and still takes the whole shift. - Drag (
DividerDrag.columnTargets, newUniformEdge): with Command, the width that every column takes is solved so that the edge rests where the pointer puts it, with the camera the anchored sync will give it (free, at 0, or at the strip's end).- The pointer is measured on screen: strip space less the camera now.
- Only a solution within the pointer's travel of the current width is taken. A further one is another branch, and taking it would jump every width.
- In a strip with room, the width changes exactly as before.
Known limit
- At a strip's end, Command-shrinking a column that is not the last moves its edge right, and the last column's edge stays pinned: the content holds it to the viewport's edge. No width can move it toward the pointer there, so the drag keeps the plain extent (Command's widths follow the pointer distance).
UniformEdgeTests.atTheStripsEndTheLastColumnsEdgeCannotFollowAShrinkcovers it.
Changelog
- Fixed: Command-dragging a column edge (every column takes its width) keeps the edge under the pointer, for every column, and the strip no longer slides back after the drag.
Frames
- Frames: column edges and panes. Before, in a Command drag of a column that is not the first, the dragged edge showed 1 pt off the pointer and then slid on 26 pt under a still pointer, in a strip that fits. In a strip that scrolls, it ran ahead of the pointer by the camera's last shift on every move. After, the edge shows where the pointer puts it in every frame and stays still with the pointer.
- Frames: the camera in any anchored layout change (a key trade, a width preset, a window resize) when the strip leaves it no room. Before, the strip showed the anchored column held and then slid it to its place. After, the column takes its place in the change's own frame, and the panes' own springs carry the motion (no extra camera spring).
- No content stops following its pane.
Tests
Red first (9eec9acc691, tests only on feat), then the fix (667ef1ebb0c).
| Run | Ref | Step | Result |
|---|---|---|---|
| red, Swift | 9eec9acc691 | (fleet, key cmd-fix-red-swift) | ColumnScrollAnchorClampTests: a fitting strip, value 30 against target 0; the camera at the start, value -20 against target 0; the camera-with-room guard passes. CommandColumnEdgeFollowsPointerTests: fitting, "still frame 0: c1's right edge moved 31.0 pt, the pointer 30.0 pt" (and -31 for -30); scrolling, "after 30.0 pt frame 0: c1's right edge moved 45.0 pt, the pointer 30.0 pt". DividerModifierDragTests and the scroll suites pass. |
| red, Rust | 9eec9acc691 | 37b4256e29766931e27c6e2d (cmux-tui-rust-check.sh test, Linux) | anchor_a_strip_that_fits_gives_the_camera_no_shift_it_cannot_keep and anchor_a_camera_at_the_start_takes_only_the_shift_it_has_room_for FAIL; anchor_a_camera_with_room_keeps_the_anchor_in_place passes |
| green, Swift (first fix) | 667ef1ebb0c | cc7fcb9e9e0f1a144afd802c | 35 suites pass |
| green, Rust (first fix) | 667ef1ebb0c | 1bfa270a3f214ee68c23b86d | the 3 anchor tests pass |
| red: the mid-spring camera (Codex round 1), test only | 24605bfbe2e | 2795b0253b63b97f64a9d98d | commandDraggingWhileTheCameraSpringsMeasuresThePointerWhereItShows: c0 grew 130 pt for the pointer's 30 pt (abs(width - (start + 30)) → 100.0) |
| red: a target that leaves the new strip (found on the fleet) | 307045f249e | ead43b60f386b89a3c322c02 | atTheStripsEndTheLastColumnsEdgeCannotFollowAShrink got 570 pt, not nil (the camera would spring back 30 pt) |
| final head, Swift | 25f458ccd4c | 96de797d60e0934791d83935 | 35 suites pass: UniformEdgeTests 5/5, ColumnScrollAnchorClampTests 3/3, CommandColumnEdgeFollowsPointerTests 4/4, DividerModifierDragTests, the scroll suites, the SharedEdge suites (18/18 cases, 0 misses), DividerDragFollowsPointerTests and the 26 S2 suites; PaneMotionWorkTests skips (a bench) |
| final head, Rust | 25f458ccd4c | 88f43ef9c05e41b29f67e871 (cmux-tui-rust-check.sh test scroll::, Linux) | 62 tests run: 62 passed |
| final head, 32 more layout suites | 25f458ccd4c | c084224bd9b40755a58aa8cd | 31 pass; OutOfSightFramesTests skips (it needs a GUI) |
display fleet run (gui-session-v1 --display) | 25f458ccd4c | eb85ccfb73f2e8ca7ab286e2 | ci-step cmd-fix3-display-25f458ccd4c-v1 exited 0: 16/16 suites, 70 tests, 0 skipped |
| red: the first Command frame with uneven widths at the strip's end (Codex round 3), drag level | 2e4b1d2c5d9 (scratch fix/command-column-edge-red5-nomerge = 054294ef137 + test d5966937dd2) | 125eb48192c39b039632d6e0 | the columns went from [960, 480, 960] to [450, 450, 450] pt; the edge moved -22 then slid to +260 pt under the still pointer (282 pt range) |
green: keep every width (fix ad7d245d24e) | d5966937dd2 | 2bc97ee116950c8519da8d43 | UniformEdgeTests, ColumnScrollAnchorClampTests, CommandColumnEdgeFollowsPointerTests, DividerModifierDragTests: 27 tests pass |
red: Command-Shift skipped .keep while the pointer is down (Codex round 4) | a4aee0b376a | 588818e5a6933342c43b8e6b | the shift: true case fails (columns changed, 282 pt slide); shift: false passes |
| final head, 10 suites | 94dd258abe2 | 53be22307ec0b96ddd5c2e26 | 10 suites, 48 tests pass |
| final head, display fleet run | 94dd258abe2 | 130fe632407fe8ecf48f4e3e | exit 0: 16 suites, 75 tests |
| feat merged (ghostty-next on Zig 0.17, so the fleet takes the ref), 10 suites | 1b120756a69 | 8f24dcc551e12b3e07df99c4 | 10 suites, 48 tests pass, 0 failures |
| app build | 1b120756a69 | ace9844aca0a4d4e60c05023 | done (cmux DEV cmd-edge-v1) |
| launch smoke (fresh-home) | build ace9844a | f14339eb3d1b5db1c55372f8 | the app launched: fresh-home launch smoke OK: up 30s, Home at 7.4s, no crash (validator at feat 4822eb5d7ea); the client's verdict is a base finding (below) |
clippy fix e438f7fc97e (CI clippy, job 127690420744: unused_qualifications at scroll/state.rs:268; the PR's new use super::{swift_max, ...} made the older super::swift_max redundant; no behavior change) | e438f7fc97e | 61a9c28a2becc1daeb9186e5 (cmux-tui-rust-check.sh clippy, Linux) | exit 0 |
| Rust scroll tests at the head | e438f7fc97e | a9873ee7cb083df0380e14de (cmux-tui-rust-check.sh test scroll::, Linux) | 62 tests run: 62 passed |
Films
This PR changes every anchored camera sync, key trades too. pixel-perfect films the head against its base b5e96a15cd37 on cmux22 with the rule NO NEW RED (STATUS 13607), at 307045f249e (build eec4cd7c); only UniformEdge changed after it:
- PASS, no new red. The Command film with real input (Command on every mouse-down): the base drifts -28.5 pt at the mouse-up, then slides 30.5 pt (it rests 59 pt off) at x1, and is 30 pt off under Reduce Motion. The head drifts 0.00 pt, does not move after the up, and rests at 0.00 pt, at both speeds.
- KEYS and S2B journeys (single-3k, held-2, rk-3t, drag-3t, split-drag-2st, clamp-2x, clamp-chase-2x): equal or better. The one m14 "worse" film is a judge artifact (E5 against a ring hidden at rest), sent to R-HARNESS.
- A real m14 failure in BOTH arms of the Command film, not from this PR: about 165-170 frames of shell beside a terminal after the mouse-up (the terminal drag hold's landing). It goes with the drags-ON PR (m14, owner frame-owner-s2).
Observed / Not run
- Base finding, not this PR (the launch passed): the hq
cmux-ci fresh-homeclient postscmux-ci/fresh-homefailure, "Home did not render in a visible window", on every fresh-home run since at least 00:09Z (receipts for #36906, #36901, #36962, #36017 and this PR, five Macs, four validator commits). The validator (scripts/ci/fresh_home_launch_smoke.py) has counted the first-run Bring your history page over Home as a healthy launch since #34475 and passes these runs, but the client re-checksreport.home_tab, which is set only when Home's tab is selected, and the first-run page leaves it unselected. The screenshot shows the window with the sidebar, Home, the first-run page and the Command Palette tip. - Observed: the fleet runs above, the prepush gate, check-no-godfiles, check-motion and the package conventions lint.
- Codex rounds (no tools; the code in the prompt), each FIX fixed red-first:
667ef1ebb0c: the solver used the camera target, not its value; a Rust doc comment and lint attribute moved off their function.25f458ccd4c: a refused width still went through the plain-extent fallback (60 pt slide); the spring-back test refused a clamp that stops a camera mid-spring.054294ef137: the first Command frame with uneven widths at the strip's end applied the plain extent (202 pt slide). Fixad7d245d24e(UniformEdge.Target.keep: every width stays).d5966937dd2: with Shift held, the drag skipped.keepwhile the pointer was down. Fix94dd258abe2: Shift snaps only at the release.
- Disclosed: commit
7670cea515eholds the fix with its drag test (a staging error, stated in054294ef137). The unit test inad7d245d24eis no red proof: its scratch red branch mapped the old nil to.keep, and it passed on the old code (step5f9c6f2b016696318b5a9141). The drag-level testd5966937dd2is the red proof (step125eb48192c39b039632d6e0). - Scratch red branches (never merged):
fix/command-column-edge-red3-nomerge,fix/command-column-edge-red4-nomerge,fix/command-column-edge-red5-nomerge. - Films: the Command film at
ad7d245d24e(round ab-cmdh4): PASS at x1 and Reduce Motion (drift 0.00 pt at the mouse-up, no move after it, rests 0.00 pt from the pointer). Its m14 terminal fails (226 frames, shell beside a terminal after the mouse-up) are the drag hold's landing, in both arms; #36328 removes the hold. - Rust: at the head
e438f7fc97e, clippy passes and the 62 scroll tests pass (above). The head commit only drops asuper::path; the Swift suites, build and launch smoke ran at1b120756a69, which has the same Swift. - Merge rule: feat-cmux-next m26/m30 (it compiles, the app launches, its focused red/green tests pass). No review subagent ran; the four Codex rounds above are the reviews it had.
- Not run: a film at the final head
e438f7fc97e(only the Command-Shift keep rule changed afterad7d245d24e, then the feat merge).