Repository navigation
layout: removing a half that was split again keeps its column - #467
Merged
Merged
Conversation
foxnne
marked this pull request as ready for review
October 16, 2026 20:30
3 of 6 tasks
A minted half split again (Main/r1 in Main | (Main/r1 over Main/r1/b1)) is two places under one name: the half Main's split made, and the half its own split kept. Emptied or removed, it closed its whole column, and the forest then collapsed the inner leaf, promoting Main/r1/b1 into Main's split sized by its height and with no link left in layout.zon. `Forest.collapse` now takes the outermost node carrying a name, a branch for a place split again, never the leaf that kept the name. Emptying or removing such a place goes the way an origin goes: its other half's views move into it and that half closes; with that half split too, it stays, and the picker offers no Remove there. A column counts as kept while any place in it holds a view, and closing it forgets them all. A place forgotten while its branch was being drawn went on drawing children `collapse` had just freed; `persistExtent` now says when it dropped the place, and the tree draws nothing more of it that frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
layout/nested-split-remove
branch
from
October 16, 2026 21:15
a511e38 to
083793f
Compare
2 of 5 tasks
foxnne
added a commit
that referenced
this pull request
Oct 16, 2026
## What changes A tape or an agent can aim at the picker's controls by name, not by position: | Name | What | |---|---| | `picker.mode:one`, `picker.mode:many` | Single / Multiple | | `picker.action:split`, `:clear`, `:defaults`, `:remove` | the actions, each only while it can be pressed (a dimmed Clear has no name) | | `picker.split:vertical`, `picker.split:horizontal` | the rows of the Split menu | These sit beside the cards' existing `picker:<view id>`, which is now in `docs/AUTOMATION.md`'s anchor table along with the new names. **Why.** The row's buttons come and go with the place. Remove sits where Split sat on a place with nothing to Clear, so a point read off one place's picker can land on a different action in the next. While driving splits through the agent plugin for #467, a click aimed at Split hit Remove twice and closed the place. **How.** - `modeButton` and `actionButton` draw through `namedButton`: `dvui.button`'s body with a `core.anchor.mark`, since `dvui.button` doesn't hand back its widget. - `core.widgets.menuRow` gets an `anchor` option for a row that runs no command (a command row is already `fizzy.command:<id>`). - A dimmed action (Clear with nothing to clear, Defaults with no list) publishes no name: it swallows the click, so `wait_for` / `expect` on a name means the action can run. - `core.anchor.mark` costs nothing unless someone asked for anchors this frame. ## SDK impact - [x] Core-only or additive: `MenuRowOptions.anchor` reaches plugins at the next SDK release (`test-sdk-version` passes; the fingerprint didn't move). ## Verified - [x] macOS: integration test `picker: its controls are named for a tape or an agent to aim at, while they can be pressed` (anchors wanted, the picker drawn over a place: Single, Multiple and Split are named; a dimmed Clear and a shape place's absent Remove are not). - [x] macOS: sandbox fizzy (`--profile`), driven through the agent plugin's `fizzy_run` by name only: - opened Main's picker: `fizzy_screen` lists `picker.mode:one`, `picker.mode:many`, `picker.action:split`, `picker.action:clear` and `picker.action:defaults`; - clicked `picker.action:split`, and the menu listed `picker.split:vertical` and `picker.split:horizontal`; - clicked `picker.split:vertical`, and Main split into Main | Main/r1; - opened Main/r1's picker, `expect`ed `picker.action:remove`, clicked it, and Main was whole again. - `scripts/check-fmt.sh`, `zig build`, `zig build test` (93/93 steps), `zig build test-integration` (53/53 steps), `zig build check-tests-reached`, `zig build check-web`, `zig build test-sdk-version`, `scripts/check-examples.sh`. - [ ] Windows: - [ ] Linux: - [ ] Web: builds ## Follow-ups - `namedButton` repeats `dvui.button`'s body to reach the widget's data, because `dvui.button` doesn't hand it back. A `dvui.button` variant in the dvui-dev fork that exposes it would keep one body (`docs/DEPENDENCIES.md`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
added a commit
that referenced
this pull request
Oct 16, 2026
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: | | before | after | |---|---|---| | Main → Split → Vertical | Main 886 px, Main/r1 854 px | 870, 870 | | then Main/r1 → Split → Horizontal | Main/r1 494 px, Main/r1/b1 462 px | 478, 478 | **Why.** A split's extent is the new half's whole reach, border to border, padding included (`Region.init`). `splitOn` halved the place's recorded `size`, which was its *content* rect. `State.setPlaceMetrics` now records the border box, and it takes the box's `WidgetData`, so all three call sites measure it the same way. A split is the only thing that reads `size` (`placeSize`, and a drag's frozen `Target.size`). **The suspected cause was not it.** #464 suspected the `.branch` arm: it sizes the branch by `extent(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.zon` of `Pane 0` | (`Pane 4` over `Pane 5`), relaunched with `FIZZY_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. So `settle` measures Main on a frame before the Panel has its room, and hands that height to `splitOnSized`. That fix belongs in #464. ## SDK impact - [x] None (`test-sdk-version` passes; `app/` only) ## Verified - [x] macOS: - new integration test `a split of a carded place, and of its new half, opens even halves`. It uses fizzy's shape: a carded Main with `pane_cards`, in a column, and compares the halves' drawn rects, not the `size` this PR redefines. Before the fix it failed at 187 vs 171. - sandbox fizzy (`--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`. - [ ] Windows: - [ ] Linux: - [ ] Web: builds ## Follow-ups - #464: measure Main once the layout under it has settled (or from the window), not on the first frame `settle` runs. Its `splitOnSized` gets the reach fix from this PR for free. - Removing a half that was split again: fixed in #467, stacked on this one. 🤖 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.
Stacked on #465 (it reuses that PR's
ColumnCardFrametest frame); review after it, or retarget tomainonce 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.
closeEmptiedsaw "minted" and slid shut the branch box registered under that name, which is the whole column. ThenForest.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 inlayout.zonstill named the goneMain/r1, and b1 had no link, so it was lost on reload.The model change:
SplitTree.Forest.collapsetakes 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. NewisSplitandplaceLeavesread the same rule.ViewDrag.closeEmptiedtreats 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.persistExtentcounts 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.collapsehad just freed. The old code had the same hazard on a rarer path.persistExtentnow returns whether it dropped the place, andinitTreeand the.brancharm 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
isSplitfirst, andplaceLeavesof a plain leaf is that leaf alone.SDK impact
app/only;test-sdk-versionpasses)Verified
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.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.--profile, through the agent plugin, macOS 2×):layout.zonhas one link (Main/r1fromMain, right) and no b1.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
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 andpersistExtent. 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 alayout.zonmigration for saved links, so it's its own change.Picker buttons named for drivers: layout: the picker's buttons are named for tapes and agents #468.
🤖 Generated with Claude Code