Layout: justify, so leftover space has a policy - #73
Merged
Conversation
Slack in a container had exactly one destination: flexible children absorbed it, and anything else was a spacer inserted by hand. That is layout state encoded as a sibling, and it does not survive somebody reordering the children. `justify` says where it goes: start, end, center, space-between, space-around, space-evenly. It defaults to start, which is what every layout did before this existed, and it only ever applies when there IS slack -- a container holding any fr or fill child has none, because that child already absorbed it. So it is inert exactly where it would otherwise fight with the constraints. Two decisions worth stating: - center floors, so an odd cell falls after the content rather than before it. That is what CSS and ratatui do, and what reads as centred. - Rounding each gap on its own loses a cell here and gains one there. Rounding the cumulative offset and taking differences means the parts add up to exactly the slack, in every mode and however awkward the division. A property test covers slack 0..17 across 1..6 children in all six modes. All six ports, cross-checked rather than assumed: 390 distribution cases in TypeScript, Rust, Go, Python, C and Zig, diffed against the reference and byte-for-byte identical. Every existing frame is unchanged. Closes #58 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
ralyodio
added a commit
that referenced
this pull request
Sep 8, 2026
Both new files imported describe/expect from "bun:test". Every other test in the package uses node:test with assert, and for a reason: CI runs the suite twice, once under bun and once as `node --test`, and bun:test does not exist in the second. They passed locally and failed seven jobs. The justify file went in with #73 before its checks came back, which is on me; it is fixed here alongside the one it would have broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
ralyodio
added a commit
that referenced
this pull request
Sep 8, 2026
* Panels: draw only some border sides (#59) A border was all four sides or nothing. Anything else meant a divider plus manual padding, which gives you no corner joins and no way to say "just a top rule" -- the thing you reach for building a header strip, a sidebar rail, or a footer that should not look boxed in. ui.panel({ title: "Header", border: "single", sides: ["top"] }, ...) `sides` takes "all" (the default), "none", or a list. The interior follows the sides actually drawn, so a top-only panel costs one row rather than two. The bit vocabulary the collapse work left behind did most of it, as #59 guessed it would. A corner belongs to the two sides that meet there, so it exists only when both are drawn; where one is, the rule runs straight through the cell the corner would have occupied. That is borderGlyph with the bits for the sides present, falling back to the plain rule when only one edge is set -- a single edge has no glyph of its own because that cell is part of a run, not a corner. A title and a subtitle live on the top rule and a footer on the bottom, so none of them are drawn when their rule is absent: painting a title over the first row of content is worse than leaving it out. All six ports. Defaulting to all four sides keeps every existing frame identical, which the suites confirm: TypeScript 249, Rust green, Go both packages, Python 36, Zig 17/17, C and C++ 9/9. Closes #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy * Write the new tests for the runner CI actually uses Both new files imported describe/expect from "bun:test". Every other test in the package uses node:test with assert, and for a reason: CI runs the suite twice, once under bun and once as `node --test`, and bun:test does not exist in the second. They passed locally and failed seven jobs. The justify file went in with #73 before its checks came back, which is on me; it is fixed here alongside the one it would have broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy * Keep the C conformance mirror in step with hq_box_options c_conformance segfaulted. The ctypes mirror of hq_box_options is passed by value, so a field missing from it shifts every field after it and the C side reads whatever is next on the stack -- for this struct, the title, subtitle and footer pointers. `collapse` was already missing, from before this branch. It happened not to crash: the shifted reads landed on zeroes. Adding `sides` moved the window far enough that they did not, which is the only reason anybody found out. Both fields are in the mirror now, and the comment says what it has to stay in step with. 10/10 ctest, including the conformance run that CI does and a local build-fix directory did not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy --------- Co-authored-by: Claude Opus 5 (1M context) <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.
Closes #58.
Slack in a container had exactly one destination: flexible children absorbed it,
and anything else was a spacer inserted by hand. That is layout state encoded as
a sibling, and it does not survive somebody reordering the children.
"start" | "end" | "center" | "space-between" | "space-around" | "space-evenly",defaulting to
start— what every layout did before this existed. It only everapplies when there is slack: a container holding any
frorfillchild hasnone, because that child already absorbed it. So it is inert exactly where it
would otherwise fight with the constraints.
Two decisions worth stating
centerfloors, so an odd cell falls after the content rather thanbefore it. That is what CSS and ratatui do, and what reads as centred.
cell here and gains one there. Rounding the running offset and taking
differences means the parts add up to exactly the slack, in every mode and
however awkward the division.
Verified across all six ports, not assumed
distributewas run for every mode × 1–5 children × slack 0–12 — 390 cases — ineach language and diffed against the TypeScript reference:
Plus a property test over slack 0–17 × 1–6 children × all six modes asserting no
cell is invented or lost, children never overlap, and nothing leaves the rect.
Every suite green with no frame changed: TypeScript 251, Rust full, Go both
packages, Python 36, Zig 17/17, C/C++ 9/9.
ports/zig/examples/justify_parity.zigis left in the tree as the harness thatproduced the Zig column.
🤖 Generated with Claude Code
https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy