Skip to content

feat(api): RowBuilder.columns() — fixed / auto / weight row columns#235

Merged
DemchaAV merged 1 commit into
developfrom
feat/row-columns
Jun 25, 2026
Merged

feat(api): RowBuilder.columns() — fixed / auto / weight row columns#235
DemchaAV merged 1 commit into
developfrom
feat/row-columns

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

A row could split its width only by weights(...) or evenly. There was no way to size a column to its content or to a fixed width — so a table-of-contents row (a label, a dotted leader, a right-aligned page number) couldn't be expressed without measuring the gap by hand.

What changed

  • RowBuilder.columns(DocumentRowColumn...) sizes each column as fixed(pt), auto() (intrinsic content width, capped at the row — content still wraps, it never overflows), or weight(w) (a share of the space left after the fixed and auto columns). Resolution order: fixed → auto → weight-remainder. Over-constrained (fixed + auto exceed the row) throws a clear IllegalArgumentException.
  • DocumentRowColumn (new public value type in document.style) — the row peer of DocumentTableColumn, same auto()/fixed() vocabulary, plus weight().
  • Paired with line().fill() (PR feat(api): LineBuilder.fill() — line stretches to its available width #234) it draws a dot-leader row: columns(auto(), weight(1), auto()) with a fill line in the middle. The label and page number size to their content; the leader fills between them.
  • weights(...) / evenWeights() stay as sugar; columns(...) and weights(...) are mutually exclusive (setting one clears the other; the node rejects both).

Engine: the explicit-column distribution lives in a shared RowSlots helper called by both the compile and measure phases, so the two stay in lockstep by construction. The existing weight / even path is untouched — empty-columns rows take the original code, and a weight-only column list resolves to exactly the same widths. So existing rows are byte-identical.

Lane: canonical DSL + node + style, shared-engine layout. DocumentRowColumn is a new type and the RowNode component is additive (back-compat ctors preserved) → japicmp-safe.

Verification

  • ./mvnw test -pl .green, 0 visual baselines changed.
  • RowColumnsTest: fixed+weight (40/160), weight-only columns byte-identical to plain weights (exact A/B), content sizing + weight remainder, over-constrained throws, a long auto label clamps to the row, the node rejects both weights+columns, and the dot-leader TOC row fills exactly between label and number.
  • A 3-lens cold review (lockstep, correctness, API-shape) confirmed measure/compile receive provably-equal inputs and the weight path is bit-identical to legacy.
  • Runnable RowColumnsExample (a table-of-contents block) with a committed preview and an examples README row.

This is the second half of the table-of-contents groundwork (after pageIndex() #231 and line().fill() #234); the native TOC builds on it next.


@Test
void fixedColumnTakesItsPointWidthAndWeightTakesTheRemainder() {
render(page -> page.addRow(r -> r.gap(0).columns(fixed(40), weight(1))

@Test
void weightOnlyColumnsResolveLikePlainWeights() {
render(page -> page.addRow(r -> r.gap(0).columns(weight(1), weight(3))

@Test
void intrinsicColumnSizesToContentAndWeightTakesRemainder() {
render(page -> page.addRow(r -> r.gap(0).columns(auto(), weight(1))
@Test
void overConstrainedFixedColumnsThrow() {
try (DocumentSession session = smallPage()) {
session.pageFlow(page -> page.addRow(r -> r.gap(0).columns(fixed(150), fixed(150))
@Test
void dotLeaderRowFillsBetweenTheLabelAndTheNumber() {
double gap = 4;
render(page -> page.addRow(r -> r.gap(gap).columns(auto(), weight(1), auto())

@Test
void weightColumnsAreByteIdenticalToPlainWeights() {
double[] viaWeights = twoFillWidths(r -> r.gap(0).weights(1, 3));
@Test
void weightColumnsAreByteIdenticalToPlainWeights() {
double[] viaWeights = twoFillWidths(r -> r.gap(0).weights(1, 3));
double[] viaColumns = twoFillWidths(r -> r.gap(0).columns(weight(1), weight(3)));
// An auto column whose content is wider than the row clamps to the row
// width (the content wraps) instead of overflowing or throwing.
try (DocumentSession session = smallPage()) {
session.pageFlow(page -> page.addRow(r -> r.gap(0).columns(auto())
A row split its width by weights or evenly; there was no way to size a
column to its content or to a fixed width, so a table-of-contents row
(label, dotted leader, page number) was not expressible. columns(...)
sizes each column as DocumentRowColumn.fixed(pt), auto() (content width),
or weight(w) (a share of the remainder), resolved fixed -> auto ->
weight. With line().fill() it draws a dot leader that fills the gap.

The explicit-column distribution lives in a shared RowSlots helper called
by both the compile and measure phases, so the two stay in lockstep; the
existing weight / even path is untouched and a weight-only column list
resolves to exactly the same widths, so existing rows are byte-identical.
weights(...) stays as sugar; columns and weights are mutually exclusive.

Tests: RowColumnsTest covers the fixed/auto/weight mix, weight-only
columns byte-identical to plain weights, content sizing plus the weight
remainder, over-constrained columns throwing, a long auto label clamping
to the row, and the dot-leader table-of-contents row. Example:
RowColumnsExample (a table-of-contents block). Full suite green, no
visual baselines changed.
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.

2 participants