Apply pane zoom by collapsing splits in place instead of swapping the rendered node

JO
3mo ago
+438-8

Problem

Toggling pane zoom rendered zoomedNode ?? rootNode, which changes SwiftUI structural identity — the entire AppKit split hierarchy (every NSSplitView plus one NSHostingController per pane) was torn down on maximize and fully rebuilt on minimize. Cost scales with pane count: with 8 panes, maximize took 443.8 ms and minimize 1303.4 ms median (debug), and minimize recreated all 8 pane content views. In cmux this shows up as a very laggy cmd+shift+enter with multiple panes open.

Fix

Zoom keeps the tree mounted and collapses the side of each split that does not contain the zoomed pane (hidden arranged subviews are excluded from NSSplitView tiling, so the zoom path expands to the full container).

  • Synchronous apply: split coordinators register a collapse applier with SplitViewController; the AppKit collapse runs in the same runloop turn as togglePaneZoom() instead of waiting for SwiftUI to re-thread state through every nested hosting controller (display-paced, hundreds of ms on deep trees).
  • Idempotent backstop: updateNSView re-applies the same state reading live controller state, so stale SwiftUI passes cannot flash the pre-toggle layout (restore-then-recollapse flap).
  • Model protection: divider sync/persistence and entry-animation placement are suspended while a split is zoom-collapsed, so the temporary full-size layout never overwrites the stored divider ratio.

Results

New window-hosted benchmark Tests/BonsplitTests/ZoomTogglePerformanceTests.swift (8 panes, 12 cycles, swift test --filter ZoomTogglePerformanceTests):

metric (median)beforeafter
maximize settle443.8 ms~170 ms*
minimize settle1303.4 ms3.3 ms
pane content NSViews recreated per maximize10
pane content NSViews recreated per minimize80

* residual is offscreen-window SwiftUI update scheduling in the headless harness; a probe confirms AppKit frames are final within the toggle call. In cmux (11 panes, real key events) end-to-end settle went from 315→115 ms (maximize) and 825→130 ms (minimize), with terminal-portal detach/attach per minimize going from 18–20 to 0.

Full benchmark report: https://polite-sandal-rgcv.here.now/

All 186 package tests pass.

Note for cmux integration: cmux's WorkspaceContentView re-keyed the Bonsplit subtree with .id(zoomState) (workaround for stale pane chrome above browser portals under the old node-swap rendering). That re-key must be removed together with this change, and pane content visibility made zoom-aware, since collapsed panes now stay mounted; the cmux-side commit is prepared to land with the submodule bump.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Apply pane zoom by collapsing the non-zoomed side of each split in place, and move divider syncing out of SwiftUI. Keeps the AppKit split tree mounted, makes zoom toggles much faster, and cuts CPU during resize storms.

  • Refactors

    • Always render the full tree; hide the side that doesn’t contain the zoomed pane in each split.
    • Synchronous collapse: SplitViewController notifies registered split coordinators on zoomedPaneId change in the same runloop turn.
    • Idempotent backstop: updateNSView re-applies collapse using live controller state to avoid layout flashes.
    • Divider syncing: observe the model’s dividerPosition and apply changes directly to NSSplitView (no updateNSView dependency, no layoutSubtreeIfNeeded on ticks); reduces main-thread work under heavy resizes (cmux: app CPU ~93% → ~50–60%).
    • Suspend divider syncing/persistence and entry animations while zoomed; restore the model ratio on unzoom.
    • SplitViewContainer passes zoomedPaneId down; appliers are registered in makeNSView and unregistered in dismantleNSView.
    • Added ZoomTogglePerformanceTests.swift (8 panes): minimize 1303.4 ms → 3.3 ms; maximize 443.8 ms → ~170 ms; pane content NSView churn → 0.
  • Migration

    • In cmux, remove .id(zoomState) from WorkspaceContentView and make pane content visibility zoom-aware, since collapsed panes now remain mounted.

Summary by CodeRabbit

  • Bug Fixes
    • Improved pane zooming/restoration across nested split layouts by collapsing the non-zoomed side and reliably restoring the previous layout.
    • Reduced visual flicker and eliminated unnecessary split hierarchy rebuilding when toggling zoom.
    • Kept divider behavior consistent during zoom by suspending/safeguarding divider syncing and position persistence.
  • New Features
    • Enabled more accurate zoom-aware propagation so nested split containers respond correctly to the current zoomed pane.
  • Tests
    • Added performance benchmarks for zoom toggle latency and view churn in an 8-pane window scenario.