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.columns(...)` + `DocumentRowColumn`** (`@since 1.9.0`). Size each row
column explicitly: `DocumentRowColumn.fixed(pt)`, `auto()` (intrinsic content
width), or `weight(w)` (a share of the space left after the fixed and intrinsic
columns). Mix them freely — `columns(auto(), weight(1), auto())` with a
`line().fill()` in the middle is a dot-leader table-of-contents row, with the
label and page number sized to their content. `weights(...)` stays as sugar for
the even / weighted split (and a weight-only column list resolves identically),
so existing rows are byte-identical.

- **`LineBuilder.fill()`** (`@since 1.9.0`). A line stretches to the width
available where it is placed — its column inside a row, or the content width at
flow level — instead of its authored fixed width. Paired with a dotted stroke
Expand Down
Binary file added assets/readme/examples/row-columns.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 @@ -76,6 +76,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Transforms](#transforms) | `rotate`, `scale`, and per-layer `zIndex` swap | [PDF](../assets/readme/examples/transforms.pdf) · [Source](src/main/java/com/demcha/examples/features/transforms/TransformsExample.java) |
| [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) |

### 📋 Templates recommended

Expand Down Expand Up @@ -473,6 +474,24 @@ page.addSection(band -> band
[📄 View PDF](../assets/readme/examples/content-bleed.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/layout/BleedExample.java)

### Row columns & TOC

`RowBuilder.columns(...)` sizes each column as fixed points, intrinsic content
width (`auto()`), or a `weight()` share of the remainder — `weights(...)` stays
as sugar for the even / weighted split. Combined with `line().fill()` it builds a
table-of-contents row without measuring the gap: the label and page number size
to their content while the dotted leader fills between them.

```java
flow.addRow(r -> r.columns(auto(), weight(1), auto())
.addParagraph(label)
.addLine(l -> l.fill().dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)) // leader fills the gap
.addParagraph(pageNumber));
```

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

### Advanced tables

`DocumentTableCell.rowSpan(int)` mirrors `colSpan(int)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.demcha.examples.features.chrome.PageNumberingExample;
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.BlockAlignExample;
import com.demcha.examples.features.lists.NestedListExample;
import com.demcha.examples.features.shapes.LineCapExample;
Expand Down Expand Up @@ -153,6 +154,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + SvgIconGalleryExample.generate());
System.out.println("Generated: " + BlockAlignExample.generate());
System.out.println("Generated: " + BleedExample.generate());
System.out.println("Generated: " + RowColumnsExample.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,100 @@
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.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentLineCap;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

import static com.demcha.compose.document.style.DocumentRowColumn.auto;
import static com.demcha.compose.document.style.DocumentRowColumn.weight;

/**
* Runnable showcase for v1.9 row columns: {@code RowBuilder.columns(...)} sizes
* each column as fixed points, intrinsic ({@code auto()}) content width, or a
* {@code weight()} share of the remainder. Combined with {@code line().fill()}
* it builds a table-of-contents row without measuring the gap — the label and
* page number size to their content while the dotted leader fills between them.
*
* <pre>{@code
* flow.addRow(r -> r.columns(auto(), weight(1), auto())
* .addParagraph(label)
* .addLine(l -> l.fill().dashed(0.1, 4).lineCap(ROUND)) // leader fills the gap
* .addParagraph(pageNumber));
* }</pre>
*
* @author Artem Demchyshyn
*/
public final class RowColumnsExample {

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

private static final String[][] ENTRIES = {
{"Introduction", "1"},
{"Getting started", "4"},
{"A longer chapter title that runs on", "12"},
{"Appendix", "28"},
};

private RowColumnsExample() {
}

/**
* Renders a table-of-contents block built from {@code columns(auto(),
* weight(1), auto())} rows with dotted-leader fill lines.
*
* @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-columns.pdf");

DocumentTextStyle entry = DocumentTextStyle.DEFAULT.withSize(11).withColor(INK);
DocumentStroke dots = DocumentStroke.of(MUTED, 1.3);

try (DocumentSession document = GraphCompose.document(pdfFile)
.pageSize(380, 260)
.margin(DocumentInsets.of(36))
.create()) {
document.pageFlow(page -> {
page.addParagraph(p -> p.text("Table of contents")
.textStyle(DocumentTextStyle.DEFAULT.withSize(18)));
page.addParagraph(p -> p.text("columns(auto(), weight(1), auto()) + line().fill()")
.textStyle(DocumentTextStyle.DEFAULT.withSize(9).withColor(MUTED))
.padding(DocumentInsets.bottom(8)));

for (String[] item : ENTRIES) {
tocRow(page, entry, dots, item[0], item[1]);
}
});

document.buildPdf();
}

return pdfFile;
}

private static void tocRow(PageFlowBuilder page,
DocumentTextStyle entry,
DocumentStroke dots,
String label,
String pageNumber) {
page.addRow(r -> r.gap(6).columns(auto(), weight(1), auto())
.padding(DocumentInsets.symmetric(0, 5))
.addParagraph(p -> p.text(label).textStyle(entry))
.addLine(l -> l.fill().height(11).stroke(dots)
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND))
.addParagraph(p -> p.text(pageNumber).textStyle(entry)));
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
39 changes: 38 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 @@ -32,6 +32,7 @@
public final class RowBuilder {
private final List<DocumentNode> children = new ArrayList<>();
private final List<Double> weights = new ArrayList<>();
private final List<DocumentRowColumn> columns = new ArrayList<>();
private String name = "";
private boolean weightsDirty;
private double gap;
Expand Down Expand Up @@ -186,6 +187,7 @@ public RowBuilder borders(DocumentBorders borders) {
*/
public RowBuilder weights(double... weights) {
this.weights.clear();
this.columns.clear();
if (weights != null) {
for (double w : weights) {
this.weights.add(w);
Expand All @@ -202,10 +204,39 @@ public RowBuilder weights(double... weights) {
*/
public RowBuilder evenWeights() {
this.weights.clear();
this.columns.clear();
this.weightsDirty = true;
return this;
}

/**
* Sizes each column explicitly: a fixed point width, an automatic
* (content-sized) width via {@link DocumentRowColumn#auto()}, or a weight that
* shares the leftover space. Mix them freely — a dot-leader row is
* {@code columns(auto(), weight(1), auto())} with a {@code line().fill()} in
* the middle. The count must match the children at {@link #build()}.
*
* <p>Fixed and auto columns are left-packed: add a {@code weight(...)} column
* to absorb the remainder, otherwise trailing space is left empty. Mutually
* exclusive with {@link #weights(double...)}; calling one clears the other. A
* weight-only column list resolves identically to {@code weights(...)}.</p>
*
* @param columns one width spec per row child
* @return this builder
* @since 1.9.0
*/
public RowBuilder columns(DocumentRowColumn... columns) {
this.columns.clear();
this.weights.clear();
this.weightsDirty = false;
if (columns != null) {
for (DocumentRowColumn column : columns) {
this.columns.add(column);
}
}
return this;
}

/**
* Adds a pre-built atomic node as the next row child. Validates the child
* type immediately so authoring mistakes (e.g. dropping a row or a table
Expand Down Expand Up @@ -409,7 +440,8 @@ public RowNode build() {
fillColor,
stroke,
cornerRadius,
borders);
borders,
List.copyOf(columns));
}

private void validate() {
Expand All @@ -418,5 +450,10 @@ private void validate() {
+ " does not match children size " + children.size()
+ ". Pass " + children.size() + " weights or call evenWeights().");
}
if (!columns.isEmpty() && columns.size() != children.size()) {
throw new IllegalStateException("RowBuilder columns size " + columns.size()
+ " does not match children size " + children.size()
+ ". Pass " + children.size() + " columns.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
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.RowNode;
import com.demcha.compose.document.style.DocumentBleed;
import com.demcha.compose.document.style.DocumentEdge;
import com.demcha.compose.document.style.DocumentRowColumn;
import com.demcha.compose.engine.components.style.Margin;
import com.demcha.compose.engine.components.style.Padding;
import org.slf4j.Logger;
Expand Down Expand Up @@ -506,8 +508,16 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
double rowInnerY = placementTopY - padding.top();

if (!children.isEmpty()) {
double[] slotWidths = distributeRowSlotWidths(children, layoutSpec.weights(),
layoutSpec.spacing(), childRegionWidth);
List<DocumentRowColumn> columns = node instanceof RowNode row ? row.columns() : List.of();
double[] slotWidths;
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);
} else {
slotWidths = distributeRowSlotWidths(children, layoutSpec.weights(),
layoutSpec.spacing(), childRegionWidth);
}
double cursorX = placementX + padding.left();

for (int index = 0; index < children.size(); index++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ 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.weights().isEmpty()) {
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()) {
double share = n > 0 ? slotsTotal / n : 0.0;
for (int i = 0; i < n; i++) {
slotWidths[i] = share;
Expand Down
Loading