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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.demcha.compose.engine.components.style.Margin;

import static com.demcha.compose.document.layout.NodeDefinitionSupport.EPS;

/**
* Mutable bookkeeping for the page-flow path of {@link LayoutCompiler}: the
* canvas the document is being placed on, the active page index, the height
Expand Down Expand Up @@ -80,4 +82,40 @@ void newPage() {
void touchPage() {
maxTouchedPage = Math.max(maxTouchedPage, pageIndex);
}

/**
* Advances the flow by {@code amount}, spilling to a fresh page first when the
* amount does not fit in the remaining height (and the page has already been
* used). A non-positive amount is dropped; the used height never exceeds the
* active page's content height.
*/
void advanceSpace(double amount) {
if (amount <= EPS) {
return;
}
if (amount > remainingHeight() + EPS && usedHeight > EPS) {
newPage();
}
touchPage();
usedHeight = Math.min(activeInnerHeight(), usedHeight + amount);
}

/**
* Closes out a composite's bottom edge. A positive bottom inset advances the
* flow as usual; a NEGATIVE one (a negative bottom margin) pulls the following
* sibling up — symmetric with a negative top margin, which already offsets via
* {@code placementTopY}. The plain {@link #advanceSpace} drops a non-positive
* amount, so the closing edge needs this dedicated path. The top-of-node
* reservation deliberately stays on {@link #advanceSpace} so a negative top
* margin keeps its existing flow behaviour; only the closing edge gains the
* pull-up. The cursor never drops below the page top.
*/
void closeBottomSpace(double amount) {
if (amount >= EPS) {
advanceSpace(amount);
} else if (amount <= -EPS) {
touchPage();
usedHeight = Math.max(0.0, usedHeight + amount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ private void compileComposite(PreparedNode<DocumentNode> prepared,
int nodeIndex = nodes.size();
nodes.add(null);

advanceSpace(startReservation, state);
state.advanceSpace(startReservation);
List<DocumentNode> children = definition.children(node);
double childRegionX = placementX + padding.left();
double childRegionWidth = Math.max(0.0, availableWidth - padding.horizontal());
Expand Down Expand Up @@ -346,11 +346,11 @@ private void compileComposite(PreparedNode<DocumentNode> prepared,
nodes,
fragments);
if (index < children.size() - 1) {
advanceSpace(layoutSpec.spacing(), state);
state.advanceSpace(layoutSpec.spacing());
}
}

closeBottomSpace(padding.bottom() + margin.bottom(), state);
state.closeBottomSpace(padding.bottom() + margin.bottom());
int endPage = state.pageIndex;
double endPageBottomY = state.pageTop() - state.usedHeight + margin.bottom();

Expand Down Expand Up @@ -1395,35 +1395,6 @@ private void addPlacedFragments(List<LayoutFragment> emitted,
fragments.addAll(CompositeDecoration.toPlacedFragments(emitted, placement));
}

private void advanceSpace(double amount, CompilerState state) {
if (amount <= EPS) {
return;
}
if (amount > state.remainingHeight() + EPS && state.usedHeight > EPS) {
state.newPage();
}
state.touchPage();
state.usedHeight = Math.min(state.activeInnerHeight(), state.usedHeight + amount);
}

/**
* Closes out a composite's bottom edge. A positive bottom inset advances the
* flow as usual; a NEGATIVE one (a negative bottom margin) pulls the following
* sibling up — symmetric with a negative top margin, which already offsets via
* {@code placementTopY}. The plain {@link #advanceSpace} drops a non-positive
* amount, so the closing edge needs this dedicated path. The top-of-node
* reservation deliberately stays on {@link #advanceSpace} so a negative top
* margin keeps its existing flow behaviour; only the closing edge gains the
* pull-up. The cursor never drops below the page top.
*/
private void closeBottomSpace(double amount, CompilerState state) {
if (amount >= EPS) {
advanceSpace(amount, state);
} else if (amount <= -EPS) {
state.touchPage();
state.usedHeight = Math.max(0.0, state.usedHeight + amount);
}
}

private String pathFor(DocumentNode node, String parentPath, int childIndex) {
String base = semanticName(node);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.demcha.compose.document.layout;

import com.demcha.compose.engine.components.style.Margin;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;

/**
* Unit coverage for the page-flow cursor arithmetic on {@link CompilerState}
* (the {@code advanceSpace} / {@code closeBottomSpace} helpers pulled off
* {@link LayoutCompiler}). A 200×100 canvas with 10pt margins gives an 80pt
* content height, so the page fills at {@code usedHeight == 80}.
*/
class CompilerStateTest {

private static final double EPS = 1e-9;

private static CompilerState state() {
return new CompilerState(LayoutCanvas.from(200, 100, Margin.of(10)));
}

@Test
void advanceSpaceConsumesHeightWhenItFits() {
CompilerState state = state();
state.advanceSpace(30);
assertThat(state.usedHeight).isCloseTo(30, within(EPS));
assertThat(state.pageIndex).isZero();
assertThat(state.maxTouchedPage).isZero();
}

@Test
void advanceSpaceSpillsToANewPageWhenItOverflowsAUsedPage() {
CompilerState state = state();
state.usedHeight = 70; // 10pt left on the page
state.advanceSpace(20); // doesn't fit → new page, then consume
assertThat(state.pageIndex).isEqualTo(1);
assertThat(state.usedHeight).isCloseTo(20, within(EPS));
}

@Test
void advanceSpaceDropsANonPositiveAmount() {
CompilerState state = state();
state.usedHeight = 15;
state.advanceSpace(0);
state.advanceSpace(-5);
assertThat(state.usedHeight).isCloseTo(15, within(EPS));
assertThat(state.pageIndex).isZero();
}

@Test
void advanceSpaceDoesNotPaginateAFreshPageAndCapsAtContentHeight() {
CompilerState state = state(); // usedHeight 0 → the "already used" guard is false
state.advanceSpace(1000); // over-tall on an empty page: stays, capped
assertThat(state.pageIndex).isZero();
assertThat(state.usedHeight).isCloseTo(80, within(EPS));
}

@Test
void closeBottomSpacePositiveAdvancesLikeAdvanceSpace() {
CompilerState state = state();
state.usedHeight = 10;
state.closeBottomSpace(20);
assertThat(state.usedHeight).isCloseTo(30, within(EPS));
}

@Test
void closeBottomSpaceNegativePullsTheCursorUpClampedAtZero() {
CompilerState state = state();
state.usedHeight = 30;
state.closeBottomSpace(-10);
assertThat(state.usedHeight).isCloseTo(20, within(EPS));

state.usedHeight = 5;
state.closeBottomSpace(-10); // would go negative → clamped to 0
assertThat(state.usedHeight).isZero();
}
}
Loading