Rich text: mixed styles in one paragraph
June 22, 2026 · View on GitHub
RichText is the fluent builder for paragraphs that need more than one
styled segment — label/value pairs, accented status keywords, inline
links, even small geometric shapes drawn on the text baseline. Each
chained call appends one inline run; the runs wrap together as a single
paragraph. Author it through addRich on any flow container
(pageFlow, module, section).
Mixed styles in one paragraph
import com.demcha.compose.document.style.DocumentColor;
section.addRich(rich -> rich
.plain("Status: ")
.bold("Pending")
.plain(" — last review on ")
.accent("Mar 14", DocumentColor.rgb(40, 90, 180))
.plain(", supersedes ")
.strikethrough("Mar 02"));
The full set of decorated runs: plain, bold, italic, boldItalic,
underline, strikethrough, color(text, color), size(text, points),
and accent(text, color) — bold-and-coloured in one call, the typical
pattern for status keywords ("Paid", "Overdue"). For anything beyond
those, style(text, DocumentTextStyle) takes a fully explicit style:
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.font.FontName;
section.addRich(rich -> rich
.plain("Code: ")
.style("RT-2026-04", DocumentTextStyle.builder()
.fontName(FontName.COURIER_BOLD)
.size(10)
.build()));
Reusable fragments
RichText.text(...) starts a standalone builder; append(other) splices
its runs into another paragraph, so recurring fragments (a badge, a
branded product name) live in one place:
import com.demcha.compose.document.dsl.RichText;
RichText badge = RichText.text("GraphCompose ").bold("v1.8");
section.addRich(rich -> rich
.plain("Built with ")
.append(badge)
.plain(" — see the changelog."));
addRich(RichText) also accepts a pre-built instance directly.
Inline links
section.addRich(rich -> rich
.plain("Read the ")
.link("authoring cheatsheet", "https://example.com/cheatsheet")
.plain(" before writing a template."));
// Shortcut when the whole paragraph is one link sentence:
section.addLink("Open the project page", "https://example.com");
link(text, uri) renders with default link styling and a clickable
annotation on supporting backends; with(text, style, linkOptions)
combines an explicit style with link metadata. On ParagraphBuilder,
inlineLink(text, options) is the equivalent low-level call.
For in-document navigation, linkTo(text, anchor) points at a named
anchor(...) elsewhere in the document instead of a URL, and inline images
and shapes can link to an anchor too (imageLinkTo / shapeLinkTo). See
in-pdf-navigation.md.
Inline images
import com.demcha.compose.document.image.DocumentImageData;
import com.demcha.compose.document.node.InlineImageAlignment;
section.addRich(rich -> rich
.plain("Powered by ")
.image(DocumentImageData.fromPath("assets/logo.png"), 24, 12,
InlineImageAlignment.CENTER)
.plain(" since 2024."));
The image flows with the text and wraps like a word. Alignment is
relative to the surrounding line (CENTER by default); the full
overload adds a baselineOffset and DocumentLinkOptions for a
clickable inline image.
Inline SVG icons
A parsed SvgIcon sits on the text baseline like a word, drawn as crisp
vector layers that carry their own colours — so it renders independently
of the active font's glyph coverage. size is the glyph height in points;
the width follows the icon's aspect ratio.
import com.demcha.compose.document.svg.SvgIcon;
SvgIcon star = SvgIcon.parse(
"<svg viewBox='0 0 24 24'>"
+ " <path d='M12 2l3 7h7l-5.5 4 2 7L12 16l-6.5 4 2-7L2 9h7z' fill='#f5b301'/>"
+ "</svg>");
section.addRich(rich -> rich
.plain("Rated ")
.svgIcon(star, 11)
.plain(" by reviewers."));
On ParagraphBuilder the equivalent call is inlineSvgIcon(icon, size);
both take alignment / baselineOffset / link overloads and a clickable
form via DocumentLinkOptions. SvgIcon.parse(String) reads inline SVG
markup; SvgIcon.read(Path) loads it from a file.
Emoji / shortcodes
emoji(":code:") resolves a GitHub-style shortcode to an inline colour
glyph through EmojiLibrary. Resolution is lenient: an unknown shortcode —
or no emoji set on the classpath — falls back to the literal text, the way
GitHub renders an unrecognised :code:.
section.addRich(rich -> rich
.plain("Deploy ")
.emoji(":white_check_mark:", 11)
.plain(" succeeded ")
.emoji(":rocket:", 11));
On ParagraphBuilder the call is inlineEmoji(":code:", size). Glyphs ship
in the optional, independently-versioned graph-compose-emoji companion
artifact (Noto Emoji, SIL OFL 1.1) — add it to the classpath to resolve
shortcodes; the engine itself carries no emoji art.
Inline shapes and checkboxes
Geometric figures drawn from geometry — not font glyphs — so they render identically regardless of font coverage. Rating dots, directional arrows, breadcrumb chevrons, and todo checkboxes are the headline uses:
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.ShapeOutline;
section.addRich(rich -> rich
.plain("Java ")
.dot(5, brand).dot(5, brand).dot(5, brand)
.dot(5, null, DocumentStroke.of(brand, 0.6)) // outlined = empty slot
.plain(" Draft ")
.arrow(8, ShapeOutline.Direction.RIGHT, accent)
.plain(" Review ")
.chevron(6, ShapeOutline.Direction.RIGHT, muted)
.plain(" Done ")
.diamond(7, accent).star(8, accent));
section.addRich(rich -> rich
.checkbox(10, true, green)
.plain(" Checked todo marker"));
section.addRich(rich -> rich
.checkbox(10, false, muted)
.plain(" Unchecked todo marker"));
Beyond the named shortcuts (dot, ellipse, diamond, triangle,
star, arrow, chevron), shape(ShapeOutline, fill) accepts any
ShapeOutline — e.g. ShapeOutline.checkmark(8, 8) or
ShapeOutline.regularPolygon(8, 8, 6) for a hexagon. The checkbox has
"pick your tick" overloads: pass a ShapeOutline.CheckmarkStyle, or any
sized ShapeOutline as the checked-state mark. Arrows likewise take a
ShapeOutline.ArrowStyle to swap the design.
Paragraph-level controls
addRich is a shorthand for addParagraph(p -> p.rich(...)). Drop down
to the paragraph builder when the rich content needs alignment, line
spacing, or margins:
import com.demcha.compose.document.node.TextAlign;
section.addParagraph(p -> p
.rich(rich -> rich
.plain("Centred line with an ")
.accent("accented", brand)
.plain(" keyword."))
.align(TextAlign.CENTER)
.lineSpacing(2));
Runnable showcases: RichTextShowcaseExample (every fluent run) and InlineShapesExample (shapes, checkboxes, and design variants).