Skip to content

Commit ad89fc7

Browse files
authored
feat(api): RowBuilder.columns() — fixed / auto / weight row columns (#235)
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.
1 parent 057ce39 commit ad89fc7

12 files changed

Lines changed: 588 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ PDF `GoTo` actions. External links are unchanged.
1212

1313
### Public API
1414

15+
- **`RowBuilder.columns(...)` + `DocumentRowColumn`** (`@since 1.9.0`). Size each row
16+
column explicitly: `DocumentRowColumn.fixed(pt)`, `auto()` (intrinsic content
17+
width), or `weight(w)` (a share of the space left after the fixed and intrinsic
18+
columns). Mix them freely — `columns(auto(), weight(1), auto())` with a
19+
`line().fill()` in the middle is a dot-leader table-of-contents row, with the
20+
label and page number sized to their content. `weights(...)` stays as sugar for
21+
the even / weighted split (and a weight-only column list resolves identically),
22+
so existing rows are byte-identical.
23+
1524
- **`LineBuilder.fill()`** (`@since 1.9.0`). A line stretches to the width
1625
available where it is placed — its column inside a row, or the content width at
1726
flow level — instead of its authored fixed width. Paired with a dotted stroke
1.13 KB
Binary file not shown.

examples/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ are with the canonical DSL, then jump to its detailed section below.
7676
| [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) |
7777
| [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) |
7878
| [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) |
79+
| [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) |
7980

8081
### 📋 Templates recommended
8182

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

477+
### Row columns & TOC
478+
479+
`RowBuilder.columns(...)` sizes each column as fixed points, intrinsic content
480+
width (`auto()`), or a `weight()` share of the remainder — `weights(...)` stays
481+
as sugar for the even / weighted split. Combined with `line().fill()` it builds a
482+
table-of-contents row without measuring the gap: the label and page number size
483+
to their content while the dotted leader fills between them.
484+
485+
```java
486+
flow.addRow(r -> r.columns(auto(), weight(1), auto())
487+
.addParagraph(label)
488+
.addLine(l -> l.fill().dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)) // leader fills the gap
489+
.addParagraph(pageNumber));
490+
```
491+
492+
[📄 View PDF](../assets/readme/examples/row-columns.pdf) ·
493+
[📜 Full source](src/main/java/com/demcha/examples/features/layout/RowColumnsExample.java)
494+
476495
### Advanced tables
477496

478497
`DocumentTableCell.rowSpan(int)` mirrors `colSpan(int)`.

examples/src/main/java/com/demcha/examples/GenerateAllExamples.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.demcha.examples.features.chrome.PageNumberingExample;
99
import com.demcha.examples.features.chrome.PdfChromeExample;
1010
import com.demcha.examples.features.layout.BleedExample;
11+
import com.demcha.examples.features.layout.RowColumnsExample;
1112
import com.demcha.examples.features.layout.BlockAlignExample;
1213
import com.demcha.examples.features.lists.NestedListExample;
1314
import com.demcha.examples.features.shapes.LineCapExample;
@@ -153,6 +154,7 @@ public static void main(String[] args) throws Exception {
153154
System.out.println("Generated: " + SvgIconGalleryExample.generate());
154155
System.out.println("Generated: " + BlockAlignExample.generate());
155156
System.out.println("Generated: " + BleedExample.generate());
157+
System.out.println("Generated: " + RowColumnsExample.generate());
156158
System.out.println("Generated: " + TransformsExample.generate());
157159
System.out.println("Generated: " + TableAdvancedExample.generate());
158160

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.demcha.examples.features.layout;
2+
3+
import com.demcha.compose.GraphCompose;
4+
import com.demcha.compose.document.api.DocumentSession;
5+
import com.demcha.compose.document.dsl.PageFlowBuilder;
6+
import com.demcha.compose.document.style.DocumentColor;
7+
import com.demcha.compose.document.style.DocumentInsets;
8+
import com.demcha.compose.document.style.DocumentLineCap;
9+
import com.demcha.compose.document.style.DocumentStroke;
10+
import com.demcha.compose.document.style.DocumentTextStyle;
11+
import com.demcha.examples.support.ExampleOutputPaths;
12+
13+
import java.nio.file.Path;
14+
15+
import static com.demcha.compose.document.style.DocumentRowColumn.auto;
16+
import static com.demcha.compose.document.style.DocumentRowColumn.weight;
17+
18+
/**
19+
* Runnable showcase for v1.9 row columns: {@code RowBuilder.columns(...)} sizes
20+
* each column as fixed points, intrinsic ({@code auto()}) content width, or a
21+
* {@code weight()} share of the remainder. Combined with {@code line().fill()}
22+
* it builds a table-of-contents row without measuring the gap — the label and
23+
* page number size to their content while the dotted leader fills between them.
24+
*
25+
* <pre>{@code
26+
* flow.addRow(r -> r.columns(auto(), weight(1), auto())
27+
* .addParagraph(label)
28+
* .addLine(l -> l.fill().dashed(0.1, 4).lineCap(ROUND)) // leader fills the gap
29+
* .addParagraph(pageNumber));
30+
* }</pre>
31+
*
32+
* @author Artem Demchyshyn
33+
*/
34+
public final class RowColumnsExample {
35+
36+
private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38);
37+
private static final DocumentColor MUTED = DocumentColor.rgb(120, 126, 135);
38+
39+
private static final String[][] ENTRIES = {
40+
{"Introduction", "1"},
41+
{"Getting started", "4"},
42+
{"A longer chapter title that runs on", "12"},
43+
{"Appendix", "28"},
44+
};
45+
46+
private RowColumnsExample() {
47+
}
48+
49+
/**
50+
* Renders a table-of-contents block built from {@code columns(auto(),
51+
* weight(1), auto())} rows with dotted-leader fill lines.
52+
*
53+
* @return path to the generated PDF
54+
* @throws Exception if rendering or file IO fails
55+
*/
56+
public static Path generate() throws Exception {
57+
Path pdfFile = ExampleOutputPaths.prepare("features/layout", "row-columns.pdf");
58+
59+
DocumentTextStyle entry = DocumentTextStyle.DEFAULT.withSize(11).withColor(INK);
60+
DocumentStroke dots = DocumentStroke.of(MUTED, 1.3);
61+
62+
try (DocumentSession document = GraphCompose.document(pdfFile)
63+
.pageSize(380, 260)
64+
.margin(DocumentInsets.of(36))
65+
.create()) {
66+
document.pageFlow(page -> {
67+
page.addParagraph(p -> p.text("Table of contents")
68+
.textStyle(DocumentTextStyle.DEFAULT.withSize(18)));
69+
page.addParagraph(p -> p.text("columns(auto(), weight(1), auto()) + line().fill()")
70+
.textStyle(DocumentTextStyle.DEFAULT.withSize(9).withColor(MUTED))
71+
.padding(DocumentInsets.bottom(8)));
72+
73+
for (String[] item : ENTRIES) {
74+
tocRow(page, entry, dots, item[0], item[1]);
75+
}
76+
});
77+
78+
document.buildPdf();
79+
}
80+
81+
return pdfFile;
82+
}
83+
84+
private static void tocRow(PageFlowBuilder page,
85+
DocumentTextStyle entry,
86+
DocumentStroke dots,
87+
String label,
88+
String pageNumber) {
89+
page.addRow(r -> r.gap(6).columns(auto(), weight(1), auto())
90+
.padding(DocumentInsets.symmetric(0, 5))
91+
.addParagraph(p -> p.text(label).textStyle(entry))
92+
.addLine(l -> l.fill().height(11).stroke(dots)
93+
.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND))
94+
.addParagraph(p -> p.text(pageNumber).textStyle(entry)));
95+
}
96+
97+
public static void main(String[] args) throws Exception {
98+
System.out.println("Generated: " + generate());
99+
}
100+
}

src/main/java/com/demcha/compose/document/dsl/RowBuilder.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
public final class RowBuilder {
3333
private final List<DocumentNode> children = new ArrayList<>();
3434
private final List<Double> weights = new ArrayList<>();
35+
private final List<DocumentRowColumn> columns = new ArrayList<>();
3536
private String name = "";
3637
private boolean weightsDirty;
3738
private double gap;
@@ -186,6 +187,7 @@ public RowBuilder borders(DocumentBorders borders) {
186187
*/
187188
public RowBuilder weights(double... weights) {
188189
this.weights.clear();
190+
this.columns.clear();
189191
if (weights != null) {
190192
for (double w : weights) {
191193
this.weights.add(w);
@@ -202,10 +204,39 @@ public RowBuilder weights(double... weights) {
202204
*/
203205
public RowBuilder evenWeights() {
204206
this.weights.clear();
207+
this.columns.clear();
205208
this.weightsDirty = true;
206209
return this;
207210
}
208211

212+
/**
213+
* Sizes each column explicitly: a fixed point width, an automatic
214+
* (content-sized) width via {@link DocumentRowColumn#auto()}, or a weight that
215+
* shares the leftover space. Mix them freely — a dot-leader row is
216+
* {@code columns(auto(), weight(1), auto())} with a {@code line().fill()} in
217+
* the middle. The count must match the children at {@link #build()}.
218+
*
219+
* <p>Fixed and auto columns are left-packed: add a {@code weight(...)} column
220+
* to absorb the remainder, otherwise trailing space is left empty. Mutually
221+
* exclusive with {@link #weights(double...)}; calling one clears the other. A
222+
* weight-only column list resolves identically to {@code weights(...)}.</p>
223+
*
224+
* @param columns one width spec per row child
225+
* @return this builder
226+
* @since 1.9.0
227+
*/
228+
public RowBuilder columns(DocumentRowColumn... columns) {
229+
this.columns.clear();
230+
this.weights.clear();
231+
this.weightsDirty = false;
232+
if (columns != null) {
233+
for (DocumentRowColumn column : columns) {
234+
this.columns.add(column);
235+
}
236+
}
237+
return this;
238+
}
239+
209240
/**
210241
* Adds a pre-built atomic node as the next row child. Validates the child
211242
* type immediately so authoring mistakes (e.g. dropping a row or a table
@@ -409,7 +440,8 @@ public RowNode build() {
409440
fillColor,
410441
stroke,
411442
cornerRadius,
412-
borders);
443+
borders,
444+
List.copyOf(columns));
413445
}
414446

415447
private void validate() {
@@ -418,5 +450,10 @@ private void validate() {
418450
+ " does not match children size " + children.size()
419451
+ ". Pass " + children.size() + " weights or call evenWeights().");
420452
}
453+
if (!columns.isEmpty() && columns.size() != children.size()) {
454+
throw new IllegalStateException("RowBuilder columns size " + columns.size()
455+
+ " does not match children size " + children.size()
456+
+ ". Pass " + children.size() + " columns.");
457+
}
421458
}
422459
}

src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import com.demcha.compose.document.node.DocumentNode;
66
import com.demcha.compose.document.node.LayerStackNode;
77
import com.demcha.compose.document.node.PageBreakNode;
8+
import com.demcha.compose.document.node.RowNode;
89
import com.demcha.compose.document.style.DocumentBleed;
910
import com.demcha.compose.document.style.DocumentEdge;
11+
import com.demcha.compose.document.style.DocumentRowColumn;
1012
import com.demcha.compose.engine.components.style.Margin;
1113
import com.demcha.compose.engine.components.style.Padding;
1214
import org.slf4j.Logger;
@@ -506,8 +508,16 @@ private void compileHorizontalRow(PreparedNode<DocumentNode> prepared,
506508
double rowInnerY = placementTopY - padding.top();
507509

508510
if (!children.isEmpty()) {
509-
double[] slotWidths = distributeRowSlotWidths(children, layoutSpec.weights(),
510-
layoutSpec.spacing(), childRegionWidth);
511+
List<DocumentRowColumn> columns = node instanceof RowNode row ? row.columns() : List.of();
512+
double[] slotWidths;
513+
if (!columns.isEmpty()) {
514+
double available = RowSlots.rowAvailableWidth(childRegionWidth, layoutSpec.spacing(), children.size());
515+
double[] intrinsic = RowSlots.intrinsicColumnWidths(children, columns, available, prepareContext);
516+
slotWidths = RowSlots.distributeColumns(columns, intrinsic, layoutSpec.spacing(), childRegionWidth, semanticName);
517+
} else {
518+
slotWidths = distributeRowSlotWidths(children, layoutSpec.weights(),
519+
layoutSpec.spacing(), childRegionWidth);
520+
}
511521
double cursorX = placementX + padding.left();
512522

513523
for (int index = 0; index < children.size(); index++) {

src/main/java/com/demcha/compose/document/layout/NodeDefinitionSupport.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,10 @@ public static MeasureResult measureRow(RowNode node,
346346
double totalGap = n > 1 ? gap * (n - 1) : 0.0;
347347
double slotsTotal = Math.max(0.0, availableWidth - totalGap);
348348
double[] slotWidths = new double[n];
349-
if (node.weights().isEmpty()) {
349+
if (!node.columns().isEmpty()) {
350+
double[] intrinsic = RowSlots.intrinsicColumnWidths(node.children(), node.columns(), slotsTotal, ctx);
351+
slotWidths = RowSlots.distributeColumns(node.columns(), intrinsic, gap, availableWidth, node.name());
352+
} else if (node.weights().isEmpty()) {
350353
double share = n > 0 ? slotsTotal / n : 0.0;
351354
for (int i = 0; i < n; i++) {
352355
slotWidths[i] = share;

0 commit comments

Comments
 (0)