Skip to content

Layout: justify, so leftover space has a policy - #73

Merged
ralyodio merged 1 commit into
mainfrom
feat/justify
Sep 8, 2026
Merged

Layout: justify, so leftover space has a policy#73
ralyodio merged 1 commit into
mainfrom
feat/justify

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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.

ui.row({ justify: "space-between" }, (r) => { ... });

"start" | "end" | "center" | "space-between" | "space-around" | "space-evenly",
defaulting to start — what every layout did before this existed. 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 is cumulative, not per-gap. Rounding each gap on its own loses a
    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

distribute was run for every mode × 1–5 children × slack 0–12 — 390 cases — in
each language and diffed against the TypeScript reference:

port result
Rust identical, 390/390
Go identical, 390/390
Python identical, 390/390
C identical, 390/390
Zig identical, 390/390

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.zig is left in the tree as the harness that
produced the Zig column.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy

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
ralyodio merged commit fb78833 into main Sep 8, 2026
13 of 18 checks passed
@ralyodio
ralyodio deleted the feat/justify branch September 8, 2026 21:40
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Layout: justify/flex distribution for leftover space

1 participant