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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
All notable changes to GraphCompose are documented here. Versions
follow semantic versioning; release dates are ISO 8601.

## v1.9.1 — Planned

Table columns now contain long inline-code content instead of letting it spill
over the next column.

### Bug fixes

- **Long inline-code chips no longer overflow their column.** A long
`inlineCode(...)` / `inlineHighlight(...)` token with no spaces — a package
coordinate, fully-qualified class name or URL — now breaks *within* its
paragraph or table cell instead of drawing over the neighbouring content. It
breaks at `.` `:` `/` `-` seams and char-splits only when a segment is still
too wide, keeping the rounded chip fill intact on every wrapped fragment.
Applies to all three paragraph wrap paths (plain, inline, markdown); text that
already fits is laid out exactly as before.

- **`auto()` table columns grow to fit composed cell content.** A composed cell
(`DocumentTableCell.node(...)`) in an `auto()` column now contributes its
intrinsic content width, so the column sizes to the content (e.g. an
inline-code chip) instead of collapsing toward zero. `fixed(...)` columns are
unchanged. As with plain-text auto columns, a table too narrow for the summed
intrinsic width of its auto columns reports `exceeds available width`.

## v1.9.0 — 2026-06-29

In-document navigation. Rendered PDFs can now declare named **anchors** and
Expand Down
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 @@ -75,6 +75,7 @@ are with the canonical DSL, then jump to its detailed section below.
| [Section presets](#section-presets) | `pageBackground`, `band`, `softPanel`, `accentLeft / Right / Top / Bottom`, per-corner `DocumentCornerRadius` | [PDF](../assets/readme/examples/section-presets.pdf) · [Source](src/main/java/com/demcha/examples/features/text/SectionPresetsExample.java) |
| [Nested lists](#nested-lists-v16) | `ListBuilder.addItem(label, Consumer)` — depth cascade, per-depth markers, mixed flat / nested authoring | [PDF](../assets/readme/examples/nested-list-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/lists/NestedListExample.java) |
| [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) |
| [Inline-code column wrap](#inline-code-column-wrap) | A long `inlineCode(...)` coordinate breaks at its `. : / -` seams inside a narrow **fixed** column and an **auto** column grows to fit it on one line | [PDF](../assets/readme/examples/inline-code-column-wrap.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java) |
| [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) |
| [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) |
Expand Down Expand Up @@ -577,6 +578,24 @@ table.columns(...)
[📄 View PDF](../assets/readme/examples/table-advanced.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java)

### Inline-code column wrap

A composed cell holding one long `inlineCode(...)` token — a Maven coordinate,
fully-qualified class name or URL — stays inside its column. In a narrow
**fixed** column the chip breaks at its `. : / -` seams (char-splitting only
when a segment is still too wide), with the rounded fill intact on every
fragment; in an **auto** column the column grows to fit the coordinate on one
line instead of collapsing.

```java
DocumentTableCell.node(document.dsl().paragraph()
.inlineCode("org.junit.jupiter:junit-jupiter:5.10.2").build())
// fixed(104) column -> wraps at seams; auto() column -> grows to one line
```

[📄 View PDF](../assets/readme/examples/inline-code-column-wrap.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java)

### Custom Business Theme

Build a `BusinessTheme` from raw `DocumentPalette` / `SpacingScale` /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.demcha.examples.features.streaming.HttpStreamingExample;
import com.demcha.examples.features.svg.SvgIconGalleryExample;
import com.demcha.examples.features.tables.ComposedTableCellExample;
import com.demcha.examples.features.tables.InlineCodeColumnWrapExample;
import com.demcha.examples.features.tables.TableAdvancedExample;
import com.demcha.examples.features.text.EmojiGalleryExample;
import com.demcha.examples.features.text.EmojiShortcodeExample;
Expand Down Expand Up @@ -151,6 +152,7 @@ public static void main(String[] args) throws Exception {
// v1.6 layout primitives
System.out.println("Generated: " + NestedListExample.generate());
System.out.println("Generated: " + ComposedTableCellExample.generate());
System.out.println("Generated: " + InlineCodeColumnWrapExample.generate());
System.out.println("Generated: " + CanvasLayerExample.generate());

// v1.5 visual primitives
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.demcha.examples.features.tables;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.node.TableNode;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.table.DocumentTableCell;
import com.demcha.compose.document.table.DocumentTableColumn;
import com.demcha.compose.document.table.DocumentTableStyle;
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;
import java.util.List;

/**
* Runnable showcase for long inline-code coordinates inside table columns.
*
* <p>A composed cell holding a single long {@code inlineCode(...)} token — a
* Maven coordinate, fully-qualified class name or URL — no longer overflows its
* column. In a narrow <b>fixed</b> column the chip breaks <em>inside</em> the
* cell, preferring its {@code . : / -} seams and char-splitting only as a last
* resort, with the rounded fill intact on every fragment. In an <b>auto</b>
* column the column grows to fit the coordinate on one line instead of
* collapsing.</p>
*
* @author Artem Demchyshyn
*/
public final class InlineCodeColumnWrapExample {

private static final DocumentColor INK = DocumentColor.rgb(34, 38, 50);
private static final DocumentColor MUTED = DocumentColor.rgb(102, 106, 118);
private static final DocumentColor RULE = DocumentColor.rgb(180, 188, 200);
private static final DocumentColor HEADER_FILL = DocumentColor.rgb(20, 60, 75);
private static final DocumentColor ROW_TINT = DocumentColor.rgb(244, 247, 252);

private static final List<String> COORDINATES = List.of(
"org.junit.jupiter:junit-jupiter:5.10.2",
"io.github.demchaav:graph-compose:1.9.0",
"https://repo1.maven.org/maven2/");

private InlineCodeColumnWrapExample() {
}

public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("features/tables", "inline-code-column-wrap.pdf");

DocumentTextStyle title = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA_BOLD).size(20).color(INK)
.decoration(DocumentTextDecoration.BOLD).build();
DocumentTextStyle caption = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA_OBLIQUE).size(10).color(MUTED).build();
DocumentTextStyle body = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA).size(10.5).color(INK).build();
DocumentTextStyle headerText = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA_BOLD).size(11).color(DocumentColor.WHITE)
.decoration(DocumentTextDecoration.BOLD).build();

DocumentTableStyle headerStyle = DocumentTableStyle.builder()
.fillColor(HEADER_FILL).stroke(DocumentStroke.of(RULE, 0.6))
.padding(DocumentInsets.of(8)).textStyle(headerText).build();
DocumentTableStyle bodyCellStyle = DocumentTableStyle.builder()
.stroke(DocumentStroke.of(RULE, 0.6)).padding(DocumentInsets.of(8)).textStyle(body).build();
DocumentTableStyle tintedCellStyle = DocumentTableStyle.builder()
.fillColor(ROW_TINT).stroke(DocumentStroke.of(RULE, 0.6))
.padding(DocumentInsets.of(8)).textStyle(body).build();

try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.margin(36, 36, 36, 36)
.create()) {

document.pageFlow()
.name("CodeWrapShowcase")
.spacing(8)
.addParagraph("Long inline-code coordinates stay inside the column", title)
.addParagraph(
"A composed cell with one long inlineCode(...) token used to spill over the "
+ "next column. It now breaks at its . : / - seams inside a narrow FIXED "
+ "column (char-splitting only when a segment is still too wide), and an "
+ "AUTO column grows to fit it on one line.", caption)
.build();

List<List<DocumentTableCell>> rows = new java.util.ArrayList<>();
rows.add(List.of(
DocumentTableCell.text("Fixed 104pt").withStyle(headerStyle),
DocumentTableCell.text("Auto").withStyle(headerStyle)));
for (int i = 0; i < COORDINATES.size(); i++) {
String coordinate = COORDINATES.get(i);
DocumentTableStyle cellStyle = i % 2 == 0 ? bodyCellStyle : tintedCellStyle;
rows.add(List.of(
DocumentTableCell.node(document.dsl().paragraph().inlineCode(coordinate).build())
.withStyle(cellStyle),
DocumentTableCell.node(document.dsl().paragraph().inlineCode(coordinate).build())
.withStyle(cellStyle)));
}

document.add(new TableNode(
"CodeWrapTable",
List.of(DocumentTableColumn.fixed(104),
DocumentTableColumn.auto()),
rows,
bodyCellStyle,
null,
DocumentInsets.zero(),
DocumentInsets.zero()));

document.buildPdf();
}
return outputFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ static ResolvedTableLayoutWithContents resolveTableLayout(TableNode node,
List<List<LogicalCell>> logicalRows = buildLogicalRows(node, columnCount);
List<TableColumnLayout> normalizedSpecs = normalizeSpecs(node, columnCount);
TableCellLayoutStyle[][] stylesGrid = buildStylesGrid(node, logicalRows, columnCount);
double[] naturalWidths = resolveNaturalColumnWidths(node, normalizedSpecs, logicalRows, stylesGrid, columnCount, measurement);
double innerAvailableWidth = Math.max(0.0, availableWidth - node.padding().horizontal());
double[] naturalWidths = resolveNaturalColumnWidths(node, normalizedSpecs, logicalRows, stylesGrid,
columnCount, measurement, prepareContext, innerAvailableWidth);
double naturalWidth = sum(naturalWidths);
double[] finalWidths = resolveFinalColumnWidths(node, normalizedSpecs, naturalWidths, naturalWidth);
double finalWidth = sum(finalWidths);

double innerAvailableWidth = Math.max(0.0, availableWidth - node.padding().horizontal());
if (finalWidth > innerAvailableWidth + EPS) {
throw new IllegalStateException("Table '" + displayName(node) + "' width " + finalWidth
+ " exceeds available width " + innerAvailableWidth + ".");
Expand Down Expand Up @@ -488,7 +489,9 @@ private static double[] resolveNaturalColumnWidths(TableNode node,
List<List<LogicalCell>> logicalRows,
TableCellLayoutStyle[][] stylesGrid,
int columnCount,
TextMeasurementSystem measurement) {
TextMeasurementSystem measurement,
PrepareContext prepareContext,
double innerAvailableWidth) {
double[] widths = new double[columnCount];
double[] singleCellRequired = new double[columnCount];

Expand All @@ -498,8 +501,12 @@ private static double[] resolveNaturalColumnWidths(TableNode node,
continue;
}
int col = logical.startColumn();
singleCellRequired[col] = Math.max(singleCellRequired[col],
cellNaturalWidth(logical.sanitizedLines(), stylesGrid[rowIndex][col], measurement));
double required = composedAutoCellNaturalWidth(logical, normalizedSpecs.get(col),
stylesGrid[rowIndex][col], prepareContext, innerAvailableWidth);
if (required < 0.0) {
required = cellNaturalWidth(logical.sanitizedLines(), stylesGrid[rowIndex][col], measurement);
}
singleCellRequired[col] = Math.max(singleCellRequired[col], required);
}
}

Expand Down Expand Up @@ -596,6 +603,35 @@ private static double[] resolveFinalColumnWidths(TableNode node,
return finalWidths;
}

/**
* Natural width contribution of a composed cell that sits in an AUTO column:
* the child's intrinsic content width plus the cell padding, so the AUTO
* column grows to fit composed content (e.g. an inline-code chip) instead of
* being sized to zero by its plain-text siblings. The child is measured
* against the table's inner width, so the result is already clamped to what
* the table can give — it never forces the table past its available width.
*
* <p>Returns a negative sentinel when this is not a composed cell in an AUTO
* column, so the caller falls back to the plain-text {@link #cellNaturalWidth}
* formula. Composed cells in FIXED columns keep contributing nothing: the
* column width is fixed and an over-wide child wraps/breaks inside it.</p>
*/
private static double composedAutoCellNaturalWidth(LogicalCell logical,
TableColumnLayout spec,
TableCellLayoutStyle style,
PrepareContext prepareContext,
double innerAvailableWidth) {
DocumentTableCell src = logical.source();
if (src == null || !src.hasComposedContent() || !spec.isAuto() || prepareContext == null) {
return -1.0;
}
Padding padding = style.padding() == null ? Padding.zero() : style.padding();
double childInner = Math.max(0.0, innerAvailableWidth - padding.horizontal());
double natural = prepareContext.prepare(src.content(), BoxConstraints.natural(childInner))
.measureResult().width();
return natural + padding.horizontal();
}

private static double cellNaturalWidth(List<String> sanitizedLines,
TableCellLayoutStyle style,
TextMeasurementSystem measurement) {
Expand Down
Loading
Loading