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
Binary file modified assets/readme/examples/table-of-contents.pdf
Binary file not shown.
17 changes: 16 additions & 1 deletion src/main/java/com/demcha/compose/document/dsl/TocBuilder.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.demcha.compose.document.dsl;

import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.RowVerticalAlign;
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentLeader;
import com.demcha.compose.document.style.DocumentLineCap;
import com.demcha.compose.document.style.DocumentRowColumn;
Expand All @@ -29,6 +31,13 @@ public final class TocBuilder {

private static final DocumentColor DEFAULT_LEADER_COLOR = DocumentColor.rgb(150, 150, 150);

// A bottom-aligned thin line lands on the descender line; lifting it by roughly the
// font's descent seats it on the text baseline. The exact descent is a font metric
// not reachable from the DSL layer, so this is a deliberate approximation tuned for
// typical text faces. It assumes the entry and page-number styles share a size — the
// common case; if they differ they sit on different baselines regardless.
private static final double LEADER_BASELINE_LIFT_RATIO = 0.2;

private String title = "";
private DocumentTextStyle titleStyle = DocumentTextStyle.DEFAULT.withSize(16);
private DocumentTextStyle entryStyle = DocumentTextStyle.DEFAULT;
Expand Down Expand Up @@ -147,15 +156,21 @@ List<DocumentNode> buildEntries() {
}

private DocumentNode buildEntryRow(Entry entry) {
// Bottom-align so the leader sits on the entries' baseline rather than
// riding along the top of the line.
RowBuilder row = new RowBuilder()
.gap(6)
.verticalAlign(RowVerticalAlign.BOTTOM)
.columns(DocumentRowColumn.auto(), DocumentRowColumn.weight(1), DocumentRowColumn.auto());
row.addParagraph(p -> p.text(entry.label()).textStyle(entryStyle).linkTo(entry.anchor()));
if (leader == DocumentLeader.NONE) {
row.addSpacer(s -> s.width(1).height(1));
} else {
// Lift the leader off the descender line onto the baseline (see the ratio above).
double baselineLift = entryStyle.size() * LEADER_BASELINE_LIFT_RATIO;
row.addLine(line -> {
line.fill().stroke(DocumentStroke.of(leaderColor, 1.0));
line.fill().stroke(DocumentStroke.of(leaderColor, 1.0))
.margin(DocumentInsets.bottom(baselineLift));
if (leader == DocumentLeader.DOTS) {
line.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.demcha.compose.document.dsl;

import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LineNode;
import com.demcha.compose.document.node.RowNode;
import com.demcha.compose.document.node.RowVerticalAlign;
import com.demcha.compose.document.style.DocumentLeader;
import org.junit.jupiter.api.Test;

import java.util.List;

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

/**
Expand All @@ -11,6 +19,34 @@
*/
class TocBuilderTest {

@Test
void entryRowIsBottomAlignedSoTheLeaderSitsOnTheBaseline() {
List<DocumentNode> nodes = new TocBuilder()
.leader(DocumentLeader.DOTS)
.entry("Introduction", "intro")
.buildEntries();

DocumentNode entryRow = nodes.get(nodes.size() - 1); // no title set -> the entry row is last
assertThat(entryRow).isInstanceOf(RowNode.class);
assertThat(((RowNode) entryRow).verticalAlign()).isEqualTo(RowVerticalAlign.BOTTOM);
}

@Test
void theLeaderIsLiftedOntoTheBaselineNotLeftOnTheDescender() {
// Bottom-alignment alone leaves the leader on the descender line — the user-visible
// fix is the upward lift, so pin that the leader carries a positive bottom margin.
List<DocumentNode> nodes = new TocBuilder()
.leader(DocumentLeader.DOTS)
.entry("Introduction", "intro")
.buildEntries();

LineNode leader = (LineNode) ((RowNode) nodes.get(nodes.size() - 1)).children().stream()
.filter(LineNode.class::isInstance)
.findFirst()
.orElseThrow();
assertThat(leader.margin().bottom()).isGreaterThan(0.0);
}

@Test
void entryRejectsBlankLabel() {
assertThatExceptionOfType(IllegalArgumentException.class)
Expand Down
Loading