Skip to content
Open
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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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)}.</p>
*
* @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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* @param text the text
* @param fg the text colour
* @param bg the chip fill colour; must not be {@code null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ParagraphTextSpan> spans = textSpans(p -> p
.textStyle(small)
.inlineText("Build ")
.inlineChip(" Paid ", DocumentColor.rgb(22, 101, 52), FILL));

ParagraphTextSpan plain = spans.stream().filter(s -> s.background() == null).findFirst().orElseThrow();
ParagraphTextSpan chip = spans.stream().filter(s -> s.background() != null).findFirst().orElseThrow();
assertThat(plain.textStyle().size()).isEqualTo(9.0, within(1e-9));
assertThat(chip.textStyle().size())
.as("the chip is drawn at the paragraph size, not the 14 pt default")
.isEqualTo(9.0, within(1e-9));
assertThat(chip.textStyle().color()).isEqualTo(new Color(22, 101, 52));

// Measured, not just declared: the same chip in a default-styled paragraph
// is wider, because its glyphs really are bigger.
List<ParagraphTextSpan> defaultSized = textSpans(p -> p
.inlineChip(" Paid ", DocumentColor.rgb(22, 101, 52), FILL));
double wide = defaultSized.stream().filter(s -> s.background() != null).findFirst().orElseThrow().width();
assertThat(chip.width()).as("a 9 pt chip measures narrower than a 14 pt one").isLessThan(wide);
}

@Test
void explicitlyStyledChipKeepsItsOwnSize() throws Exception {
List<ParagraphTextSpan> spans = textSpans(p -> p
.textStyle(DocumentTextStyle.builder().size(9).build())
.inlineStyledChip("BIG", DocumentTextStyle.builder().size(18).build(), FILL));
ParagraphTextSpan chip = spans.stream().filter(s -> s.background() != null).findFirst().orElseThrow();
assertThat(chip.textStyle().size())
.as("an explicit chip style overrides the paragraph, both ways")
.isEqualTo(18.0, within(1e-9));
}

@Test
void twoAdjacentDifferentChipsStaySeparateSpans() throws Exception {
List<ParagraphTextSpan> spans = textSpans(p -> p
Expand Down
Loading