Repository navigation
cmux NEXT: a drag during a spring takes over where the edge shows; shared edges never open (FRAME OWNER S2B) - #35036
Conversation
…ery frame, also when two motions overlap (FRAME OWNER S2B) FRAME OWNER REFACTOR stage S2B, the test that decides whether S2B is needed (the refactor owner's rule: today's films show no gap between two springs; a fix lands only if this test is red at the feat tip). SharedEdgeDuringSpringTests (CmuxNextAppTests): layout A | B (split x: b1 | b2) | C, the test as the display clock (120 Hz). At every frame tick, each pair of panes that shares a vertical edge at rest shows the same gap as at rest (no shell gap, no overlap), and a dragged edge moves with the pointer from where it showed at the press. Three overlaps: - a key press grows column A (its edge springs) and, during that spring, a drag starts on B's column edge; - the same, with the drag on the split divider inside B; - a Shift drag of the split lands on a snap (its release springs) and, during that spring, a key press grows column A. Why it may be red (code reading): each pane and divider frame is its own spring, and a drag snaps only the frames it retargets (`ScreenContentView.place`, M3), so a column on a key spring and the dragged column beside it can show from different motions; and a drag starts from the rest geometry (`DividerDrag.start`), not from where a springing edge shows.
…during a key spring keep shared edges together Two more cases in SharedEdgeDuringSpringTests, before the S2B fix (the refactor owner's conditions for the fix): - keySpringThenPushDrag: a key press grows column A and, during that spring, an Option drag of the split in B (B grows and pushes C); - keySpringThenFlingRelease: a key press grows column A and, during that spring, a Shift drag of the split in B released at 600 pt/s: the release lands on the snap and seeds its spring's velocity (`SplitMotion.seed`) while A still springs. The first red run (step 8cf32b98462d7aedc9d4cf18 at c43f92c) showed the two drag cases red (59 pt shell gap, 47 pt overlap, the dragged edge 65 pt off the pointer) and the release-then-key case green.
…d starts under the pointer (FRAME OWNER S2B) FRAME OWNER REFACTOR stage S2B. A divider, column-edge or row-edge drag that starts while a spring runs (a key resize, a release) showed a shell gap or an overlap between two panes that share an edge, and the dragged edge jumped off the pointer: SharedEdgeDuringSpringTests, red at the feat tip (step 8cf32b98462d7aedc9d4cf18 at c43f92c; with the push and fling cases, step 67c6e5f135ea7fb24cc9e311 at 9680ddd: 4 of 5 cases red, e.g. a|b1 59 pt open and b2|c 47 pt overlapping in the first drag frame, the dragged edge 65 pt from the pointer). Causes: - During a gesture `ScreenContentView.place` snapped the WHOLE frame a drag retargets (M3). A column whose right edge the drag moves also had its left edge snapped to where the key spring was still taking it, while the pane beside it was still on that spring. - `DividerDrag.start` measured the grab offset from the rest layout, so a drag that started on a springing edge put that edge at its rest place plus the pointer's move. - A release's fling (`SplitMotion.seed`) set the velocity of whole frames, so an edge the release did not move lost the speed of the spring it was on. Fix (`SharedEdgeMotion`, no change to the spring model): - During a drag, an edge whose target changed lands on it; every other edge keeps where it shows and its velocity. Both neighbours of a shared edge see the same target change, so they land together or spring on together. - The drag measures the pointer from where its edge shows (split, column edge, and so the Option push and the Shift landing too). - A release seeds only the edges its layout moved. Kill switch, default ON: `motion.frameOwner.sharedEdges` (MotionTunables, Debug Settings Springs), latched when a drag starts (`DividerDragState.sharedEdges`, also carried to its release). OFF is exactly the path before this commit. It goes after the films and the first dogfood gate pass. Also: `ScreenContentView.dividerLineRects` (a forwarder) is gone; its two callers build `ScreenDividers` themselves, so the ScreenContentView type does not grow (2052 lines). S1's TabStripFollowsPaneTickTests builds the release with the new field. Sidebar-Look: no visual change
…gged edge is read in its column's frame The split case dragged b1's edge 30 pt left, but b1 is 13 pt above its minimum width: on any rule the divider stops at the minimum after 13 pt (red run 67c6e5f1: 844, 841, 838, 835, then 834 for the other frames). It now drags right. A divider pressed while its column still slides on a spring cannot both stay under the pointer and leave the layout as it is at the press: the column moves under the pointer, so the pane would change by the column's remaining travel (65 pt here) on a press alone. The drag measures the pointer against the rest layout, so the pointer's travel changes the dragged pane 1:1 and the edge rides its column's spring. The test now reads the dragged edge from column B's left edge, which only the key spring moves. Sidebar-Look: no visual change
…hange, on the spring it rides The first rule landed every edge whose target the drag changed on that target. In a column that still slides on a key spring, that put the dragged split edge at its rest place while the column's other edge was 65 pt short of its own: the pane jumped by the column's remaining travel (fleet step 8c204f56). Measuring the grab from where the divider shows clamped the split at b1's minimum width instead. Now each component of a retargeted frame (x, y, width, height) moves by as much as its target and keeps its velocity, so each edge moves by its own target's change and stays on the spring it rides; both neighbours of a shared edge see the same change. A component at rest lands on its new target exactly, so a drag with no spring running places frames as before. The grab offset is measured from the rest layout again, as on feat, so a press changes no size and the pointer's travel changes the dragged pane 1:1. shownDrift is gone. Sidebar-Look: no visual change
…wner-s2b-shared-edge
…width below zero, a new pane at a release keeps its column, motion stops Three new cases (regrab during a release spring, shrink during growth, a new pane at a release) and stronger checks in all eight: the press moves nothing, a still pointer holds the dragged edge, the split divider stays at b1's edge, no frame shows a negative size, a fling's seed is set, the new pane keeps c's speed, shared edges agree within 0.5 pt, and the motion stops on its targets. Red at the tests-only branch scratch/s2b-red4 (feat tip + this file). Sidebar-Look: no visual change
…kes over where the edge shows Lead decision (2026-11-04): rule C, the motion rule that an animation is interruptible from its current value. The shift rule alone kept the dragged pane's own size spring: a divider grabbed again during its release spring kept sliding under a still pointer, and a pane shrunk while it still grew could show a negative width (Codex review FIX at 54af943). At the press (DividerDrag.catchShown, only with motion.frameOwner.sharedEdges latched and the screen moving), the drag's value (the split's ratio, the column's width) goes to where its edge shows from its anchor (the edge across the pane or span it resizes), in the drag's own transaction, and the screen lays that out in one pass that moves nothing on screen: the edges on the dragged line take the anchor's offset and velocity, every other edge the new value moves keeps where it shows and its velocity. The drag then starts from that layout, so the pointer's travel changes the pane 1:1 and nothing shows past its minimum. Each move after the press shifts the edges it changes by their targets' change, on their springs (as before). A release's fling no longer seeds a frame that is new in its pass: a split's new pane keeps the motion it rides. Sidebar-Look: no visual change
…lease spring changes nothing, the switch is on Review (Codex FIX at d221f9d): the press check now compares every pane and divider frame right after the mouse down, before any frame; a ninth case clicks the divider during its release spring (press and release, no move) and expects nothing to move and the release's ratio to stay; the suite requires motion.frameOwner.sharedEdges on. Sidebar-Look: no visual change
…nd stops the dragged line by influence Review (Codex FIX at d221f9d): the catch found the dragged line's edges by distance (it could take a neighbour on another spring), missed a catch with no target change (an edge crossing its target), moved the edge on the press when the value clamped at a minimum, and fell back to the shift rule when it found no anchor. Now the press's pass keeps every frame where it shows and its velocity and only sets the targets of the caught value. The change of speed that stops the dragged line against its anchor goes to each edge in proportion to how far that edge moves when the value moves (a probe layout 8 pt further, either way: SharedEdgeMotion.Influence): the line stops against its anchor, the panes after it move as one block, inner edges of a split take their share, and the two edges of a shared edge always get the same change. The anchor is the nearest edge among the panes the layout puts on the anchor's side (the split's first side, the column's panes). A clamped value moves nothing on the press; that edge springs to its minimum. The press syncs through the window (scroll, chrome, overlay as any layout change), and a cancel (a click, Escape) puts back the value the catch replaced, an unconfirmed release's included. Sidebar-Look: no visual change
…the red run compiles on a base without the switch Sidebar-Look: no visual change
…ing needs every velocity at rest Review (Codex FIX at 83bf2ef): CGRect.width is the standardized width, never negative, so the size check read the springs' own values; landing now checks every component's velocity. Sidebar-Look: no visual change
…old drag; a cancel sends the replaced release again Review (Codex FIX at 83bf2ef): - No anchor frame, a value no probe can move, or a row edge, while the screen moves: the drag turns motion.frameOwner.sharedEdges off for itself (whole frames snap, as before S2B), never the shift rule with a size spring's leftover. The screen counts as moving when a frame is off its target or has any velocity (a spring can cross its target with speed); the press no longer needs hasMotion. - The influence layouts are laid out off the pixel grid, so the two edges of a shared edge get the same change. - A cancel (a click, Escape) after a catch sends the unconfirmed release it replaced again, in a new transaction: it settles or is refused as any release, never a stale local value put back. A modifier mode that stops setting the caught key puts it back at the caught value (the drag's start), not the daemon's. - A shown extent the layout cannot hold (under a minimum, past the ratio range) is caught at the nearest one it can hold and springs there from where it shows (documented policy). Sidebar-Look: no visual change
…ws shows no negative width Review (Codex FIX at 87f82ae): a column whose own edges are at rest while an inner split springs (an equalize, a release) kept b1's spring leftover under the shift rule; shrinking B to its minimum then showed b1 with a negative width. A tenth case grows b1 by far more than a pane's minimum with an equalize and drags B's edge in to B's minimum while it grows. Green on feat (whole frames snap there): a guard for the rule. Sidebar-Look: no visual change
…e past zero takes the drag from before S2B Review (Codex FIX at 87f82ae): - The press now also checks a drag whose line is at rest against its anchor: for every pane the drag resizes (its size changes between the caught layout and a probe), a pane that shows narrower than its rest size by more than a pane's minimum would show a negative size when the drag shrinks it (a move keeps each edge's leftover). Such a press turns motion.frameOwner.sharedEdges off for its drag: whole retargeted frames snap to rest, as before S2B, which shows no negative size. The check runs before anything changes. - Compile: DividerDrag+Catch imports CmuxNextDesign (AnimatedFrame); apply's targets is declared once. Sidebar-Look: no visual change
… a negative size, in every drag mode Review (Codex FIX at c9236aa): the guard looked only at the panes the plain drag resizes and only at the leftover at the press. A modifier mode (Command uniform widths, Option trading) resizes other panes, and a size spring's speed can carry a pane below zero after the press. Now the press follows every pane's width and height spring (its leftover from the caught rest size and its speed, with the catch's share of the change of speed) along its whole path on the panes' spring, and takes the drag from before S2B when any could go further under its rest size than a pane's minimum: any drag mode can shrink a rest size to that minimum, never under. The check runs before anything changes, also when the line is at rest; a catch whose probes cannot move the line takes the old drag too. Sidebar-Look: no visual change
…three moves), so b1 still grows when it is shrunk The probe without the guard (scratch/s2b-probe-noguard, step a25345332c72fcabdaa150ca) passed the slow version: b1's spring settled during 18 small moves before its rest width got small. Sidebar-Look: no visual change
…'s minimum in one move, one frame into the spring The fast version (three moves) also passed without the guard (probe step 4ed66a09e176d6c7126b003a): b1's spring settled faster than its rest width shrank. Sidebar-Look: no visual change
…g from before S2B Review (Codex FIX at 1355c29): a shown extent under a minimum (a shrinking spring past it) or past the ratio range was caught at the nearest value the layout can hold, and the line then sprang on to it under a still pointer. The press now lays the caught value out and takes the old drag when it does not put the line where it shows (1 pt: the rest layout is on the pixel grid, the check's own is not). Sidebar-Look: no visual change
…within the shared-edge tolerance Review (Codex FIX at 87af13f): the 1 pt allowance passed a catch whose value the layout clamps by less than a point (a shrinking spring's undershoot past a minimum, grabbed at 0.75 pt under it); the line then sprang 0.75 pt under a still pointer. The press now lays the caught value out as the screen will (its viewport and pixel scale) and compares the line's rest place there with where the line shows from its anchor: 0.5 pt or more apart, the press takes the drag from before S2B. Sidebar-Look: no visual change
|
Review (codex): SHIP at 3fbd40a Independent review by Codex (gpt-6.1-sol), seventh round. The reviewer had no tools: the pack held the full diff from the merge base, the full changed files (SharedEdgeMotion, DividerDrag+Catch, DividerDrag, DividerDragState), and the reconcile geometry code. Rounds 1 to 6 at d221f9d, 83bf2ef, 87f82ae, c9236aa, 1355c29 and 87af13f said FIX; each blocker was fixed in the commits after them. Round 7 answer:
No additional concrete blocker is demonstrated by the supplied code. The extra layout computation occurs once per catching press; its actual latency cannot be established from these excerpts. |
|
Found 6 test failures on Blacksmith runners: Failures
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: Default changesThis pull request changes 3 defaults at
Signed off on this commit by Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
|
Red-check triage for head 3fbd40a:
Both are fixed by open #34979 ("ci(cmux-next checks): clear base reds (fixture host name, rollup placement job)"). They do not hold this PR. |
|
Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order). This PR raised
plans/cmux-next/layer-ownership.md says Rust owns every state, list and rule and Swift is the platform shell. Please move the new logic of this PR in a follow-up: to Rust (the crate that owns the area; the owner lists in STATUS 12147-12165 name the target per area), or at least out of the app into its package. If a declaration above is platform code (AppKit, Core Animation, OS calls, strings, tokens, DEBUG tooling), reply here and the table gets a PLATFORM row with your reason instead. The swift-logic row is a report, not a gate: nothing blocks on this comment. |
…size Conflicts: - CmuxNextApp/LaunchWarmup.swift: feat moved the display UUID prewarm into LaunchSystemWarmup.displays() (CmuxNextDesign) and added appleMenuDevice(); feat's side, and its doc comment names NSScreen.stableDisplayID (the S7 branch moved WindowPlacementFallback.displayID there). - CmuxNextDesign/Diagnostics/LayoutPassGuard.swift: both additions (S7's windowLayoutsBegun, feat's per-window pass counts for debug.frames). MotionTunables.swift: feat added and then deleted motion.frameOwner.sharedEdges (#35036, #35908): no net change there.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
FRAME OWNER REFACTOR S2B. A drag that starts while panes still spring (a key resize, a release, a column growing) takes over where its edge shows. Two panes that share an edge show it at one place in every frame, the press moves nothing, the pane follows the pointer 1:1, and no pane shows a negative size. Before, the shell showed between neighbours (up to 92 pt) or they overlapped (up to 80 pt), and a regrabbed divider kept sliding under a still pointer.
Root cause
During a pointer gesture,
ScreenContentView.placesnapped a whole frame (all four edges) whenever the drag retargeted it, and a drag measured the pointer against the rest layout. If a spring still moved a neighbour, the dragged column's panes jumped to rest while the neighbour stayed on its spring. A release fling wrote velocity to whole frames.Fix: rule C (lead decision; the motion rule "an animation is interruptible from its current value")
Behind
motion.frameOwner.sharedEdges(Debug, default on), latched when a drag starts. With the switch off, the old path is unchanged.DividerDrag.catchShown): the drag's own value (the split's ratio, the column's width) is set to where its edge shows from its anchor. The anchor is the nearest edge among the panes the layout puts on the other side. The value is written in the drag's own transaction, and the window syncs it in one pass in which every frame keeps where it shows and its velocity. Each edge then gets its share of the change of speed that stops the dragged line against its anchor (SharedEdgeMotion.Influence: how far the edge moves when the value moves, from an unrounded probe layout), so shared edges always get the same change. The drag then starts from that layout.SharedEdgeMotion.place): each component of a retargeted frame moves by its target's change and keeps its velocity. A component at rest lands on its target exactly, so a drag with no spring places frames as before.SplitMotion.seed): a fling seeds only the edges the release moved. A frame that is new in that pass keeps the motion it rides.Changelog
Tests
SharedEdgeDuringSpringTests(new, 10 cases, checked at every 120 Hz frame tick). Run 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=SharedEdgeDuringSpringTestsThe checks in every case:
The red proof is the tests-only branch
scratch/s2b-red4(the feat tip + the test file only), so the commit order on this branch is not red-first.clickDuringReleaseSpring,releaseSpringThenKeyPressandshrinkColumnDuringInnerSpringpass on feat too; they are guard cases for the rule.scratch/s2b-probe-noguardb6dbb1e, step b62adaeed572bb2da01ba796):shrinkColumnDuringInnerSpringfails (b1 at -105 pt, edges open by about 100 pt).Review
Codex (gpt-6.1-sol, no tools; the pack held the full diff, the changed files and the model, sync, reconcile and spring code it calls):
Review (codex): SHIP at 3fbd40a34c1e33c4b40e81d78e8b7e3ac1480fca.Bench (switch OFF vs ON, same host, interleaved OFF/ON/OFF/ON)
No regression (pixel-perfect). cmux22, LayoutDragBench + PaneMotionBench, CMUX_DRAG_BENCH_ROUNDS=5, runs OFF, ON, OFF, ON (OFF: scratch/pixel-perfect-s2b-off-3fbd40a 81642f1, only the switch default false). Per-frame main-thread ms:
Films
GREEN for S2B's scope (pixel-perfect). Release build 9453c297 at 3fbd40a, cmux22, terminal-only layouts at x0.125, OFF then ON in one launch, judge_pixel 3d+C f6c2ef7. Both presses grabbed the edge (column drag travel 170-171.5 pt, split drag 120 pt; 20-21 pt of the key spring left at the mouse-down).
Frames notes (not blockers):
Judge reports: /work/tmp/pixel-perfect/judge/s2b/ (s2bt-col-, s2bt-split-).
Observed / Not run
No capture: the change is frame timing during motion; the films from the refactor owner are the evidence.