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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ PDF `GoTo` actions. External links are unchanged.

### Public API

- **`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`
analogue for a horizontal row, without manual coordinates. The measure phase is
unchanged and `TOP` rows render byte-for-byte as before, so existing documents are
unaffected.

- **`GraphCompose.documents()` + `MultiSectionDocumentBuilder` / `MultiSectionDocument`**
(`@since 1.9.0`). Concatenates several independently authored `DocumentSession`
sections — each with its own page size, margins, fonts, and footer numbering —
Expand Down
Binary file added assets/readme/examples/row-vertical-align.pdf
Binary file not shown.
18 changes: 18 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Block alignment](#block-alignment) | `addAligned(align, node)` / `addSvgIcon(icon, w, align)` — seat any fixed-size node left / centre / right across the content width | [PDF](../assets/readme/examples/block-align.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java) |
| [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) |

### 📋 Templates recommended

Expand Down Expand Up @@ -494,6 +495,23 @@ flow.addRow(r -> r.columns(auto(), weight(1), auto())
[📄 View PDF](../assets/readme/examples/row-columns.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/layout/RowColumnsExample.java)

### Row vertical align

`RowBuilder.verticalAlign(...)` seats a row's children on the cross axis within
the row band, whose height is that of the tallest child. A short label beside a
large price moves from the top to the middle to the bottom of the band as the
alignment changes — the `align-items` analogue for a horizontal row, no manual
coordinates. `TOP` is the default, so existing rows are unchanged.

```java
flow.addRow(r -> r.verticalAlign(RowVerticalAlign.BOTTOM)
.addParagraph(bigPrice) // tallest child sets the band height
.addParagraph(smallLabel)); // seated on the band bottom
```

[📄 View PDF](../assets/readme/examples/row-vertical-align.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/layout/RowVerticalAlignExample.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.RowVerticalAlignExample;
import com.demcha.examples.features.layout.BlockAlignExample;
import com.demcha.examples.features.lists.NestedListExample;
import com.demcha.examples.features.shapes.LineCapExample;
Expand Down Expand Up @@ -157,6 +158,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + BlockAlignExample.generate());
System.out.println("Generated: " + BleedExample.generate());
System.out.println("Generated: " + RowColumnsExample.generate());
System.out.println("Generated: " + RowVerticalAlignExample.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,86 @@
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.RowVerticalAlign;
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 {@code RowBuilder.verticalAlign(...)}: cross-axis
* placement of a row's children within the band set by the tallest child. A
* short {@code / month} label seated beside a large price moves from the top to
* the middle to the bottom of the band as the alignment changes — no manual
* coordinates.
*
* <pre>{@code
* flow.addRow(r -> r.verticalAlign(RowVerticalAlign.BOTTOM)
* .addParagraph(bigPrice) // tallest child -> sets the band height
* .addParagraph(smallLabel)); // seated on the band bottom
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class RowVerticalAlignExample {

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

private RowVerticalAlignExample() {
}

/**
* Renders the same price row at {@code TOP}, {@code CENTER}, and {@code BOTTOM}
* vertical alignment.
*
* @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-vertical-align.pdf");

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(380, 300)
.margin(DocumentInsets.of(34))
.create()) {
document.pageFlow(page -> {
page.addParagraph(p -> p.text("Row verticalAlign")
.textStyle(DocumentTextStyle.DEFAULT.withSize(18).withColor(INK)));
page.addParagraph(p -> p.text("the short label is seated against the band set by the price")
.textStyle(DocumentTextStyle.DEFAULT.withSize(9).withColor(MUTED))
.padding(DocumentInsets.bottom(10)));

priceRow(page, RowVerticalAlign.TOP);
priceRow(page, RowVerticalAlign.CENTER);
priceRow(page, RowVerticalAlign.BOTTOM);
});

document.buildPdf();
}

return pdfFile;
}

private static void priceRow(PageFlowBuilder page, RowVerticalAlign align) {
page.addRow(r -> r.verticalAlign(align)
.fillColor(BAND)
.cornerRadius(8)
.padding(DocumentInsets.of(12))
.gap(10)
.addParagraph(p -> p.text("$49")
.textStyle(DocumentTextStyle.DEFAULT.withSize(30).withColor(INK)))
.addParagraph(p -> p.text("/ month · verticalAlign(" + align + ")")
.textStyle(DocumentTextStyle.DEFAULT.withSize(11).withColor(MUTED))));
page.addSpacer(s -> s.height(10));
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
19 changes: 18 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 @@ -42,6 +42,7 @@ public final class RowBuilder {
private DocumentStroke stroke;
private DocumentCornerRadius cornerRadius = DocumentCornerRadius.ZERO;
private DocumentBorders borders = DocumentBorders.NONE;
private RowVerticalAlign verticalAlign = RowVerticalAlign.TOP;

/**
* Creates a row builder.
Expand Down Expand Up @@ -179,6 +180,21 @@ public RowBuilder borders(DocumentBorders borders) {
return this;
}

/**
* Sets the cross-axis (vertical) placement of the row's children within the
* row band, whose height is that of the tallest child. Shorter children align
* to the top, middle, or bottom without manual coordinates.
*
* @param verticalAlign cross-axis alignment; {@code null} resets to
* {@link RowVerticalAlign#TOP}
* @return this builder
* @since 1.9.0
*/
public RowBuilder verticalAlign(RowVerticalAlign verticalAlign) {
this.verticalAlign = verticalAlign == null ? RowVerticalAlign.TOP : verticalAlign;
return this;
}

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

private void validate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.demcha.compose.document.node.LayerStackNode;
import com.demcha.compose.document.node.PageBreakNode;
import com.demcha.compose.document.node.RowNode;
import com.demcha.compose.document.node.RowVerticalAlign;
import com.demcha.compose.document.style.DocumentBleed;
import com.demcha.compose.document.style.DocumentEdge;
import com.demcha.compose.document.style.DocumentRowColumn;
Expand Down Expand Up @@ -518,6 +519,9 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
slotWidths = distributeRowSlotWidths(children, layoutSpec.weights(),
layoutSpec.spacing(), childRegionWidth);
}
RowVerticalAlign verticalAlign = node instanceof RowNode rowNode
? rowNode.verticalAlign() : RowVerticalAlign.TOP;
double bandContentHeight = naturalMeasure.height() - padding.vertical();
double cursorX = placementX + padding.left();

for (int index = 0; index < children.size(); index++) {
Expand All @@ -539,6 +543,17 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
+ "Reduce the child height, shorten its content, or increase the row height.");
}

// Cross-axis seating within the row band. TOP yields offset 0.0,
// so the band-top expression below is byte-identical to the
// pre-verticalAlign placement; only CENTER/BOTTOM shift the child.
// The slack is never negative: the measure phase sets the band to
// the tallest child's margin-box, so bandContentHeight >=
// childMargin.vertical() + childMeasure.height() for every child.
double verticalOffset = verticalAlign == RowVerticalAlign.TOP
? 0.0
: (bandContentHeight - childMargin.vertical() - childMeasure.height())
* (verticalAlign == RowVerticalAlign.CENTER ? 0.5 : 1.0);

if (childPrepared.isComposite()) {
PlacementContext slotCtx = new FixedSlotPlacementContext(
state.pageIndex, state.canvas, prepareContext, fragmentContext, nodes, fragments);
Expand All @@ -551,7 +566,7 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
index,
depth + 1,
cursorX,
rowInnerY,
rowInnerY - verticalOffset,
slotWidth,
FixedSlotKind.ROW_SLOT,
slotCtx);
Expand All @@ -561,7 +576,7 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,

String childPath = pathFor(child, path, index);
String childSemanticName = semanticName(child);
double childTopY = rowInnerY - childMargin.top();
double childTopY = rowInnerY - childMargin.top() - verticalOffset;
double childPlacementY = childTopY - childMeasure.height();
Padding childPadding = toPadding(child.padding());

Expand Down
43 changes: 40 additions & 3 deletions src/main/java/com/demcha/compose/document/node/RowNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
* @param columns optional per-child column widths (fixed / intrinsic / weight);
* length must match children, or be empty. Mutually exclusive
* with {@code weights}.
* @param verticalAlign cross-axis placement of children within the row band
* (defaults to {@link RowVerticalAlign#TOP})
* @author Artem Demchyshyn
*/
public record RowNode(
Expand All @@ -45,7 +47,8 @@ public record RowNode(
DocumentStroke stroke,
DocumentCornerRadius cornerRadius,
DocumentBorders borders,
List<DocumentRowColumn> columns
List<DocumentRowColumn> columns,
RowVerticalAlign verticalAlign
) implements DocumentNode {
/**
* Creates a normalized horizontal row container.
Expand Down Expand Up @@ -83,11 +86,43 @@ public record RowNode(
margin = margin == null ? DocumentInsets.zero() : margin;
cornerRadius = cornerRadius == null ? DocumentCornerRadius.ZERO : cornerRadius;
borders = borders == null ? DocumentBorders.NONE : borders;
verticalAlign = verticalAlign == null ? RowVerticalAlign.TOP : verticalAlign;
if (gap < 0 || Double.isNaN(gap) || Double.isInfinite(gap)) {
throw new IllegalArgumentException("gap must be finite and non-negative: " + gap);
}
}

/**
* Backwards-compatible constructor without a cross-axis vertical alignment —
* defaults to {@link RowVerticalAlign#TOP}.
*
* @param name node name used in snapshots and layout graph paths
* @param children child semantic nodes in source order
* @param weights optional per-child weights (length must match children, or be empty)
* @param gap horizontal gap between children
* @param padding inner padding
* @param margin outer margin
* @param fillColor optional background fill
* @param stroke optional border stroke
* @param cornerRadius optional render-only corner radius
* @param borders optional per-side border strokes
* @param columns optional per-child column widths
*/
public RowNode(String name,
List<DocumentNode> children,
List<Double> weights,
double gap,
DocumentInsets padding,
DocumentInsets margin,
DocumentColor fillColor,
DocumentStroke stroke,
DocumentCornerRadius cornerRadius,
DocumentBorders borders,
List<DocumentRowColumn> columns) {
this(name, children, weights, gap, padding, margin, fillColor, stroke, cornerRadius, borders, columns,
RowVerticalAlign.TOP);
}

/**
* Backwards-compatible constructor without per-child columns — defaults to an
* empty column list (weights / even split).
Expand All @@ -113,7 +148,8 @@ public RowNode(String name,
DocumentStroke stroke,
DocumentCornerRadius cornerRadius,
DocumentBorders borders) {
this(name, children, weights, gap, padding, margin, fillColor, stroke, cornerRadius, borders, List.of());
this(name, children, weights, gap, padding, margin, fillColor, stroke, cornerRadius, borders, List.of(),
RowVerticalAlign.TOP);
}

/**
Expand All @@ -138,6 +174,7 @@ public RowNode(String name,
DocumentColor fillColor,
DocumentStroke stroke,
DocumentCornerRadius cornerRadius) {
this(name, children, weights, gap, padding, margin, fillColor, stroke, cornerRadius, DocumentBorders.NONE, List.of());
this(name, children, weights, gap, padding, margin, fillColor, stroke, cornerRadius, DocumentBorders.NONE,
List.of(), RowVerticalAlign.TOP);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.demcha.compose.document.node;

/**
* Cross-axis (vertical) placement of a row's children within the row band, whose
* height is the tallest child. The {@code align-items} analogue for a horizontal
* row — line up shorter children to the top, middle, or bottom without manual
* coordinates.
*
* @author Artem Demchyshyn
* @since 1.9.0
*/
public enum RowVerticalAlign {
/** Children sit flush with the top of the row band (the default). */
TOP,
/** Children are centred vertically within the row band. */
CENTER,
/** Children sit flush with the bottom of the row band. */
BOTTOM
}
Loading
Loading