Writing ClaFi Text

One string and a list of markers

Introduction

A Text is a plain string and a list of markers, each marker a position and one formatting item. It carries no fonts, no colours and no geometry - a marker says push bold here or this run is drawn in the accent, and what those mean is settled later, by the control that draws it and the theme in effect at the time.

That is what makes a Text cheap to build and safe to keep. Building one every paint is the normal way to use it: the shaping is cached behind the engine, and a rebuilt Text that says the same thing hits that cache.

This manual is about what a text says: its runs, styles, colours, paragraphs and links. How a control holds its text, draws it and answers for it is the Working with Text in Controls manual.

Module imports: ClaFi.Core.TextEngine.Text for Text, ClaFi.Core.TextEngine.Types for the formatting items and TextStyle, and ClaFi.Core.TextEngine.Fmt for the format-string syntax. Colours come from ClaFi.Core.System.InkWell - see the Colour manual.

Quick Start

Two spellings of the same thing. The stream is for text assembled from parts; the format string is for text whose shape is known and whose values are not.

// The stream.
Text text;
text << TextStyleId::Section << L"Performance" << PopTextStyle{} << k_endLine
     << InkWell::textInk(InkGrade::Muted) << L"Frames: " << PopColor{}
     << InkWell::accentInk() << frameCount;

// The format string. Both syntaxes in one: [tag] formats, {} takes an argument.
Text same{ Fmt{
    L"[section]Performance[/section]\n"
    L"[color muted]Frames: [/color][color accent]{}[/color]",
    frameCount } };

Building Text

The stream

Everything is streamed in with operator<<. The variadic constructor is the same sequence written as one expression, which is what makes a Text usable as a control property.

operator= takes any one of the same arguments and states the whole text rather than adding to it. Assigning one Text to another copies or moves it, so a text is an ordinary value to hold in a member or hand back from a function.

Text caption;
caption << L"Opening ";                 // adds
caption = L"Ready";                     // replaces
caption = other.selectedText(range);    // moves
caption = Text{ InkColor::Accent, name, L" saved" };   // several parts

An assignment operator takes exactly one argument, so a text built from several parts is spelt as a Text and moved in, as the last line shows.

The constructor stays explicit. It accepts any argument list at all, so an implicit one would offer a Text conversion from every type in every overload set it appears in. Name the type where a Text is built - Text{ L"Ready" } - and assign without it.

StreamedEffect
std::wstring_view, const wchar_t*, wchar_t, int, size_tAppends text. A count - a line, a column, a length - streams as the size_t it is held in.
Ink, PushThemeColor{ ink }Pushes a theme ink for the runs that follow.
InkGradePushes the framework's own ink at that step - InkWell::textInk(grade), written as the step alone.
InkColorPushes that colour at Strongest - InkColor::Red is InkWell::Red, written as the colour alone. Any other grade is streamed as the Ink it is.
Color, PushCustomColor{ c }Pushes a colour stated outright, which the theme does not touch.
PopColor{}Pops one colour off the stack.
TextStyleId, PopTextStyle{}Pushes and pops a named style.
PushFontSize{ f }, PopFontSize{}Overrides the size within the current style.
PushFontFamily{ L"..." }, PopFontFamily{}Overrides the family.
k_endLineEnds the paragraph. It is the character L'\n', streamed as any other wchar_t.
TextOp::PushBold … PopItalicWeight and slant.
TextOp::PushSuperscript, PushSubscript, PopScriptRaises or lowers the runs that follow, taking the size down with them.
TextAlignAlignment of the paragraph this lands in - see Paragraphs.
ParaIndent{ f }, ParaLineSpacing{ f }, setParaIndent(f), setParaLineSpacing(f)Paragraph geometry - see Paragraphs.
Space{ w }, FlexSpace{}, TabTo{ x }, VSpace{ h }Inline spacing objects - see below.
InTextIcon{ ... }A painted object that sits in the text flow.
PushLink{ L"..." }, PopLink{}A link to the target named, over the runs between the two - see Links and Anchors.
PushAnchor{ L"..." }, PopAnchor{}An anchor of that name over the runs between the two, which a link reaches with # and the name.
const Text&Appends another text with its markers, offset into place.

The rest of the surface is small: plainText(), markers(), empty(), clear(), hash(), hasFlexSpace(), anchorRange(name), and the editing calls replaceText and selectedText - see Copying and Moving Text.

Fmt and arguments

Fmt holds a format string and a reference to each argument. Text{ Fmt{ ... } } builds a text from it; text << Fmt{ ... } appends to one that exists.

The format string carries two constructs, and the difference between them is whether they consume an argument:

ConstructConsumes an argumentMeaning
[tag]NoChanges the stream state - a style, a colour, a space, an alignment.
{}, {:.2f}, {:02X}YesFormats the next argument through std::vformat, with the spec written inside the braces.
[color {}]YesThe bridge between the two: takes a Color argument and pushes it.
[icon {} 24,24]YesTakes a paint callable and places it inline.
\nNoA literal newline in the format string is a line break, the same as [n].

Arguments are consumed strictly in order by whichever construct takes one, so an [icon {}] and a {} draw from the same queue. A construct that wants an argument when none is left is dropped rather than diagnosed.

Text{ Fmt{
    L"[icon {} 18,18][b]{}[/b] [color muted]{:.1f} ms[/color]",
    paintDot,        // 1 - the icon
    name,            // 2 - the bold run
    elapsed } };     // 3 - the number
An argument's type picks its handler. Integers and floating point go through std::vformat; a Color prints as text under {} and pushes as a colour under [color {}]; anything callable with a PaintIconEvent& is an icon painter. Everything else is streamed as it stands.

Paragraphs

A newline ends a paragraph: k_endLine, which is L'\n', or [n] and [end] in a format string. Three properties belong to a paragraph rather than to a run of it - its alignment, its indent and its line spacing.

StreamedTagProperty
TextAlign::Left, Center, Right, Justified[left], [center], [right], [justified]Where each line stands across the width the paragraph is placed in. Justified stretches every line but the last to that width.
ParaIndent{ f }, setParaIndent(f)[indent 34]How far the paragraph stands in from the leading edge, its wrapped lines included.
ParaLineSpacing{ f }, setParaLineSpacing(f)[linespacing 1.5]A factor on the height of its lines, 1 being the font's own. The text and a format carry it; the layout does not apply it yet.

A paragraph property is read at the newline. Each of the three is a running value: a marker sets it, and a paragraph takes whatever the three hold when its newline is reached. So one written anywhere inside a paragraph applies to the whole of it, and it holds for every paragraph after until it is set again. One line set apart from the rest takes the property before its newline and gives it back after:

out << TextAlign::Center;
out << InTextIcon{ 220.0f, 24.0f, paintOrnament };
out << k_endLine;
out << TextAlign::Left;

Written before the newline, the TextAlign::Left would take the centring off the very line it was meant for.

The alignment places lines inside the width the paragraph is placed in. Where that width stands in a control - the whole of its box, or a block the text measures - is the control's text anchor: see Text Anchors. A control can state all three for every paragraph of its text as its TextFormat, and a property the text writes replaces the format's from the paragraph it stands in - see Text Format.

Tag Reference

Typography

TagStyleDrawn as
[title]TextStyleId::Titlesans-serif 28, bold
[subtitle]SubTitlesans-serif 20
[heading]Headingsans-serif 18, semibold
[subheading]SubHeadingsans-serif 16
[section]Sectionsans-serif 14, bold
[subsection]SubSectionsans-serif 14, italic
[body]Bodysans-serif 14 - the default
[subbody]SubBodysans-serif 12
[code]Codemonospace 14

Each tag is the style's TextStyleId member in lower case, and is short for [style title] - the spelling a document writes. Either form reads.

The two families are generic - GenericFamily::sansSerif and GenericFamily::monospace - and each platform resolves them to a face of its own: DirectWrite is handed Segoe UI and Consolas for them, fontconfig reads both names as the aliases its configuration defines. A concrete family named with [font] is passed through as written and stands or falls on the fonts installed.

A style closes with its own name or with [/style]; either pops the style opened last. Inside a style, three more pairs override one property at a time:

TagEffect
[b] [/b], [i] [/i]Weight and slant.
[sup] [/sup], [sub] [/sub]A run off the baseline. Either closer ends the nearest script still open.
[size 24] [/size]Font size in design pixels.
[font Consolas] [/font]Font family by name.
[n], [end]End the line. Interchangeable, and the same as a literal newline.
[left], [center], [right], [justified]Alignment of the paragraph the tag lands in.

A script takes the size of what it stands on down by a fixed factor and steps the baseline by a share of that size, so [sup] inside [sup] shrinks and steps again - 2[sup]2[sup]n[/sup][/sup] comes out as a tower of three sizes. One stack holds both kinds, which is why [/sup] and [/sub] are one instruction: each ends the nearest script still open.

The line is not made taller for a script. A raised run keeps inside the ascent of the run it stands on; a lowered one whose letters descend can reach below what its line was measured at.

Colour

A colour tag names an ink - an InkColor at an InkGrade - and states no tone of its own; the tones are InkWell's. Both are spelt as their members in lower case, the colour first, and either can be left out: the colour is then text and the grade strongest.

TagInk
[color strongest]textInk() - the ink the paint chain arrived at.
[color muted]textInk(InkGrade::Muted) - secondary text.
[color accent]accentInk() - the colour the interface answers with.
[color spot]spotInk() - the second accent, for what stands apart from the interface.
[color green], [color blue], [color red]The semantic inks of those names.
[color red muted]A colour at a grade. The colours are text, accent, spot, yellow, green, blue, red, black and white; the grades are faint, subtle, muted, strong and strongest.
[color accent 0.45]A grade between the steps, stated as its share.
[color #4EC9B0]A colour stated outright. The theme does not touch it.
[color {}]A Color taken from the arguments.
[/color]Pops the colour stack.
Yellow is drawn, not read. At the 4.5:1 reading threshold on a light page the whole warm arc collapses to brown, so no yellow is both legible and recognisable. Use InkWell::Yellow on an icon and let the text say it in words. [color yellow] reads, so that a text built from code can be written out and read back.

Layout and spacing

TagStreamed formEffect
[space 40]Space{ 40 }A fixed horizontal gap.
[flex], [fill], [space flex]FlexSpace{}Takes the leftover width of the line, pushing what follows to the right.
[tabto 150]TabTo{ 150 }Moves to an absolute x within the block.
[vspace 60]VSpace{ 60 }A minimum height for the row it stands in. Shorter than the natural height, it does nothing.
[indent 34]ParaIndent{ 34 }Indents the block, wrapped lines included, until it is set again. [indent 0] returns.
[linespacing 1.5]ParaLineSpacing{ 1.5 }Line spacing of the paragraph - see Paragraphs.

Icons

An icon in text is a PaintIconFunc - anything callable with a PaintIconEvent& - given a box in the flow. The tag takes the painter from the arguments and the box from its own text:

WrittenBox
[icon {}]16 × 16, baseline at 85% of the height.
[icon {} 24]24 × 24.
[icon {} 24,20]Width 24, height 20.
[icon {} 24,24,20]Width, height, and baseline stated outright.
InTextIcon{ w, h, baseline, painter, tag }The streamed form, which also carries a Tag the painter can read back.

The painter is handed an event that answers everything a control's paint event does about colour: event.ink(...), event.textColor(), event.iconRect(), event.scaleF(...). An icon of width 0 draws outside the flow without occupying it, which is how a bullet is made.

TagStreamed formEffect
[link https://example.com] [/link]PushLink{ L"https://example.com" }, PopLink{}A link to the target, which is everything after the word link, spaces included.
[link #clue-17] [/link]PushLink{ L"#clue-17" }, PopLink{}A link to the anchor of that name.
[anchor clue-17] [/anchor]PushAnchor{ L"clue-17" }, PopAnchor{}An anchor named clue-17. It draws nothing.

A target is read up to the closing bracket, so one holding a ] cannot be written in a format string. Stream a PushLink for it instead; a document and the clipboard carry it whole.

In a document

A Text in a document is its plain text and one entry per marker, and each marker is written as the tag Fmt reads for it. Import ClaFi.Core.Dom_TextSerializers wherever a blueprint states a Text, and Dt::Value{ L"Caption", Text{} } holding [title]Title[/title] and [color muted]note[/color] writes as

Caption=[
    Markers=[
        =[
            Tag=style title
            At=0
        ]
        =[
            Tag=/style
            At=5
        ]
        =[
            Tag=color muted
            At=10
        ]
        =[
            Tag=/color
            At=14
        ]
    ]
    Text=Title and note
]

At is the index in the plain text the marker applies from, and each child is found by name, in any order. A style is written as style and its name. An ink is written with its colour and its grade, each left out where it is text or strongest; the text ink at strongest is written color strongest. Every spelling in the tables above reads back, the style shorthand included.

An icon's painter is a callable and is not written. It reads back at its stated extent and draws nothing.

Worked Examples

Flexible space

Text{ Fmt{
    L"[color muted]System[/color][flex][b]ONLINE[/b]\n"
    L"[color muted]Ping[/color][flex]{} ms",
    ping } };
SystemOnline
Ping24 ms

Each line's flex space eats whatever that line has left over, so the right-hand column is flush against the right edge and ragged against the left. That is the difference from a tab stop, and the reason both exist.

Tabular alignment

Text{ Fmt{
    L"[b]Component[/b][tabto 150][b]Status[/b][tabto 260][b]Load[/b]\n"
    L"Parser[tabto 150][color green]Active[/color][tabto 260]High\n"
    L"Renderer[tabto 150][color muted]Standby[/color][tabto 260]Low" } };
ComponentStatusLoad
ParserActiveHigh
RendererStandbyLow

A tab stop is an absolute x inside the block, so every row lands on the same column whatever the width the block was given. Text that has already passed a stop does not go back to it.

A tab character in the text is the other kind of stop: it reaches the next multiple of four spaces of the font the paragraph starts in, so in a monospace font a tab is four columns.

Bullets and inline icons

auto paintBullet = [](PaintIconEvent& event) {
    FloatPoint center = event.iconRect().center();
    // The icon has no width, so the centre sits on the indent - shift it into the margin.
    center.x -= event.scaleF(16.0f);
    event.canvas().fillCircle(center, event.scaleF(3.0f),
        InkWell::accentInk().toColor(event));
    };

Text{ Fmt{
    L"[indent 34]"
    L"[icon {} 0,18][b]Lambda drawables:[/b] [color muted]any callable renders into the flow.[/color]\n"
    L"[icon {} 0,18][b]Reusability:[/b] [color muted]pass one painter as many times as you need.[/color]"
    L"[indent 0]",
    paintBullet, paintBullet } };
Lambda drawables: any callable renders into the flow.
Reusability: pass one painter as many times as you need.

The same painter passed twice draws twice - the argument is read, not consumed destructively - so a list of any length costs one lambda and one argument per row.

A link is a run of text that points somewhere, and an anchor is a run of text a link can point to. Both are pairs of markers, the way bold is, so they travel through every edit, copy and paste the rest of the formatting does. What a link does is up to the control holding the text: a TextBox follows it, and a label draws it and nothing more - see Links in a TextBox.

Text{ Fmt{
    L"See [link https://example.com]the site[/link] or "
    L"[link #gallery]the long gallery[/link]." } };

// The same link, streamed.
Text text;
text << L"See " << PushLink{ L"https://example.com" } << L"the site" << PopLink{};
See the site or the long gallery.

The target is any string at all - a URL, a path, # and an anchor name, or a word the application answers for itself, such as clafi:ledger. The engine never reads it.

A link is drawn in the accent ink. A colour pushed before the link opened gives way to it; one pushed inside the link is the link's own, which is how a link that should not look like one is written. The underline is not part of the text: the control underlines the link under the pointer when a click there would follow it, as the second link above shows, and draws nothing under it otherwise.

The underline stands clear of the ink it would cross, the way a browser's does: it is cut where a descender reaches through it, with a gap of its own thickness on either side - see The link underline.

L"[anchor clue-17][b]Clue 17.[/b][/anchor] Dmitri stands two doors from whoever was counting."
// ... and anywhere in the same text:
L"The blue landing is settled by [link #clue-17]Clue 17[/link]."

An anchor is a run rather than a point: it holds text, and a box going to it selects that text. It draws nothing. Where two anchors share a name the first in the text is the one reached, and one left open runs to the end of the text.

Text::anchorRange(name) answers the range an anchor holds, or nothing where the text holds no anchor of that name. It walks the markers when it is asked: an anchor is looked for when a link to it is followed, and nothing is kept for it in between.

A link does not grow at its end. A character typed after a bold word is bold, and every other run takes in what is typed at its end the same way. A link is the exception, because a word typed after it is not part of where it points. Typing over the end of a link is different: the text replaced was the link's, and what replaces it is too.

Two links that meet and name one target are one link. They are pointed at, underlined and followed whole, and what is typed where they meet goes inside. An undo that puts a link's last word back produces exactly such a pair, so the link it restores behaves as it did.

An anchor grows at its end the way bold does, since what is typed there is part of what it holds. Deleting the whole of a link or an anchor takes its markers with it, and an undo puts them back.

Do and Don't

Do

  • Name inks, not colours, so a run keeps its meaning through a theme change.
  • Use [tabto] for columns and [flex] for two ends of a line. They answer different questions.
  • Set a paragraph property back after the newline of the line it was set for.
  • Give an icon a Tag when one painter serves several rows and has to tell them apart.

Don't

  • Don't count on an unmatched tag failing loudly. An unknown [tag] and an {} with no argument left are both dropped in silence.
  • Don't mix argument order between the two syntaxes. There is one queue, and [icon {}] draws from it too.
  • Don't reach for [color #RRGGBB] in a control. A literal colour is right for art and wrong for interface.
  • Don't write a link target holding ] in a format string. The tag ends there; stream a PushLink instead.
All manuals Top