Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ PDF `GoTo` actions. External links are unchanged.

### Public API

- **`RowBuilder.flexSpacer()` / `pushRight()` / `arrangement(...)` + `RowArrangement`
+ `SpacerBuilder.grow(...)`** (`@since 1.9.0`). Main-axis (`justify-content`) layout
for a row. A `flexSpacer()` (or `pushRight()`) is an invisible spring that absorbs
the row's leftover width — a title stays left while a badge sits flush right; a
spacer's `grow(...)` factor sets its share. `arrangement(START / CENTER / END /
SPACE_BETWEEN / SPACE_AROUND / SPACE_EVENLY)` justifies content-sized children
instead. Flex is mutually exclusive with `weights` / `columns`. The default
(`START`, no grow) is byte-for-byte unchanged, so existing rows are unaffected.

- **`RowBuilder.verticalAlign(...)` + `RowVerticalAlign`** (`@since 1.9.0`). Seats a
row's children on the cross axis within the row band, whose height is that of the
tallest child: `TOP` (the default), `CENTER`, or `BOTTOM` — the `align-items`
Expand Down
Binary file added assets/readme/examples/row-flex.pdf
Binary file not shown.
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Content bleed](#content-bleed) | `band.bleedToEdge(TOP, LEFT, RIGHT)` / `bleed(DocumentBleed.of(...))` — a section's fill reaches the trimmed page edge while its children stay in the content margin | [PDF](../assets/readme/examples/content-bleed.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BleedExample.java) |
| [Row columns & TOC](#row-columns--toc) | `row.columns(auto(), weight(1), auto())` — size columns by content / fixed points / weight; with `line().fill()` it builds a dot-leader table of contents | [PDF](../assets/readme/examples/row-columns.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/RowColumnsExample.java) |
| [Row vertical align](#row-vertical-align) | `row.verticalAlign(TOP / CENTER / BOTTOM)` — seat a row's children on the cross axis within the band set by the tallest child | [PDF](../assets/readme/examples/row-vertical-align.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/RowVerticalAlignExample.java) |
| [Row flex & arrangement](#row-flex--arrangement) | `row.pushRight()` / `flexSpacer()` springs + `arrangement(SPACE_BETWEEN / CENTER / …)` — push children apart or justify leftover width | [PDF](../assets/readme/examples/row-flex.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/RowFlexExample.java) |

### 📋 Templates recommended

Expand Down Expand Up @@ -512,6 +513,24 @@ flow.addRow(r -> r.verticalAlign(RowVerticalAlign.BOTTOM)
[📄 View PDF](../assets/readme/examples/row-vertical-align.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/layout/RowVerticalAlignExample.java)

### Row flex & arrangement

`RowBuilder.pushRight()` / `flexSpacer()` add an invisible spring that absorbs the
row's leftover width — a header title stays left while a status badge sits flush
right. `arrangement(...)` instead justifies content-sized children across the row
(`SPACE_BETWEEN`, `CENTER`, `END`, `SPACE_AROUND`, `SPACE_EVENLY`) — the
`justify-content` analogue, no manual coordinates. `START` is the default, so
existing rows are unchanged.

```java
flow.addRow(r -> r.addParagraph(title).pushRight().addParagraph(status)); // title left, status right
flow.addRow(r -> r.arrangement(RowArrangement.SPACE_BETWEEN)
.addParagraph(a).addParagraph(b).addParagraph(c)); // spread edge-to-edge
```

[📄 View PDF](../assets/readme/examples/row-flex.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/layout/RowFlexExample.java)

### Advanced tables

`DocumentTableCell.rowSpan(int)` mirrors `colSpan(int)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.demcha.examples.features.chrome.PdfChromeExample;
import com.demcha.examples.features.layout.BleedExample;
import com.demcha.examples.features.layout.RowColumnsExample;
import com.demcha.examples.features.layout.RowFlexExample;
import com.demcha.examples.features.layout.RowVerticalAlignExample;
import com.demcha.examples.features.layout.BlockAlignExample;
import com.demcha.examples.features.lists.NestedListExample;
Expand Down Expand Up @@ -159,6 +160,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + BleedExample.generate());
System.out.println("Generated: " + RowColumnsExample.generate());
System.out.println("Generated: " + RowVerticalAlignExample.generate());
System.out.println("Generated: " + RowFlexExample.generate());
System.out.println("Generated: " + TransformsExample.generate());
System.out.println("Generated: " + TableAdvancedExample.generate());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.demcha.examples.features.layout;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.PageFlowBuilder;
import com.demcha.compose.document.node.RowArrangement;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Runnable showcase for v1.9 row flex layout: {@code pushRight()} / {@code
* flexSpacer()} springs absorb the row's leftover width, and {@code
* arrangement(...)} justifies content-sized children — the {@code justify-content}
* analogue for a horizontal row, no manual coordinates.
*
* <pre>{@code
* flow.addRow(r -> r.addParagraph(title).pushRight().addParagraph(status)); // title left, status right
* flow.addRow(r -> r.arrangement(RowArrangement.SPACE_BETWEEN)
* .addParagraph(a).addParagraph(b).addParagraph(c)); // spread across the row
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class RowFlexExample {

private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
private static final DocumentColor MUTED = DocumentColor.rgb(120, 126, 135);
private static final DocumentColor GREEN = DocumentColor.rgb(22, 128, 70);
private static final DocumentColor BAND = DocumentColor.rgb(238, 241, 246);

private RowFlexExample() {
}

/**
* Renders a {@code pushRight()} header, a {@code SPACE_BETWEEN} nav bar, and a
* {@code CENTER} footer.
*
* @return path to the generated PDF
* @throws Exception if rendering or file IO fails
*/
public static Path generate() throws Exception {
Path pdfFile = ExampleOutputPaths.prepare("features/layout", "row-flex.pdf");

DocumentTextStyle label = DocumentTextStyle.DEFAULT.withSize(12).withColor(INK);

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(400, 280)
.margin(DocumentInsets.of(34))
.create()) {
document.pageFlow(page -> {
heading(page, "Row flex & arrangement");

// pushRight(): title on the left, status flush to the right edge.
band(page, r -> r
.addParagraph(p -> p.text("Invoice #1042").textStyle(label))
.pushRight()
.addParagraph(p -> p.text("PAID")
.textStyle(DocumentTextStyle.DEFAULT.withSize(12).withColor(GREEN))));

// SPACE_BETWEEN: a nav bar spread edge-to-edge.
band(page, r -> r.arrangement(RowArrangement.SPACE_BETWEEN)
.addParagraph(p -> p.text("Overview").textStyle(label))
.addParagraph(p -> p.text("Details").textStyle(label))
.addParagraph(p -> p.text("History").textStyle(label))
.addParagraph(p -> p.text("Settings").textStyle(label)));

// CENTER: a centred footer note.
band(page, r -> r.arrangement(RowArrangement.CENTER)
.addParagraph(p -> p.text("· thank you for your business ·")
.textStyle(DocumentTextStyle.DEFAULT.withSize(11).withColor(MUTED))));
});

document.buildPdf();
}

return pdfFile;
}

private static void heading(PageFlowBuilder page, String text) {
page.addParagraph(p -> p.text(text).textStyle(DocumentTextStyle.DEFAULT.withSize(18).withColor(INK)));
page.addParagraph(p -> p.text("pushRight() / flexSpacer() and arrangement(...) on a row")
.textStyle(DocumentTextStyle.DEFAULT.withSize(9).withColor(MUTED))
.padding(DocumentInsets.bottom(10)));
}

private static void band(PageFlowBuilder page, java.util.function.Consumer<com.demcha.compose.document.dsl.RowBuilder> spec) {
page.addRow(r -> {
r.fillColor(BAND).cornerRadius(8).padding(DocumentInsets.of(12));
spec.accept(r);
});
page.addSpacer(s -> s.height(10));
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
56 changes: 55 additions & 1 deletion src/main/java/com/demcha/compose/document/dsl/RowBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public final class RowBuilder {
private DocumentCornerRadius cornerRadius = DocumentCornerRadius.ZERO;
private DocumentBorders borders = DocumentBorders.NONE;
private RowVerticalAlign verticalAlign = RowVerticalAlign.TOP;
private RowArrangement arrangement = RowArrangement.START;

/**
* Creates a row builder.
Expand Down Expand Up @@ -195,6 +196,58 @@ public RowBuilder verticalAlign(RowVerticalAlign verticalAlign) {
return this;
}

/**
* Sets the main-axis distribution of the row's leftover width when its
* children do not fill it — the {@code justify-content} analogue. Only takes
* effect when the children are content-sized (no weights / columns / grow
* spacer absorbs the slack).
*
* @param arrangement main-axis arrangement; {@code null} resets to
* {@link RowArrangement#START}
* @return this builder
* @since 1.9.0
*/
public RowBuilder arrangement(RowArrangement arrangement) {
this.arrangement = arrangement == null ? RowArrangement.START : arrangement;
return this;
}

/**
* Adds a flex spacer — an invisible spring with {@code grow == 1} that absorbs
* the row's leftover width, pushing the children before and after it apart.
*
* @return this builder
* @since 1.9.0
*/
public RowBuilder flexSpacer() {
return flexSpacer(1.0);
}

/**
* Adds a flex spacer with the given grow factor. Multiple flex spacers share
* the leftover width in proportion to their grow factors.
*
* @param grow grow factor; must be finite and {@code >= 0} ({@code 0} is a
* rigid, zero-width spacer)
* @return this builder
* @since 1.9.0
*/
public RowBuilder flexSpacer(double grow) {
return add(new SpacerBuilder().grow(grow).build());
}

/**
* Inserts a {@link #flexSpacer()} at the current position. With one spacer this
* pushes the children added after it to the right edge; with several it splits
* the leftover width between them.
*
* @return this builder
* @since 1.9.0
*/
public RowBuilder pushRight() {
return flexSpacer();
}

/**
* Replaces the per-child weights used to distribute the row's inner width.
*
Expand Down Expand Up @@ -490,7 +543,8 @@ public RowNode build() {
cornerRadius,
borders,
List.copyOf(columns),
verticalAlign);
verticalAlign,
arrangement);
}

private void validate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public final class SpacerBuilder {
private double height;
private DocumentInsets padding = DocumentInsets.zero();
private DocumentInsets margin = DocumentInsets.zero();
private double grow;

/**
* Creates a spacer builder.
Expand Down Expand Up @@ -68,6 +69,20 @@ public SpacerBuilder size(double width, double height) {
return this;
}

/**
* Sets the flex grow factor. Inside a row, a spacer with {@code grow > 0}
* becomes a spring that absorbs a share of the row's leftover width
* proportional to its grow factor; {@code 0} (the default) is rigid.
*
* @param grow grow factor; must be finite and {@code >= 0}
* @return this builder
* @since 1.9.0
*/
public SpacerBuilder grow(double grow) {
this.grow = grow;
return this;
}

/**
* Sets spacer padding.
*
Expand Down Expand Up @@ -96,6 +111,6 @@ public SpacerBuilder margin(DocumentInsets margin) {
* @return spacer node
*/
public SpacerNode build() {
return new SpacerNode(name, width, height, padding, margin);
return new SpacerNode(name, width, height, padding, margin, grow);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LayerStackNode;
import com.demcha.compose.document.node.PageBreakNode;
import com.demcha.compose.document.node.RowArrangement;
import com.demcha.compose.document.node.RowNode;
import com.demcha.compose.document.node.RowVerticalAlign;
import com.demcha.compose.document.style.DocumentBleed;
Expand Down Expand Up @@ -511,7 +512,27 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
if (!children.isEmpty()) {
List<DocumentRowColumn> columns = node instanceof RowNode row ? row.columns() : List.of();
double[] slotWidths;
if (!columns.isEmpty()) {
// flexLeading / flexExtraGap stay 0.0 for every non-flex row, so the
// cursor math below is byte-identical to the pre-flex placement.
double flexLeading = 0.0;
double flexExtraGap = 0.0;
if (RowSlots.hasFlexLayout(node)) {
slotWidths = RowSlots.distributeFlex(children, layoutSpec.spacing(), childRegionWidth, prepareContext);
RowArrangement arrangement = node instanceof RowNode flexRow
? flexRow.arrangement() : RowArrangement.START;
if (arrangement != RowArrangement.START) {
double available = RowSlots.rowAvailableWidth(
childRegionWidth, layoutSpec.spacing(), children.size());
double used = 0.0;
for (double slot : slotWidths) {
used += slot;
}
double[] justify = RowSlots.flexJustify(
arrangement, Math.max(0.0, available - used), children.size());
flexLeading = justify[0];
flexExtraGap = justify[1];
}
} else if (!columns.isEmpty()) {
double available = RowSlots.rowAvailableWidth(childRegionWidth, layoutSpec.spacing(), children.size());
double[] intrinsic = RowSlots.intrinsicColumnWidths(children, columns, available, prepareContext);
slotWidths = RowSlots.distributeColumns(columns, intrinsic, layoutSpec.spacing(), childRegionWidth, semanticName);
Expand All @@ -522,7 +543,7 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
RowVerticalAlign verticalAlign = node instanceof RowNode rowNode
? rowNode.verticalAlign() : RowVerticalAlign.TOP;
double bandContentHeight = naturalMeasure.height() - padding.vertical();
double cursorX = placementX + padding.left();
double cursorX = placementX + padding.left() + flexLeading;

for (int index = 0; index < children.size(); index++) {
DocumentNode child = children.get(index);
Expand Down Expand Up @@ -570,7 +591,8 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
slotWidth,
FixedSlotKind.ROW_SLOT,
slotCtx);
cursorX += slotWidth + layoutSpec.spacing();
cursorX += slotWidth + layoutSpec.spacing()
+ (index < children.size() - 1 ? flexExtraGap : 0.0);
continue;
}

Expand Down Expand Up @@ -618,7 +640,8 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
childMargin,
childPadding));

cursorX += slotWidth + layoutSpec.spacing();
cursorX += slotWidth + layoutSpec.spacing()
+ (index < children.size() - 1 ? flexExtraGap : 0.0);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ public static MeasureResult measureRow(RowNode node,
double totalGap = n > 1 ? gap * (n - 1) : 0.0;
double slotsTotal = Math.max(0.0, availableWidth - totalGap);
double[] slotWidths = new double[n];
if (!node.columns().isEmpty()) {
if (RowSlots.hasFlexLayout(node)) {
slotWidths = RowSlots.distributeFlex(node.children(), gap, availableWidth, ctx);
} else if (!node.columns().isEmpty()) {
double[] intrinsic = RowSlots.intrinsicColumnWidths(node.children(), node.columns(), slotsTotal, ctx);
slotWidths = RowSlots.distributeColumns(node.columns(), intrinsic, gap, availableWidth, node.name());
} else if (node.weights().isEmpty()) {
Expand Down
Loading
Loading