Skip to content

Commit 331bc95

Browse files
authored
Merge pull request #183 from DemchaAV/feat/block-align
feat(layout): block-level horizontal alignment for fixed-size flow children
2 parents 41e2db0 + 3a88585 commit 331bc95

12 files changed

Lines changed: 449 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ Entries land here as they merge.
1010

1111
### Public API
1212

13+
- **Block-level horizontal alignment** (`@since 1.8.0`). Fixed-size flow
14+
children (paths, images, SVG icons, barcodes, shape containers) left-align
15+
by default — there was no built-in way to centre or right-align one without
16+
wrapping it in a full-width container and hand-computing the content width.
17+
New `AlignNode` + `HorizontalAlign` (LEFT / CENTER / RIGHT) seat any node
18+
across the available width: `flow.addAligned(HorizontalAlign.CENTER, node)`
19+
and the icon sugar `flow.addSvgIcon(icon, width, HorizontalAlign.CENTER)`.
20+
The wrapper fills the width and reuses the stack placement engine (one
21+
anchor), so there is no new render handler and no hot-path change.
1322
- **Native vector charts** (`@since 1.8.0`). New `com.demcha.compose.document.chart`
1423
package with a layered, serialization-friendly API: `ChartData` (categories +
1524
series, type/colour-agnostic), sealed `ChartSpec` (`bar()` / `line()` with
1.6 KB
Binary file not shown.

examples/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Install the library artifact once from the repository root:
1515
./mvnw -DskipTests install
1616
```
1717

18-
Then run all 54 examples in one shot:
18+
Then run all 55 examples in one shot:
1919

2020
```bash
2121
./mvnw -f examples/pom.xml exec:java \
@@ -32,7 +32,7 @@ Then run all 54 examples in one shot:
3232
Generated PDFs land in `examples/target/generated-pdfs/`. The same
3333
`mvnw.cmd` form works on Windows PowerShell with backslash paths.
3434

35-
`GenerateAllExamples` runs **54** example programs — 16 CV + 15
35+
`GenerateAllExamples` runs **55** example programs — 16 CV + 15
3636
cover-letter presets plus invoices, proposals, a schedule, the feature
3737
demos, and the flagships. The showcase site surfaces the full generated
3838
catalogue (~53 PDFs); a curated 28-PDF subset is committed under
@@ -73,6 +73,7 @@ are with the canonical DSL, then jump to its detailed section below.
7373
| [Composed table cells](#composed-table-cells-v16) | `DocumentTableCell.node(DocumentNode)` — paragraphs, lists, sub-tables inside cells with two-pass measurement | [PDF](../assets/readme/examples/composed-table-cell-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java) |
7474
| [Canvas layer (free placement)](#canvas-layer-v16) | `CanvasLayerNode` — pixel-precise `(x, y)` placement of children inside a fixed bounding box, with `ClipPolicy` clipping | [PDF](../assets/readme/examples/canvas-layer-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/canvas/CanvasLayerExample.java) |
7575
| [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) |
76+
| [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) |
7677

7778
### 📋 Templates recommended
7879

@@ -396,6 +397,22 @@ flow.addSvgIcon(SvgIcon.parse(readResource("/icons/apple.svg")), 50);
396397
[📄 View PDF](../assets/readme/examples/svg-icon-gallery.pdf) ·
397398
[📜 Full source](src/main/java/com/demcha/examples/features/svg/SvgIconGalleryExample.java)
398399

400+
### Block alignment
401+
402+
A fixed-size node (an SVG icon, a vector path, an image) left-aligns in
403+
the flow by default. `addSvgIcon(icon, width, HorizontalAlign.CENTER)` and
404+
the general `addAligned(align, node)` seat it left, centre, or right across
405+
the content width — the `margin: auto` the flow does not give fixed nodes on
406+
its own, with no manual width maths.
407+
408+
```java
409+
flow.addSvgIcon(icon, 44, HorizontalAlign.CENTER);
410+
flow.addAligned(HorizontalAlign.RIGHT, anyFixedNode);
411+
```
412+
413+
[📄 View PDF](../assets/readme/examples/block-align.pdf) ·
414+
[📜 Full source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java)
415+
399416
### Advanced tables
400417

401418
`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
@@ -6,6 +6,7 @@
66
import com.demcha.examples.features.debug.DebugOverlayExample;
77
import com.demcha.examples.features.docx.WordExportExample;
88
import com.demcha.examples.features.chrome.PdfChromeExample;
9+
import com.demcha.examples.features.layout.BlockAlignExample;
910
import com.demcha.examples.features.lists.NestedListExample;
1011
import com.demcha.examples.features.shapes.ShapeContainerExample;
1112
import com.demcha.examples.features.shapes.VectorPathExample;
@@ -132,6 +133,7 @@ public static void main(String[] args) throws Exception {
132133
System.out.println("Generated: " + ShapeContainerExample.generate());
133134
System.out.println("Generated: " + VectorPathExample.generate());
134135
System.out.println("Generated: " + SvgIconGalleryExample.generate());
136+
System.out.println("Generated: " + BlockAlignExample.generate());
135137
System.out.println("Generated: " + TransformsExample.generate());
136138
System.out.println("Generated: " + TableAdvancedExample.generate());
137139

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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.node.HorizontalAlign;
6+
import com.demcha.compose.document.style.DocumentColor;
7+
import com.demcha.compose.document.style.DocumentInsets;
8+
import com.demcha.compose.document.style.DocumentStroke;
9+
import com.demcha.compose.document.style.DocumentTextStyle;
10+
import com.demcha.compose.document.svg.SvgIcon;
11+
import com.demcha.examples.support.ExampleOutputPaths;
12+
13+
import java.nio.file.Path;
14+
15+
/**
16+
* Runnable showcase for v1.8 block-level horizontal alignment: a fixed-size
17+
* node (an SVG icon, a vector path, …) placed LEFT / CENTER / RIGHT within the
18+
* page content width with one call — the {@code margin: auto} the flow does not
19+
* give fixed nodes on its own.
20+
*
21+
* <pre>{@code
22+
* flow.addSvgIcon(icon, 48, HorizontalAlign.CENTER);
23+
* flow.addAligned(HorizontalAlign.RIGHT, anyFixedNode);
24+
* }</pre>
25+
*
26+
* @author Artem Demchyshyn
27+
*/
28+
public final class BlockAlignExample {
29+
30+
private static final DocumentColor INK = DocumentColor.rgb(34, 38, 50);
31+
private static final DocumentColor TEAL = DocumentColor.rgb(20, 80, 95);
32+
33+
/** Inline two-tone badge so the example needs no icon resource. */
34+
private static final String BADGE_SVG = """
35+
<svg viewBox="0 0 24 24">
36+
<circle cx="12" cy="12" r="11" fill="#fde9e3"/>
37+
<path fill="#c41e3a" d="M12 2 L22 12 L12 22 L2 12 Z"/>
38+
</svg>
39+
""";
40+
41+
private BlockAlignExample() {
42+
}
43+
44+
/**
45+
* Renders the alignment sheet: one icon and one path, each shown
46+
* left / centre / right aligned.
47+
*
48+
* @return path to the generated PDF
49+
* @throws Exception if rendering or file IO fails
50+
*/
51+
public static Path generate() throws Exception {
52+
Path pdfFile = ExampleOutputPaths.prepare("features/layout", "block-align.pdf");
53+
54+
SvgIcon icon = SvgIcon.parse(BADGE_SVG);
55+
DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(INK);
56+
57+
try (DocumentSession document = GraphCompose.document(pdfFile)
58+
.pageSize(420, 400)
59+
.margin(DocumentInsets.of(28))
60+
.create()) {
61+
document.pageFlow(page -> {
62+
page.addParagraph(p -> p
63+
.text("Block alignment")
64+
.textStyle(DocumentTextStyle.DEFAULT.withSize(20)));
65+
page.addParagraph(p -> p
66+
.text("A fixed-size node left-aligns in the flow by default. "
67+
+ "addSvgIcon(icon, w, align) / addAligned(align, node) seats it "
68+
+ "left, centre, or right across the content width — no manual maths.")
69+
.textStyle(DocumentTextStyle.DEFAULT.withSize(9.5)
70+
.withColor(DocumentColor.rgb(90, 96, 105)))
71+
.padding(DocumentInsets.bottom(8)));
72+
73+
for (HorizontalAlign align : HorizontalAlign.values()) {
74+
page.addParagraph(p -> p
75+
.text("addSvgIcon(icon, 44, HorizontalAlign." + align + ")")
76+
.textStyle(caption)
77+
.padding(DocumentInsets.top(6)));
78+
page.addSvgIcon(icon, 44, align);
79+
}
80+
81+
page.addParagraph(p -> p
82+
.text("addAligned(CENTER, anyNode) — works for any fixed node, e.g. a path")
83+
.textStyle(caption)
84+
.padding(DocumentInsets.top(10)));
85+
page.addAligned(HorizontalAlign.CENTER, chevron());
86+
});
87+
88+
document.buildPdf();
89+
}
90+
91+
return pdfFile;
92+
}
93+
94+
/** A small stroked chevron path to show alignment is not icon-specific. */
95+
private static com.demcha.compose.document.node.DocumentNode chevron() {
96+
return new com.demcha.compose.document.dsl.PathBuilder()
97+
.name("Chevron")
98+
.size(60, 30)
99+
.moveTo(0.0, 1.0).lineTo(0.5, 0.0).lineTo(1.0, 1.0)
100+
.stroke(DocumentStroke.of(TEAL, 4))
101+
.lineCap(com.demcha.compose.document.style.DocumentLineCap.ROUND)
102+
.lineJoin(com.demcha.compose.document.style.DocumentLineJoin.ROUND)
103+
.build();
104+
}
105+
106+
public static void main(String[] args) throws Exception {
107+
System.out.println("Generated: " + generate());
108+
}
109+
}

examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
9797
feature("shapes", "shape-container", "Shape-as-Container", "Rounded rect, ellipse, circle containers with ClipPolicy and layered children.", "shapes", "clip");
9898
feature("svg", "svg-icon-gallery", "SVG Icon Gallery", "34 real-world multicolour svgrepo icons through SvgIcon.parse — native vector layers, the whole set 156 KB of sources.", "svg", "icons", "v1.8");
9999
feature("shapes", "vector-path", "Vector Paths (Bézier)", "addPath(...) — free-form design shapes with native cubic Bézier curves: stroked waves, filled blobs, mixed line/curve ribbons. No tessellation.", "shapes", "bezier", "v1.8");
100+
feature("layout", "block-align", "Block Alignment", "addAligned(align, node) / addSvgIcon(icon, w, align) — seat any fixed-size node left / centre / right across the content width.", "layout", "align", "v1.8");
100101
feature("transforms", "transforms", "Layers + Transforms", "rotate / scale on every leaf builder + LayerStack with explicit z-index.", "transforms", "layers");
101102
feature("text", "rich-text-showcase", "Rich Text", "Inline runs with bold / italic / colour / link options, markdown parsing.", "text", "rich");
102103
feature("text", "section-presets", "Section Presets", "Pre-baked section bands, accent strips, soft panels for templates.", "text", "sections");
@@ -145,6 +146,7 @@ static String groupLabel(String category, String group) {
145146
case "features/streaming" -> "Streaming & I/O";
146147
case "features/snapshots" -> "Snapshot Testing";
147148
case "features/svg" -> "SVG Import";
149+
case "features/layout" -> "Layout & Alignment";
148150
case "features/debug" -> "Debug & Diagnostics";
149151
case "flagships/default" -> "Flagship Demos";
150152
default -> capitalize(group);

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,42 @@ public T addSvgIcon(com.demcha.compose.document.svg.SvgIcon icon, double width)
559559
return add(icon.node(width));
560560
}
561561

562+
/**
563+
* Adds a multi-layer SVG icon at the given width, horizontally aligned
564+
* within the available content width — the one-call way to centre or
565+
* right-align an icon on the page.
566+
*
567+
* @param icon parsed SVG icon
568+
* @param width target icon width in points
569+
* @param align horizontal placement within the available width
570+
* @return this builder
571+
* @since 1.8.0
572+
*/
573+
@com.demcha.compose.document.api.Beta
574+
public T addSvgIcon(com.demcha.compose.document.svg.SvgIcon icon, double width,
575+
com.demcha.compose.document.node.HorizontalAlign align) {
576+
Objects.requireNonNull(icon, "icon");
577+
return addAligned(align, icon.node(width));
578+
}
579+
580+
/**
581+
* Adds a node positioned horizontally within the full available content
582+
* width — the block-level {@code margin: auto} / {@code align(center)} for
583+
* any fixed-size node (path, image, icon, barcode, shape container) that
584+
* would otherwise left-align in the flow.
585+
*
586+
* @param align horizontal placement
587+
* @param node the node to position
588+
* @return this builder
589+
* @since 1.8.0
590+
*/
591+
public T addAligned(com.demcha.compose.document.node.HorizontalAlign align,
592+
com.demcha.compose.document.node.DocumentNode node) {
593+
Objects.requireNonNull(align, "align");
594+
Objects.requireNonNull(node, "node");
595+
return add(new com.demcha.compose.document.node.AlignNode(node, align));
596+
}
597+
562598
/**
563599
* Adds a filled circle ellipse — shortcut for
564600
* {@code addEllipse(e -> e.circle(diameter).fillColor(fillColor))}.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static NodeRegistry registerDefaults(NodeRegistry registry) {
4444
.register(new CanvasLayerDefinition())
4545
.register(new PolygonDefinition())
4646
.register(new PathDefinition())
47+
.register(new AlignDefinition())
4748
.register(new ChartDefinition());
4849
}
4950
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.demcha.compose.document.layout.definitions;
2+
3+
import com.demcha.compose.document.layout.*;
4+
import com.demcha.compose.document.layout.payloads.PreparedStackLayout;
5+
import com.demcha.compose.document.node.AlignNode;
6+
import com.demcha.compose.document.node.DocumentNode;
7+
import com.demcha.compose.document.node.HorizontalAlign;
8+
import com.demcha.compose.document.node.LayerAlign;
9+
10+
import java.util.List;
11+
12+
/**
13+
* Layout definition for {@link AlignNode}: a wrapper that fills the available
14+
* content width and seats its single child left / centre / right. It reuses
15+
* the stack placement machinery (one layer, one anchor) — the only difference
16+
* from a {@code LayerStackNode} is that the box is measured to the full
17+
* available width instead of shrink-wrapping to the child, which is exactly
18+
* what makes horizontal alignment visible against the page.
19+
*
20+
* @author Artem Demchyshyn
21+
* @since 1.8.0
22+
*/
23+
public final class AlignDefinition implements NodeDefinition<AlignNode> {
24+
25+
/**
26+
* Creates the align layout definition.
27+
*/
28+
public AlignDefinition() {
29+
}
30+
31+
@Override
32+
public Class<AlignNode> nodeType() {
33+
return AlignNode.class;
34+
}
35+
36+
@Override
37+
public PreparedNode<AlignNode> prepare(AlignNode node, PrepareContext ctx, BoxConstraints constraints) {
38+
DocumentNode child = node.child();
39+
double childInner = Math.max(0.0, constraints.availableWidth() - child.margin().horizontal());
40+
PreparedNode<DocumentNode> childPrepared = ctx.prepare(child, BoxConstraints.natural(childInner));
41+
double height = childPrepared.measureResult().height() + child.margin().vertical();
42+
// Fill the width (so the anchor has room to centre / right-align);
43+
// height tracks the child so the wrapper adds no vertical space.
44+
return PreparedNode.composite(
45+
node,
46+
new MeasureResult(constraints.availableWidth(), height),
47+
new PreparedStackLayout(
48+
List.of(toAnchor(node.align())),
49+
List.of(0.0),
50+
List.of(0.0),
51+
List.of(0)),
52+
new CompositeLayoutSpec(0.0, CompositeLayoutSpec.Axis.STACK));
53+
}
54+
55+
/**
56+
* Maps a block alignment to the top-seated stack anchor. The wrapper's box
57+
* height equals the child's, so the vertical band of the anchor is moot;
58+
* only the horizontal placement matters.
59+
*/
60+
private static LayerAlign toAnchor(HorizontalAlign align) {
61+
return switch (align) {
62+
case LEFT -> LayerAlign.TOP_LEFT;
63+
case CENTER -> LayerAlign.TOP_CENTER;
64+
case RIGHT -> LayerAlign.TOP_RIGHT;
65+
};
66+
}
67+
68+
@Override
69+
public PaginationPolicy paginationPolicy(AlignNode node) {
70+
return PaginationPolicy.ATOMIC;
71+
}
72+
73+
@Override
74+
public List<DocumentNode> children(AlignNode node) {
75+
return node.children();
76+
}
77+
78+
@Override
79+
public List<LayoutFragment> emitFragments(PreparedNode<AlignNode> prepared,
80+
FragmentContext ctx,
81+
FragmentPlacement placement) {
82+
return List.of();
83+
}
84+
}

0 commit comments

Comments
 (0)