feat(api): row flexSpacer / pushRight / arrangement — main-axis row layout#240
Merged
Conversation
…ayout
A row distributed its width by even split, weights, or columns, and packed its
children at the start; aligning a title left with a status flush right, or
spreading a nav bar edge-to-edge, meant inserting hand-sized spacers.
flexSpacer() (and the pushRight() alias) add an invisible spring — a SpacerNode
with a grow factor — that absorbs the row's leftover width and pushes the
children around it apart; several springs share the leftover in proportion to
their grow. arrangement(RowArrangement.{START, CENTER, END, SPACE_BETWEEN,
SPACE_AROUND, SPACE_EVENLY}) justifies content-sized children instead — the
justify-content analogue. Flex is mutually exclusive with weights and columns.
The slot math is a single shared helper (RowSlots.distributeFlex) called
identically by the measure and compile phases, so the two cannot drift. The flex
path is gated behind RowSlots.hasFlexLayout as the first branch in both phases; a
row with no grow spacer and the default START arrangement never enters it, and
the cursor's leading / extra-gap terms are 0, so existing rows are byte-for-byte
unchanged (full suite green, no visual baselines moved).
Tests: RowFlexTest pins the placement for pushRight, two springs sharing by
grow, and START/CENTER/END/SPACE_BETWEEN/SPACE_AROUND/SPACE_EVENLY against a
240pt row, and rejects a non-finite grow and flex combined with weights or
columns. Example: RowFlexExample (a pushRight header, a SPACE_BETWEEN nav bar,
a CENTER footer).
|
|
||
| @Test | ||
| void flexSpacerPushesFollowingChildrenToTheRightEdge() { | ||
| try (DocumentSession session = row(r -> r.gap(0) |
| @Test | ||
| void twoFlexSpacersShareTheLeftoverByGrow() { | ||
| // grow 1 then grow 3 → leftover (200-20 = 180) splits 45 / 135. | ||
| try (DocumentSession session = row(r -> r.gap(0) |
|
|
||
| @Test | ||
| void spaceBetweenPinsFirstAndLastToTheEdges() { | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.SPACE_BETWEEN) |
| @Test | ||
| void centerArrangementCentersTheContent() { | ||
| // 3 x 20 = 60 content, leftover 140, leading 70 → a at 20+70 = 90. | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.CENTER) |
|
|
||
| @Test | ||
| void endArrangementPushesTheContentRight() { | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.END) |
| void spaceAroundPutsEqualSpaceAroundEachChild() { | ||
| // 3 x 20 = 60, leftover 140, gap 140/3 ≈ 46.67, leading half-gap ≈ 23.33 | ||
| // → a at 43.33, b centred at 110. | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.SPACE_AROUND) |
| @Test | ||
| void spaceEvenlyPutsEqualGapsIncludingTheEdges() { | ||
| // four equal 35pt gaps → a at 55, b at 110, c at 165. | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.SPACE_EVENLY) |
…w doc Add a RowFlexTest case where a SPACE_BETWEEN row's middle child is a section (a composite column), exercising the composite cursor-advance branch with a non-zero arrangement gap — the last child still lands flush right. Correct the flexSpacer(grow) Javadoc to say the factor is >= 0 (0 is a rigid spacer), matching SpacerBuilder.grow and the SpacerNode validation.
| // The middle child is a section (a composite column), so its cursor advance | ||
| // goes through the composite branch — confirm the SPACE_BETWEEN extra gap is | ||
| // applied there too, pushing the last child flush right. | ||
| try (DocumentSession session = row(r -> r.gap(0).arrangement(RowArrangement.SPACE_BETWEEN) |
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.
Why
A row distributed its width by even split, weights, or columns and packed its children at the start. Aligning a title left with a status flush right, or spreading a nav bar edge-to-edge, meant inserting hand-sized spacers. This is the main-axis half of the row-layout work (
verticalAlign#239 was the cross-axis half).What changed
RowBuilder.flexSpacer()/flexSpacer(grow)/pushRight()+SpacerBuilder.grow(...)— an invisible spring (aSpacerNodewith a grow factor) that absorbs the row's leftover width and pushes the children around it apart; several springs share the leftover in proportion to their grow.RowBuilder.arrangement(RowArrangement.{START, CENTER, END, SPACE_BETWEEN, SPACE_AROUND, SPACE_EVENLY})— thejustify-contentanalogue, justifying content-sized children. Flex is mutually exclusive withweights/columns(rejected at construction).How (the hot-path discipline)
RowSlots.distributeFlex, called identically by the measure (NodeDefinitionSupport.measureRow) and compile (LayoutCompiler.compileHorizontalRow) phases — the two distribution sites cannot drift (the same patterncolumns()feat(api): RowBuilder.columns() — fixed / auto / weight row columns #235 used).RowSlots.hasFlexLayout(node)as the first branch in both phases; a row with no grow spacer and the defaultSTARTarrangement never enters it, the existing columns/weights/even chain is literally untouched, and the cursor'sflexLeading/flexExtraGapterms are0.0— so existing rows are byte-for-byte unchanged. (Both cursor-advance sites — composite and leaf — were updated together; a miss in one would desync placement.)Lane: canonical DSL (
RowBuilder,RowArrangement,SpacerBuilder,RowNode,SpacerNode) + a shared-engine slot helper. Purely additive → japicmp-safe (the newRowNode13th andSpacerNode6th components ship behind re-declared back-compat delegating constructors).Verification
./mvnw test -pl .— green, 0 changed visual baselines → existing rows render identically../mvnw -P japicmp verify— binary-compatible.RowFlexTest(11): pins placement forpushRight(status flush right), two springs sharing by grow, andCENTER/END/SPACE_BETWEEN/SPACE_AROUND/SPACE_EVENLYagainst a 240pt row; rejects a non-finite grow and flex combined with weights and columns.SPACE_AROUND/SPACE_EVENLY/ flex+columns tests.RowFlexExample+ committed preview: apushRight()invoice header, aSPACE_BETWEENnav bar, aCENTERfooter.This completes G6 (row main + cross axis):
verticalAlign(#239) + this.