diff --git a/src/main/java/com/demcha/compose/document/layout/CompileContext.java b/src/main/java/com/demcha/compose/document/layout/CompileContext.java
new file mode 100644
index 00000000..8bf51b88
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/layout/CompileContext.java
@@ -0,0 +1,27 @@
+package com.demcha.compose.document.layout;
+
+import java.util.List;
+
+/**
+ * The cursor + emission context threaded through the compile pass: the page
+ * cursor, the node-preparation and fragment-emission contexts, and the output
+ * accumulators. Bundled so the per-node compile collaborators take one context
+ * argument instead of repeating the same five parameters.
+ *
+ *
Package-private — engine surface, not public API. The accumulators are the
+ * caller's live lists (appended to, not copied); the {@code state} cursor is
+ * mutated as pages advance.
+ *
+ * @param state the compiler page cursor (mutated as pages advance)
+ * @param prepareContext the node-preparation context
+ * @param fragmentContext the fragment-emission context
+ * @param nodes accumulator for placed nodes (appended to)
+ * @param fragments accumulator for placed fragments (appended to)
+ * @author Artem Demchyshyn
+ */
+record CompileContext(CompilerState state,
+ PrepareContext prepareContext,
+ FragmentContext fragmentContext,
+ List nodes,
+ List fragments) {
+}
diff --git a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java
index 11dff7d8..5ee452f6 100644
--- a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java
+++ b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java
@@ -246,9 +246,9 @@ private void compileComposite(PreparedNode prepared,
}
if (layoutSpec.axis() == CompositeLayoutSpec.Axis.STACK) {
- compileStackedLayer(prepared, definition, path, semanticName, parentPath, childIndex, depth,
- regionX, state, prepareContext, fragmentContext, nodes, fragments,
- margin, padding, availableWidth, naturalMeasure);
+ StackedLayerCompiler.compile(this, prepared, definition, path, semanticName, parentPath, childIndex,
+ depth, regionX, margin, padding, naturalMeasure,
+ new CompileContext(state, prepareContext, fragmentContext, nodes, fragments));
return;
}
@@ -562,140 +562,6 @@ private void compileHorizontalRow(PreparedNode prepared,
state.usedHeight += rowOuterHeight;
}
- private void compileStackedLayer(PreparedNode prepared,
- NodeDefinition definition,
- String path,
- String semanticName,
- String parentPath,
- int childIndex,
- int depth,
- double regionX,
- CompilerState state,
- PrepareContext prepareContext,
- FragmentContext fragmentContext,
- List nodes,
- List fragments,
- Margin margin,
- Padding padding,
- double availableWidth,
- MeasureResult naturalMeasure) {
- DocumentNode node = prepared.node();
- double stackOuterHeight = naturalMeasure.height() + margin.vertical();
- admitAtomicBlock(stackOuterHeight, path, state);
-
- int startPage = state.pageIndex;
- double placementX = regionX + margin.left();
- double placementTopY = state.pageTop() - state.usedHeight - margin.top();
- double placementY = placementTopY - naturalMeasure.height();
- int decorationInsertIndex = fragments.size();
- int stackNodeIndex = nodes.size();
- nodes.add(null);
-
- PreparedStackLayout stackLayout =
- prepared.requirePreparedLayout(PreparedStackLayout.class);
- List children = definition.children(node);
- double innerWidth = Math.max(0.0, naturalMeasure.width() - padding.horizontal());
- double innerHeight = Math.max(0.0, naturalMeasure.height() - padding.vertical());
- double innerStartX = placementX + padding.left();
- double innerTopY = placementTopY - padding.top();
-
- // Sort layers by ascending zIndex (stable — equal zIndex keeps
- // source order). Iteration order then determines render order:
- // earlier in the list → drawn first / behind. childIndex below
- // is the SOURCE index so semantic paths stay stable for tests
- // and snapshots; only the iteration order shifts.
- int[] iterationOrder = LayerStackGeometry.zOrder(stackLayout.zIndices());
- PlacementContext layerHostCtx = new FixedSlotPlacementContext(
- state.pageIndex, state.canvas, prepareContext, fragmentContext, nodes, fragments);
- for (int slot = 0; slot < iterationOrder.length; slot++) {
- int index = iterationOrder[slot];
- placeStackLayer(
- children.get(index),
- index,
- path,
- depth,
- innerStartX,
- innerTopY,
- innerWidth,
- innerHeight,
- stackLayout.alignments().get(index),
- stackLayout.offsetsX().get(index),
- stackLayout.offsetsY().get(index),
- layerHostCtx);
- }
-
- int endPage = state.pageIndex;
- double endPageBottomY = placementTopY - naturalMeasure.height() + margin.bottom();
- List decorationFragments = CompositeDecoration.fill(
- prepared,
- definition,
- path,
- parentPath,
- childIndex,
- depth,
- placementX,
- placementTopY,
- endPageBottomY,
- naturalMeasure.width(),
- startPage,
- endPage,
- margin,
- padding,
- state.canvas,
- DocumentBleed.none(),
- fragmentContext);
- if (!decorationFragments.isEmpty()) {
- fragments.addAll(decorationInsertIndex, decorationFragments);
- }
-
- // Overlay fragments arrive AFTER children — they are the
- // "after the body" half of paired begin/end markers (e.g. the
- // graphics-state restore of a ShapeContainerNode clip path).
- List overlayFragments = CompositeDecoration.overlay(
- prepared,
- definition,
- path,
- parentPath,
- childIndex,
- depth,
- placementX,
- placementTopY,
- endPageBottomY,
- naturalMeasure.width(),
- startPage,
- endPage,
- margin,
- padding,
- state.canvas,
- fragmentContext);
- if (!overlayFragments.isEmpty()) {
- fragments.addAll(overlayFragments);
- }
-
- nodes.set(stackNodeIndex, new PlacedNode(
- path,
- semanticName,
- node.nodeKind(),
- parentPath,
- childIndex,
- depth,
- depth,
- placementX,
- placementY,
- placementX,
- placementY,
- naturalMeasure.width(),
- naturalMeasure.height(),
- startPage,
- endPage,
- naturalMeasure.width(),
- naturalMeasure.height(),
- margin,
- padding));
-
- state.usedHeight += stackOuterHeight;
- }
-
private void compileAtomicLeaf(PreparedNode prepared,
NodeDefinition definition,
String path,
@@ -791,7 +657,7 @@ private void placeAtomicLeafFragments(PreparedNode prepared,
/**
* Places a single layer inside a stack composite at the given inner-box
- * coordinates. Shared between {@link #compileStackedLayer} (mutating
+ * coordinates. Shared between {@link StackedLayerCompiler} (mutating
* placement path) and the STACK branch of {@link #compileNodeInFixedSlot}
* (non-mutating placement path). Both feed per-layer offsets from
* {@link PreparedStackLayout}, so a {@code position(node, dx, dy, align)}
@@ -802,18 +668,18 @@ private void placeAtomicLeafFragments(PreparedNode prepared,
* because each layer occupies a single fixed page slot whose origin
* has already been resolved by the alignment + offset math here.
*/
- private void placeStackLayer(DocumentNode child,
- int sourceIndex,
- String parentPath,
- int parentDepth,
- double innerStartX,
- double innerTopY,
- double innerWidth,
- double innerHeight,
- com.demcha.compose.document.node.LayerAlign align,
- double layerOffsetX,
- double layerOffsetY,
- PlacementContext ctx) {
+ void placeStackLayer(DocumentNode child,
+ int sourceIndex,
+ String parentPath,
+ int parentDepth,
+ double innerStartX,
+ double innerTopY,
+ double innerWidth,
+ double innerHeight,
+ com.demcha.compose.document.node.LayerAlign align,
+ double layerOffsetX,
+ double layerOffsetY,
+ PlacementContext ctx) {
PreparedNode childPrepared =
prepareForRegionWidth(ctx.prepareContext(), child, innerWidth);
MeasureResult childMeasure = childPrepared.measureResult();
@@ -944,7 +810,7 @@ private double compileNodeInFixedSlot(PreparedNode prepared,
double stackInnerStartX = placementX + padding.left();
double stackInnerTopY = placementTopY - padding.top();
- // Same z-index iteration order as compileStackedLayer
+ // Same z-index iteration order as StackedLayerCompiler
// (root-level case). Source-order semantic paths are
// preserved — only render order shifts.
int[] iterationOrder = LayerStackGeometry.zOrder(stackLayout.zIndices());
@@ -1158,7 +1024,7 @@ private String semanticName(DocumentNode node) {
* @param state the compiler cursor (mutated: may advance to a new page)
* @throws AtomicNodeTooLargeException if the block cannot fit on a full page
*/
- private void admitAtomicBlock(double outerHeight, String path, CompilerState state) {
+ void admitAtomicBlock(double outerHeight, String path, CompilerState state) {
double fullPageHeight = state.activeInnerHeight();
if (outerHeight > fullPageHeight + CAPACITY_TOLERANCE) {
throw AtomicNodeTooLargeException.forNode(path, outerHeight, fullPageHeight);
diff --git a/src/main/java/com/demcha/compose/document/layout/StackedLayerCompiler.java b/src/main/java/com/demcha/compose/document/layout/StackedLayerCompiler.java
new file mode 100644
index 00000000..edde78d2
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/layout/StackedLayerCompiler.java
@@ -0,0 +1,184 @@
+package com.demcha.compose.document.layout;
+
+import com.demcha.compose.document.layout.payloads.PreparedStackLayout;
+import com.demcha.compose.document.node.DocumentNode;
+import com.demcha.compose.document.style.DocumentBleed;
+import com.demcha.compose.engine.components.style.Margin;
+import com.demcha.compose.engine.components.style.Padding;
+
+import java.util.List;
+
+/**
+ * Places a {@link com.demcha.compose.document.node.LayerStackNode} layer stack:
+ * admits the stack band onto the page, renders each layer into the stack's inner
+ * rectangle in stable z-index order, then emits the fill / overlay decoration and
+ * records the stack's placed node.
+ *
+ * The per-layer placement recurses back through the host compiler
+ * ({@code host.placeStackLayer(...)} → {@code compileNodeInFixedSlot}), so
+ * {@link #compile} takes the {@link LayoutCompiler} as a host argument and calls
+ * back into it rather than owning the recursion. Package-private — engine surface,
+ * not public API.
+ *
+ * @author Artem Demchyshyn
+ */
+final class StackedLayerCompiler {
+
+ private StackedLayerCompiler() {
+ // Utility class, no instantiation.
+ }
+
+ /**
+ * Compiles a layer stack onto the current page.
+ *
+ * @param host the compiler that owns the per-layer recursion and the
+ * atomic-block admission guard
+ * @param prepared the prepared stack node (carries its {@link PreparedStackLayout})
+ * @param definition the stack's node definition
+ * @param path the stack's layout path
+ * @param semanticName the stack's semantic name
+ * @param parentPath the parent's layout path
+ * @param childIndex the stack's index among its siblings
+ * @param depth the stack's tree depth
+ * @param regionX the content region's left edge
+ * @param margin the stack's margin
+ * @param padding the stack's padding
+ * @param naturalMeasure the stack's measured size
+ * @param ctx the cursor + emission context (mutated as the stack is placed)
+ */
+ static void compile(LayoutCompiler host,
+ PreparedNode prepared,
+ NodeDefinition definition,
+ String path,
+ String semanticName,
+ String parentPath,
+ int childIndex,
+ int depth,
+ double regionX,
+ Margin margin,
+ Padding padding,
+ MeasureResult naturalMeasure,
+ CompileContext ctx) {
+ CompilerState state = ctx.state();
+ PrepareContext prepareContext = ctx.prepareContext();
+ FragmentContext fragmentContext = ctx.fragmentContext();
+ List nodes = ctx.nodes();
+ List fragments = ctx.fragments();
+
+ DocumentNode node = prepared.node();
+ double stackOuterHeight = naturalMeasure.height() + margin.vertical();
+ host.admitAtomicBlock(stackOuterHeight, path, state);
+
+ int startPage = state.pageIndex;
+ double placementX = regionX + margin.left();
+ double placementTopY = state.pageTop() - state.usedHeight - margin.top();
+ double placementY = placementTopY - naturalMeasure.height();
+ int decorationInsertIndex = fragments.size();
+ int stackNodeIndex = nodes.size();
+ nodes.add(null);
+
+ PreparedStackLayout stackLayout =
+ prepared.requirePreparedLayout(PreparedStackLayout.class);
+ List children = definition.children(node);
+ double innerWidth = Math.max(0.0, naturalMeasure.width() - padding.horizontal());
+ double innerHeight = Math.max(0.0, naturalMeasure.height() - padding.vertical());
+ double innerStartX = placementX + padding.left();
+ double innerTopY = placementTopY - padding.top();
+
+ // Sort layers by ascending zIndex (stable — equal zIndex keeps
+ // source order). Iteration order then determines render order:
+ // earlier in the list → drawn first / behind. childIndex below
+ // is the SOURCE index so semantic paths stay stable for tests
+ // and snapshots; only the iteration order shifts.
+ int[] iterationOrder = LayerStackGeometry.zOrder(stackLayout.zIndices());
+ PlacementContext layerHostCtx = new FixedSlotPlacementContext(
+ state.pageIndex, state.canvas, prepareContext, fragmentContext, nodes, fragments);
+ for (int slot = 0; slot < iterationOrder.length; slot++) {
+ int index = iterationOrder[slot];
+ host.placeStackLayer(
+ children.get(index),
+ index,
+ path,
+ depth,
+ innerStartX,
+ innerTopY,
+ innerWidth,
+ innerHeight,
+ stackLayout.alignments().get(index),
+ stackLayout.offsetsX().get(index),
+ stackLayout.offsetsY().get(index),
+ layerHostCtx);
+ }
+
+ int endPage = state.pageIndex;
+ double endPageBottomY = placementTopY - naturalMeasure.height() + margin.bottom();
+ List decorationFragments = CompositeDecoration.fill(
+ prepared,
+ definition,
+ path,
+ parentPath,
+ childIndex,
+ depth,
+ placementX,
+ placementTopY,
+ endPageBottomY,
+ naturalMeasure.width(),
+ startPage,
+ endPage,
+ margin,
+ padding,
+ state.canvas,
+ DocumentBleed.none(),
+ fragmentContext);
+ if (!decorationFragments.isEmpty()) {
+ fragments.addAll(decorationInsertIndex, decorationFragments);
+ }
+
+ // Overlay fragments arrive AFTER children — they are the
+ // "after the body" half of paired begin/end markers (e.g. the
+ // graphics-state restore of a ShapeContainerNode clip path).
+ List overlayFragments = CompositeDecoration.overlay(
+ prepared,
+ definition,
+ path,
+ parentPath,
+ childIndex,
+ depth,
+ placementX,
+ placementTopY,
+ endPageBottomY,
+ naturalMeasure.width(),
+ startPage,
+ endPage,
+ margin,
+ padding,
+ state.canvas,
+ fragmentContext);
+ if (!overlayFragments.isEmpty()) {
+ fragments.addAll(overlayFragments);
+ }
+
+ nodes.set(stackNodeIndex, new PlacedNode(
+ path,
+ semanticName,
+ node.nodeKind(),
+ parentPath,
+ childIndex,
+ depth,
+ depth,
+ placementX,
+ placementY,
+ placementX,
+ placementY,
+ naturalMeasure.width(),
+ naturalMeasure.height(),
+ startPage,
+ endPage,
+ naturalMeasure.width(),
+ naturalMeasure.height(),
+ margin,
+ padding));
+
+ state.usedHeight += stackOuterHeight;
+ }
+}