Pane motion: during a close the kept resize handle stays centred on a visible gap; the last column and the last row close without showing the window background (FRAME OWNER S3d)

Merged
LA
1d ago
+496-67

Summary

FRAME OWNER REFACTOR follow-up S3d, after S3 (#34170, merged as a08c406ba0f). S3's rule: the shell color never shows inside a pane's rect during a key resize or a close; a closing pane keeps its host with only its own ground (ClosingPanes) on the space its neighbours have not covered yet. S3d fixes the closes S3 still got wrong, and makes the kept resize handle sit in the middle of a visible gap in every frame of a close.

Frames (what a person sees in each frame of a close, per content kind; before = feat with S3, after = this PR):

  • Frames: divider and column-edge handle. Before: when the handle that stays is on the closing pane's trailing or bottom side (a split pane's right sibling closes, the pane above a kept divider closes, a column's right split pane closes), the closing pane's ground covers the gap under that handle, and the shell gap shows on the other side, where the handle goes; the handle is off its gap by up to the closing pane's width, so a press on it during the close can miss. After: in every frame the shell gap beside a closing pane is the kept handle's gap, with the handle in its middle; the closing ground covers the gap whose handle goes. On the refactor owner's films the base showed the handle up to 15 device px off its gap.
  • Frames: the closed pane's place (closing ground). Before: the strip's last column (3+ columns) showed the window background where it was while the other columns slid back (596 px in the close's first frame); the last row of a scrolling column showed it while the rows scrolled back (321 px); two closes in a row showed up to 109 pt of background between the two closing panes, and left 412 pt of closing ground past the strip's end that disappeared in the landing frame. After: in every frame the closed place shows the closing pane's own ground until a live pane covers it, and the ground is gone (under 1 pt) before the landing frame.
  • Frames: terminal, page, agent chat. No change: S3d touches only closing panes, which show no content; live panes' content moves as on feat.
  • Frames: tab strip. No change.

How:

  • The gap that shows is the kept handle's (ClosingPanes.begin, handleStays). Each side of a closing pane looks up the resize handle in its gap in the layout the pane left (split dividers, column edges, row edges, in the pane's own space: strip or dock, scroll area, row column) and keeps the gap only when the new layout keeps that handle; where the handle goes, the side covers the gap. No handle found: S3's rule (the leading side keeps the gap when the trailing side has neighbours). The divider spring code does not change.
  • The decision waits for the change's cameras (ScreenContentView.settleClosing, in syncScroll after settleCarried, before the change's first frame). S3 decided "nothing moves beside this closing pane" inside reconcile, before syncScroll started the carried neighbours' springs, and hid the strip's last column at once. A column's removal clamps the strip camera at once and the view carries the panes from where they showed (ColumnScrollReducer), so the last column's neighbour moves on its own spring and the closing pane collapses on it. A pane nothing moves over still hides at once, as before; with nothing moving at all the retired hosts still go in the change's turn.
  • A row column's scroll keeps its spring: closing the last row of a column whose rows still overflow moves no pane. Its travel (Entry.rowTravel) collapses the free side of a closing pane whose other y side rides a neighbour that does not move.
  • Two closes in a row: closing panes are placed from the last to close back to the first (a pane borders only panes live at its close), each pass keeps every closing pane's rest (zero size where its sides meet), and a side that rides a closing pane reads that rest as a side on a live pane reads its target frame. A free side's travel remembers its end (Side.end) and starts again from where both sides show when that end moves or the row scroll takes another target, so it never jumps.
  • Trace: the debug.motion sample (DEBUG and bench hooks only) lists each closing ground under a new key closing ([pane, x, y, width, height, alpha, z]), apart from panes, so a trace judge can centre a handle on the gap beside a closing ground (R-HARNESS pair C needs it; told). panes rows do not change.

Takes back #34170: S3 made the debug.motion trace sampler skip every host that shows a closing ground (!host.showsClosingGround in ScreenContentView+MotionSample.swift), so the live-pane judges would not take a ground for a pane with content. That hid closing panes from the trace completely, and R-HARNESS pair C (a divider centred on the gap between the panes beside it) then reads the closed place as the gap. S3d keeps closing grounds out of panes as S3 meant, and lists them under a new closing key instead. Same author, the S3 follow-up; debug and bench builds only.

No second owner, timer, hold or retry: a closing pane's rect stays a function of its neighbours' springs and its row column's scroll, read each frame. Behind S3's switch motion.frameOwner.contentPolicy (default on); off is unchanged.

Area owner told (STATUS 11183): PL-motion-core (perf-hot ClosingPanes.rects and presentClosingPanes).

Testing

Red first, on the fleet (each red is its own commit before its fix):

  • Scratch branch scratch/frame-owner-s3d-red at f620e16692a (the base 154073fc8e6 + the S3d tests, no fix), step 8e450b7a89c73b40665831e3: every new case red for its reason: the strip's last column (596 px hole in the close's first frame), the last row of a scrolling column (321 px), and 4 kept-handle cases (the kept handle's centre on the closing ground, or 292 pt off the middle of its gap); the 2 guards (the films' px-close-pane-3 and px-close-column-c3 layouts) green.
  • 7fdf9f7fd67 (two last-row closes in a row), step 03270980560f5bd06b1d21bc: red, up to 109 pt of shell between the two closing panes from frame 7.
  • Scratch scratch/frame-owner-s3d-red2 at 7a957176344 (two last-column closes in a row, without the rest fix), step 5ad0eec45e5715c44b5de60a: red, 412 pt of closing ground left for the landing.
  • 6eaaac71407 (the trace's closing key), step 64c4793ca602837e5f3469f2: red, no closing row in the sample (the other 6 kept-handle cases green there).
  • Guard, not red: af48fabac3b (a second last-row close that makes the rows fit; Codex's round-2 case) passed without its fix (step cadeb2ac05f197c36580c860): rows that fit fill their column, so the frozen edge was already at its rest. It stays as a guard; 7a957176344 is the red for that fix.

Green at this head 2f13aa409db617d9c228c66dcd72105efe25cf8f: step 17da977cc01046a001809388: 44 of 45 suites passed; the one failure, CmdWEmptiedSplitPaneLeavesInItsTurnTests.aRunningProgramBringsThePaneBackWithItsFocusBeforeTheQuestion, is a base flake (it failed alone at the base 154073fc8e6 in step 7f0156f8b679905f76cc17ee and passed there in 70ba39a34d3420304fab949f; STATUS 11361; not in S3d's files) (the 2 new suites and every close, key-resize, S3 and motion suite: PaneCloseKeptHandleTests, PaneCloseCameraGroundTests, PaneGroundCoversPaneTests, PaneContentPolicyTests, KeyboardResize* (7), CloseTeardownAfterLandingTests, PaneCloseMotionTests, ColumnCloseMotionTests, ColumnCloseScrollMotionTests, ColumnCloseClampVelocityTests, RowCloseMotionTests, PaneCloseCanvasTests, ClosedPaneContentStaysUntilLandingTests, ClosingHoldStaleWithdrawTests, ClosedPendingSplitFocusTests, PaneBurstMotionTests, PaneZoomMotionTests, ...). Earlier heads: 45/45 at 02de7be2a22 (7c4165f2b88c8fbcc07d1a02) and at d369376ebd9 (2a4c4105f65bc0a78dc4c217).

Release compile: cmux-ci compile-check --app --release at this head, job a3509913ec2ae257a046938c (at bd5a6a32eca; 2f13aa409db changes only the nested functions): passed (162 s); the optimized build is proven by the release bench below at this head. (The release bench at d369376ebd9 had found a release-only error, "sending 'edge' risks causing data races"; fixed in bd5a6a32eca.)

Performance

PaneContentPolicyBench (release, CMUX_PANE_BENCH=1, main-thread time per motion frame over stub content, key presses and closes, switch off and on in one process), base 154073fc8e6 (step c0191b9fbceb5d2e8fe340a4) vs this head (step 4b4b80b33807e84dbb37cbfc, optimized build):

frame timebase, switch onthis head, switch onthis head, switch off
key presses, p50 / p95 / p99 / max0.130 / 0.139 / 0.152 / 0.291 ms0.119 / 0.130 / 0.134 / 0.154 ms0.110 / 0.120 / 0.123 / 0.144 ms
closes, p50 / p95 / p99 / max0.143 / 0.168 / 0.320 / 0.375 ms0.135 / 0.143 / 0.180 / 0.187 ms0.131 / 0.140 / 0.184 / 0.193 ms

Each run is one process on one owned Mac (base aws-m4pro-7, head aws-m4pro-9), so the base/head difference is mostly host noise: S3d adds no measurable cost to a close frame (budget 4 ms per 120 Hz frame).

PaneMotionWorkTests (work per frame): passed at the base and at this head.

Review

  • Codex (gpt-6.1-sol, codex exec --sandbox read-only, no tools on this VM: the whole PR diff, the changed files in full and their callees in the prompt). Round 1 at 447bb3ad31d: FIX (a camera travel left stale by a later retarget); round 2 at 02de7be2a22: FIX (a pane riding a closing pane had no rest to collapse toward); round 3 at d369376ebd9: SHIP, no findings; round 4 at 500b3f1756d: SHIP, no findings; round 5 at this head 2f13aa409db: SHIP, no findings. Receipt comment below.

Changelog

Fixed: During a pane, column or row close, the resize handle that stays is always in the middle of a visible gap, the last column of a strip and the last row of a scrolling column close without showing the window background, and two closes in a row leave no background between or after them.

Proof

No capture: a motion change; the refactor owner's 120 Hz fixture films (judge_pixel, and R-HARNESS pair C once it reads the trace's closing key) are the proof: px-close-pane-3t-s1r (case: kept divider on the closing pane's trailing side), px-close-pane-3-s1r, px-close-column-c3-s1r. GUI proof on a display worker (--display --label gui-session-v1) at this head: step 2b1e395042b6aeb1785414cb (cmuxs-Mac-mini-3): passed 8 of 8 suites (PaneCloseKeptHandleTests, PaneCloseCameraGroundTests, PaneGroundCoversPaneTests, PaneContentPolicyTests, CloseTeardownAfterLandingTests, ColumnCloseScrollMotionTests, RowCloseMotionTests, PaneCloseMotionTests).

Observed / Not run

Observed (fleet, read from the step logs): the red steps above failing for their reasons and the guards passing; the greens and the bench above; the release compile-check.
Not run or not observed by me: the 120 Hz films and typing (the refactor owner runs them on one mini); a close in a docked column or a scroll area beside a closing pane in a unit test (handleStays filters handles by the pane's space; read, not run); a user pan during a close.