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; + } +}