diff --git a/CHANGELOG.md b/CHANGELOG.md index dfbb7978f..a8b474965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ follow semantic versioning; release dates are ISO 8601. ## v2.3.0 — Planned +### Public API + +- **A chip is sized like the text around it.** `ParagraphBuilder.inlineChip(text, fg, bg)` + built its glyph style from scratch — `DocumentTextStyle.builder().color(fg)` — so the + badge came out at the 14 pt Helvetica default whatever the paragraph said. In a 9 pt + footer the words rendered at 9 pt and the chip between them rendered half again as + large, which reads as a rendering fault rather than a choice. + + The colour-only overload now derives its style from the paragraph's `textStyle` and + replaces only the colour: + + ```java + paragraph.textStyle(DocumentTextStyle.builder().size(9).build()) + .inlineText("Build ") + .inlineChip(" Paid ", GREEN_INK, GREEN_FILL); // a 9 pt chip + ``` + + That is how the rest of the builder already behaves. `inlineText`, `inlineLink` and + `inlineLinkTo` pass a `null` run style, which the layout resolves to the paragraph's, + and `inlineHighlight` documents that fallback on its `textStyle` parameter. The chip + sugar was the one call that opted out of it. + + **This moves rendered output** wherever a chip sits in a paragraph that is not at the + default style: the glyphs — and with them the chip's measured width — now follow the + paragraph. The paragraph style is read at the point of the call, so `textStyle(...)` + has to come before the chip. + +- **`inlineStyledChip(text, textStyle, bg)`** styles a chip that is meant to differ from + its paragraph: an explicit glyph style on a custom fill, keeping the default chip + radius and padding, which previously meant restating both through `inlineHighlight`. + It carries its own name rather than overloading `inlineChip` on the second parameter, + which would have made a literal `inlineChip(text, null, bg)` ambiguous and stopped it + compiling. + + `RichText.chip(...)` is unchanged and still draws at the default size: a rich-text + builder is assembled without a paragraph to read. Its documentation now says so, and + points at `highlight(...)` for an explicitly sized chip. + ### Templates - **Monogram Sidebar draws the employer.** Its experience entries rendered the position, diff --git a/core/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java b/core/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java index 35f75add7..b9e928e9e 100644 --- a/core/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java +++ b/core/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java @@ -372,16 +372,38 @@ public ParagraphBuilder inlineCode(String text, DocumentTextStyle textStyle) { * Adds a coloured chip: {@code text} in {@code fg} on a {@code bg} fill, with * the default code radius and padding. * + *
The glyphs take the paragraph's {@linkplain #textStyle(DocumentTextStyle) + * text style} — family, size and decoration — with only the colour replaced by + * {@code fg}, so a chip in a 9 pt paragraph is a 9 pt chip. The paragraph style is + * read when this method is called, so set it before the chip; to size a chip + * independently of the paragraph, pass the style explicitly with + * {@link #inlineStyledChip(String, DocumentTextStyle, DocumentColor)}.
+ * * @param text the text - * @param fg the text colour + * @param fg the text colour; {@code null} leaves the glyphs black * @param bg the chip fill colour; must not be {@code null} * @return this builder * @since 1.9.0 */ public ParagraphBuilder inlineChip(String text, DocumentColor fg, DocumentColor bg) { + return inlineStyledChip(text, this.textStyle.withColor(fg), bg); + } + + /** + * Adds a chip with an explicit glyph style on a {@code bg} fill, keeping the + * default code radius and padding — the escape hatch from + * {@link #inlineChip(String, DocumentColor, DocumentColor)} for a chip that is + * meant to differ from the paragraph around it. + * + * @param text the text + * @param textStyle the glyph style; falls back to the paragraph style when {@code null} + * @param bg the chip fill colour; must not be {@code null} + * @return this builder + * @since 2.3.0 + */ + public ParagraphBuilder inlineStyledChip(String text, DocumentTextStyle textStyle, DocumentColor bg) { Objects.requireNonNull(bg, "bg"); - this.inlineRuns.add(new InlineHighlightRun(text == null ? "" : text, - DocumentTextStyle.builder().color(fg).build(), + this.inlineRuns.add(new InlineHighlightRun(text == null ? "" : text, textStyle, new InlineBackground(bg, CodeChip.BACKGROUND.cornerRadius(), CodeChip.BACKGROUND.padding()))); this.text = ""; return this; diff --git a/core/src/main/java/com/demcha/compose/document/dsl/RichText.java b/core/src/main/java/com/demcha/compose/document/dsl/RichText.java index f3c70d583..4731cdd53 100644 --- a/core/src/main/java/com/demcha/compose/document/dsl/RichText.java +++ b/core/src/main/java/com/demcha/compose/document/dsl/RichText.java @@ -282,6 +282,13 @@ public RichText code(String text, DocumentTextStyle textStyle) { * Appends a coloured chip: {@code text} in {@code fg} on a {@code bg} fill, * with the default code radius and padding. * + *A rich-text builder is assembled without a paragraph, so — unlike + * {@link ParagraphBuilder#inlineChip(String, DocumentColor, DocumentColor)}, + * which reads the paragraph it is called on — these glyphs are drawn at the + * default family and size. In a paragraph styled to another size, reach for + * {@link #highlight(String, DocumentTextStyle, DocumentColor, double, DocumentInsets)} + * with an explicit style instead.
+ * * @param text the text * @param fg the text colour * @param bg the chip fill colour; must not be {@code null} diff --git a/core/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRunTest.java b/core/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRunTest.java index 8a82a0317..68f6d7905 100644 --- a/core/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRunTest.java +++ b/core/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRunTest.java @@ -3,6 +3,7 @@ import com.demcha.compose.document.node.InlineHighlightRun; import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; import com.demcha.compose.document.style.DocumentTextStyle; import com.demcha.compose.document.style.InlineBackground; import com.demcha.compose.font.FontName; @@ -83,6 +84,60 @@ void highlightCarriesTheExplicitChipAndOptionalLink() { assertThat(linked.linkTarget()).isNotNull(); } + @Test + void paragraphChipInheritsTheParagraphStyleAndOverridesOnlyTheColour() { + DocumentTextStyle paragraphStyle = DocumentTextStyle.builder() + .fontName(FontName.TIMES_ROMAN) + .size(9) + .decoration(DocumentTextDecoration.BOLD) + .build(); + InlineHighlightRun run = onlyHighlight(new ParagraphBuilder() + .textStyle(paragraphStyle) + .inlineChip(" Paid ", DocumentColor.rgb(0, 100, 0), DocumentColor.rgb(220, 255, 220))); + + // Everything but the colour is the paragraph's, compared as a whole so a + // component added to DocumentTextStyle later is covered too. Normalized to + // one shared colour because DocumentColor compares by identity. + assertThat(run.textStyle().withColor(DocumentColor.BLACK)) + .isEqualTo(paragraphStyle.withColor(DocumentColor.BLACK)); + assertThat(run.textStyle().size()).isEqualTo(9.0, within(1e-9)); + assertThat(run.textStyle().fontName()).isEqualTo(FontName.TIMES_ROMAN); + assertThat(run.textStyle().decoration()).isEqualTo(DocumentTextDecoration.BOLD); + assertThat(run.textStyle().color().color()).isEqualTo(new Color(0, 100, 0)); + // The fill and the chip geometry are the caller's, not the paragraph's. + assertThat(run.background().fill().color()).isEqualTo(new Color(220, 255, 220)); + assertThat(run.background().cornerRadius()).isEqualTo(3.0); + } + + @Test + void paragraphChipWithAnExplicitStyleIgnoresTheParagraph() { + DocumentTextStyle chipStyle = DocumentTextStyle.builder().size(20).build(); + InlineHighlightRun run = onlyHighlight(new ParagraphBuilder() + .textStyle(DocumentTextStyle.builder().size(9).build()) + .inlineStyledChip("BIG", chipStyle, DocumentColor.GRAY)); + assertThat(run.textStyle().size()).isEqualTo(20.0, within(1e-9)); + } + + @Test + void paragraphChipReadsTheStyleThatWasSetWhenItWasCalled() { + // The documented ordering caveat, pinned: the chip snapshots the paragraph + // style at the call, so a textStyle(...) that lands afterwards does not + // reach it. Change this test deliberately if the resolution ever moves to + // build() -- do not let it drift silently. + InlineHighlightRun run = onlyHighlight(new ParagraphBuilder() + .inlineChip("x", DocumentColor.rgb(0, 100, 0), DocumentColor.GRAY) + .textStyle(DocumentTextStyle.builder().size(9).build())); + assertThat(run.textStyle().size()).isEqualTo(DocumentTextStyle.DEFAULT.size(), within(1e-9)); + } + + private static InlineHighlightRun onlyHighlight(ParagraphBuilder paragraph) { + return paragraph.build().inlineRuns().stream() + .filter(InlineHighlightRun.class::isInstance) + .map(InlineHighlightRun.class::cast) + .findFirst() + .orElseThrow(() -> new AssertionError("no InlineHighlightRun in the paragraph")); + } + private static InlineHighlightRun onlyHighlight(RichText rich) { return rich.runs().stream() .filter(InlineHighlightRun.class::isInstance) diff --git a/qa/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java b/qa/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java index f96287b68..cded4e938 100644 --- a/qa/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java +++ b/qa/src/test/java/com/demcha/compose/document/dsl/InlineHighlightRenderTest.java @@ -19,6 +19,7 @@ import org.apache.pdfbox.text.PDFTextStripper; import org.junit.jupiter.api.Test; +import java.awt.Color; import java.awt.image.BufferedImage; import java.util.List; import java.util.function.Consumer; @@ -322,6 +323,43 @@ void wrappedChipMidParagraphKeepsItsFullTextAcrossTheBreak() throws Exception { } } + @Test + void chipFollowsTheParagraphSizeInsteadOfTheDefault() throws Exception { + // A chip is glyphs on a fill, so it has to be sized like the glyphs around + // it: in a 9 pt paragraph the badge measures 9 pt, not the 14 pt default. + DocumentTextStyle small = DocumentTextStyle.builder().size(9).build(); + List