Repository navigation
layout: a split of a carded place opens even halves - #465
Merged
Merged
Conversation
2 of 5 tasks
foxnne
marked this pull request as ready for review
October 16, 2026 20:15
2 of 5 tasks
A place's recorded size was its content rect, but a split's extent is the new half's whole reach, padding included. Every split of a carded place (fizzy's Main and Panel, a float's places) gave the new half the card's padding twice over: 886 | 854 px for Main, 494 / 462 px for its new half. `State.setPlaceMetrics` now records the border box, which is all a split reads it for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
layout/split-reach
branch
from
October 16, 2026 21:15
1af9263 to
f39f221
Compare
foxnne
added a commit
that referenced
this pull request
Oct 16, 2026
Stacked on #465 (it reuses that PR's `ColumnCardFrame` test frame); review after it, or retarget to `main` once it merges. This is the follow-up #465 named. ## What changes Take Main | (Main/r1 over Main/r1/b1). Emptying **Main/r1** (carrying its last view to the trash or elsewhere), or picking **Remove** on it, now closes just that half. b1's views move into Main/r1, and the column keeps its name, its width and its link in `layout.zon`. That's the same rule as emptying the half that was split anywhere else (SPLITS.md, "Removing a split"). If b1 is split again too, there's no single place to merge into, and Main/r1 stays, empty. **Before:** Main/r1 is two places under one name: the half Main's split minted, and the half its own split kept. `closeEmptied` saw "minted" and slid shut the branch box registered under that name, which is the whole column. Then `Forest.collapse("Main/r1")` matched the inner *leaf*, so b1 was promoted into Main's split. b1 took the column at an extent that was its height (195 pt in the test). The only link left in `layout.zon` still named the gone `Main/r1`, and b1 had no link, so it was lost on reload. **The model change:** - **`SplitTree.Forest.collapse`** takes the outermost node that carries the name: for a place split again, that's the branch its split made, never the leaf that kept the name. New `isSplit` and `placeLeaves` read the same rule. - **`ViewDrag.closeEmptied`** treats a minted half that was split again as an origin: it merges in its other half, or stays. The **Picker's Remove** goes through it for such a place. - **`Region.persistExtent`** counts a column as kept while *any* place in it holds a view. When the column closes, it forgets every place in it, not only the one whose name the column carries. Before, closing a column with an empty Main/r1 and a full b1 would have left b1's views assigned to a place that no longer existed. It walks the forest only once the place has shut, not every frame. - **The picker** offers no Remove on a half that was split again when its other half is split too: there's nothing to merge, and Remove would only have cleared it. - **A use-after-free while drawing.** When a place was forgotten in the middle of drawing its branch, the tree went on drawing children that `collapse` had just freed. The old code had the same hazard on a rarer path. `persistExtent` now returns whether it dropped the place, and `initTree` and the `.branch` arm draw nothing more of it that frame (a refresh is already queued). The new column-close test caught this: the freed b1 leaf re-stored its extent after the column had cleared it. A leaf that was never split again behaves exactly as before. Every changed path checks `isSplit` first, and `placeLeaves` of a plain leaf is that leaf alone. ## SDK impact - [x] None (`app/` only; `test-sdk-version` passes) ## Verified - [x] macOS: - integration tests (new) on fizzy's forest with a carded Main: - `split: emptying the half that was split again closes only that half`. It failed before the fix: Main/r1 was gone and b1 sat in Main's split. - `split: emptying the half that was split again stays when its other half is split too`. - `split: a column shut by its sash goes only once every place in it is empty`: shut with a view in b1, it stays; emptied, the whole column goes and leaves no assignment or extent behind. - unit tests in `SplitTree.zig`: collapsing a place split again drops its whole branch, and collapsing the minted half of a nested split leaves its origin, with one link. - sandbox fizzy (`--profile`, through the agent plugin, macOS 2×): - Main → Split → Vertical gives 870 \| 870 px. - Main/r1 → Split → Horizontal gives 478 / 478 px, with the column's extent unchanged at 435 pt. - Output into b1, then Main/r1 → Remove gives Main 870 \| Main/r1 870 px. Main/r1 holds Output, and `layout.zon` has one link (`Main/r1` from `Main`, right) and no b1. - Remove on b1 itself still closes just b1, as before. - `scripts/check-fmt.sh`, `zig build`, `zig build test` (91/91 steps), `zig build test-integration` (53/53 steps), `zig build check-tests-reached`, `zig build check-web`, `zig build test-sdk-version`. - [ ] Windows: - [ ] Linux: - [ ] Web: builds ## Follow-ups - **The deeper fix is a name of its own for a branch.** A branch and the half that kept its name share one name, and this PR special-cases that (`isSplit`) where it bites: `closeEmptied`, the picker's Remove and `persistExtent`. Anything else that resolves a place by name (`regionNamed`, which finds the branch box first; `placeSize`) still sees two places. Naming branches separately would need a `layout.zon` migration for saved links, so it's its own change. - Picker buttons named for drivers: #468. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while chasing the uneven nested split reported in #464's follow-ups.
What changes
A split of a carded place now opens two even halves. Before, the new half was short by the card's padding on both sides. That covers fizzy's Main and Panel, and every split of their halves. At 2× on macOS:
Why. A split's extent is the new half's whole reach, border to border, padding included (
Region.init).splitOnhalved the place's recordedsize, which was its content rect.State.setPlaceMetricsnow records the border box, and it takes the box'sWidgetData, so all three call sites measure it the same way. A split is the only thing that readssize(placeSize, and a drag's frozenTarget.size).The suspected cause was not it. #464 suspected the
.brancharm: it sizes the branch byextent(b.origin), the same name the kept leaf has. But the kept origin is always the leftover half of its own branch (resize = false), so it never writes an extent under that name. Inside one tree,extent(name)always means the minted leaf's reach along its parent's axis, before and after that leaf is split again. Persistence round-trips too: the nested split came back at the same sizes after a restart.The 1:3 in #464 comes from its own carry-over. I reproduced it on #464's head (f927a7f) with its recipe: a
panes.zonofPane 0| (Pane 4overPane 5), relaunched withFIZZY_WORKSPACES=1. Main/r1 came out 248 px over Main/r1/b1 708 px. The stored extent for Main/r1/b1 was 354 pt, which is exactly (718 − 10) × 0.5. 718 pt is Main's content height without the 236 pt Panel under it. Sosettlemeasures Main on a frame before the Panel has its room, and hands that height tosplitOnSized. That fix belongs in #464.SDK impact
test-sdk-versionpasses;app/only)Verified
a split of a carded place, and of its new half, opens even halves. It uses fizzy's shape: a carded Main withpane_cards, in a column, and compares the halves' drawn rects, not thesizethis PR redefines. Before the fix it failed at 187 vs 171.--profile, driven through the agent plugin): Main → Split → Vertical, then Main/r1 → Split → Horizontal, measured as in the table.scripts/check-fmt.sh,zig build,zig build test(91/91 steps),zig build test-integration(53/53 steps),zig build check-tests-reached,zig build check-web,zig build test-sdk-version.Follow-ups
settleruns. ItssplitOnSizedgets the reach fix from this PR for free.🤖 Generated with Claude Code