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
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,40 @@ follow semantic versioning; release dates are ISO 8601.

### Fixed

- **A composite node inside a composed table cell renders its children.**
`DocumentTableCell.node(...)` holding a `SectionNode`, `ContainerNode`,
`RowNode` or `LayerStackNode` measured the child, reserved its full height,
and then drew nothing inside it — a correctly-sized blank hole in the table.
A composite leaves its children to the compiler and emits only its own
decoration from `emitFragments`, so dispatching a composed cell straight at
the child's `emitFragments` picked up the section background and dropped
every paragraph under it. The cell now lays the child's whole sub-tree out
inside the cell box, with the same column / row / stack layout the sub-tree
gets anywhere else on the page. Leaf children (paragraph, list) and nested
tables already worked and are unchanged. The row stays atomic: a composed
cell still does not split across a page break.

- **A row nested in a fixed rectangle keeps its horizontal band.** A `RowNode`
inside a `LayerStackNode` layer — allowed since 1.6.2 — stacked its children
downwards instead of seating them side by side, and because the band was
measured as one row tall, every child after the first spilled out of the
layer. The fixed-rectangle walk had no horizontal branch at all: it is a
vertical y-cursor, right for a section or a container and wrong for a row.
It now resolves slot widths through the same `RowSlots` path the page-level
row band uses, so a nested row honours weights, fixed columns, flex
arrangement and vertical alignment identically. Vertical composites in a
fixed rectangle are unchanged. Found while composing a row into a table
cell, which is the second rectangle this walk fills.

**Behaviour note:** a row nested *directly inside another row* in a fixed
rectangle now raises the same `IllegalStateException` the page-level row
band has always raised (`"cannot contain a nested horizontal row"`, which
names the fix: wrap the inner row in its own layer). It previously
produced a layout instead — but not a usable one: with a two-child inner
row inside a two-child outer row in a layer, two of the three leaves
landed on the same point, 17pt below the layer's own bottom edge. Wrapping
the inner row in its own `LayerStackNode` layer lays it out correctly.

- **The SVG reader honours the opacity family.** `opacity`, `fill-opacity` and
`stroke-opacity` — attribute or `style=""`, number or percentage, with SVG's
inheritance for the paint slots and composition for group `opacity` — now
Expand Down Expand Up @@ -69,6 +103,17 @@ follow semantic versioning; release dates are ISO 8601.

### Documentation

- **`DocumentTableCell.text("a\nb")` is one line, and now says so.** The
advanced-tables recipe demonstrated a multi-line cell by putting `\n` inside
`text(...)`, which renders as a single line — the newline is whitespace
between two words there. The recipe and the `DocumentTableCell` Javadoc now
name the three cell shapes explicitly: `text(...)` for one line,
`lines(...)` for several, `node(...)` for any registered node (and
`ParagraphNode` *does* honour `\n` as a hard break, inside a cell as
anywhere else). The two examples that showed the misleading form were
switched to `lines(...)`, and the composed-cell showcase gained a section
and a row inside table cells.

- **The SVG Javadoc stopped describing a younger reader.** `SvgGradients` claimed
focal radials and `stop-opacity` are "loudly refused" — both degrade
deliberately (centred-radial approximation, opaque stops, alpha-only overlay
Expand Down
Binary file modified assets/readme/examples/composed-table-cell-showcase.pdf
Binary file not shown.
Binary file modified assets/readme/examples/table-advanced.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
import com.demcha.compose.font.FontLibrary;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -30,6 +31,14 @@ public final class DocumentLayoutPassContext implements PrepareContext, Fragment
private final PageGeometry pageGeometry;
private final Map<String, Integer> nodeStartPages;
private final Map<PreparedNodeCacheKey, PreparedNode<?>> preparedNodes = new HashMap<>();
/**
* Compiler used to lay out a composite child inside a box its parent owns
* (see {@link #emitChildFragments}). Created on first use so a pass with no
* composed cells never builds one; a pass is single-threaded, and the
* compiler carries no state beyond the registry, so one instance serves
* every box in the pass.
*/
private LayoutCompiler subtreeCompiler;

/**
* Creates a layout-pass context with no resolved page numbers — the first
Expand Down Expand Up @@ -165,10 +174,61 @@ public <E extends DocumentNode> List<LayoutFragment> emitChildFragments(
FragmentPlacement placement) {
Objects.requireNonNull(child, "child");
Objects.requireNonNull(placement, "placement");
if (child.isComposite()) {
return emitCompositeSubtree((PreparedNode<DocumentNode>) child, placement);
}
NodeDefinition<E> definition = (NodeDefinition<E>) registry.definitionFor(child.node());
return definition.emitFragments(child, this, placement);
}

/**
* Lays out a composite child's whole sub-tree inside {@code placement} and
* returns its fragments in the placement's local coordinate space.
*
* <p>A composite's own {@code emitFragments} yields nothing but its
* decoration — the section background, the container border — because the
* compiler, not the definition, walks {@link NodeDefinition#children}.
* Dispatching to it alone would leave the caller with a correctly measured
* but empty box. The fixed-box walk applies the same column / row / stack
* layout the sub-tree would get at document level, then the absolute
* coordinates it produces are rebased onto the placement so the caller can
* translate them into its own fragment space exactly as it does for a leaf
* child.</p>
*/
private List<LayoutFragment> emitCompositeSubtree(PreparedNode<DocumentNode> child,
FragmentPlacement placement) {
if (subtreeCompiler == null) {
subtreeCompiler = new LayoutCompiler(registry);
}
List<PlacedFragment> placed = subtreeCompiler.compileFixedBoxSubtree(
child,
placement.parentPath(),
placement.childIndex(),
placement.depth(),
placement.x(),
placement.y() + placement.height(),
placement.width(),
placement.pageIndex(),
canvas,
this,
this);
if (placed.isEmpty()) {
return List.of();
}
List<LayoutFragment> local = new ArrayList<>(placed.size());
for (PlacedFragment fragment : placed) {
local.add(new LayoutFragment(
fragment.path(),
fragment.fragmentIndex(),
fragment.x() - placement.x(),
fragment.y() - placement.y(),
fragment.width(),
fragment.height(),
fragment.payload()));
}
return List.copyOf(local);
}

private long normalizeWidth(double value) {
return Math.round(value * 1_000.0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ default boolean markdownEnabled() {
}

/**
* Dispatches fragment emission to a previously-prepared child node
* via the active {@code NodeRegistry}.
* Emits the fragments of a previously-prepared child sub-tree, laid out
* inside {@code placement}.
*
* <p>Used by composite primitives (e.g. {@code TableNode} cells with
* {@code content}) that hold a prepared child sub-tree and need to
Expand All @@ -53,12 +53,23 @@ default boolean markdownEnabled() {
* {@code FragmentContext} implementations have to opt-in to the
* recursion.</p>
*
* <p>A <em>leaf</em> child is dispatched straight to its
* {@link NodeDefinition#emitFragments}. A <em>composite</em> child is laid
* out whole: its own {@code emitFragments} yields nothing but decoration
* (a section background, a container border) because the compiler, not the
* definition, walks {@link NodeDefinition#children} — so the implementation
* seats the sub-tree inside the placement with the same column / row /
* stack layout it would get at document level and returns every fragment
* the walk produced, not just the child's own. The returned fragments are
* local to {@code placement}, as for a leaf, so callers translate them
* into their own fragment space the same way either way.</p>
*
* @param child prepared child node previously obtained from
* {@link PrepareContext#prepare(DocumentNode, BoxConstraints)}
* @param placement placement assigned to the child within the
* composite parent's geometry
* @param <E> child node type
* @return fragments emitted by the child's {@code NodeDefinition}
* @return fragments of the child sub-tree, local to {@code placement}
* @throws UnsupportedOperationException when the
* {@code FragmentContext} implementation does not back
* child-fragment emission
Expand Down
Loading
Loading