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.
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.
| Streamed | Effect |
|---|---|
std::wstring_view, const wchar_t*, wchar_t, int, size_t | Appends 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. |
InkGrade | Pushes the framework's own ink at that step - InkWell::textInk(grade), written as the step alone. |
InkColor | Pushes 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_endLine | Ends the paragraph. It is the character L'\n', streamed as any other wchar_t. |
TextOp::PushBold … PopItalic | Weight and slant. |
TextOp::PushSuperscript, PushSubscript, PopScript | Raises or lowers the runs that follow, taking the size down with them. |
TextAlign | Alignment 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:
| Construct | Consumes an argument | Meaning |
|---|---|---|
[tag] | No | Changes the stream state - a style, a colour, a space, an alignment. |
{}, {:.2f}, {:02X} | Yes | Formats the next argument through std::vformat, with the spec written inside the braces. |
[color {}] | Yes | The bridge between the two: takes a Color argument and pushes it. |
[icon {} 24,24] | Yes | Takes a paint callable and places it inline. |
\n | No | A 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
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.
| Streamed | Tag | Property |
|---|---|---|
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
| Tag | Style | Drawn as |
|---|---|---|
[title] | TextStyleId::Title | sans-serif 28, bold |
[subtitle] | SubTitle | sans-serif 20 |
[heading] | Heading | sans-serif 18, semibold |
[subheading] | SubHeading | sans-serif 16 |
[section] | Section | sans-serif 14, bold |
[subsection] | SubSection | sans-serif 14, italic |
[body] | Body | sans-serif 14 - the default |
[subbody] | SubBody | sans-serif 12 |
[code] | Code | monospace 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:
| Tag | Effect |
|---|---|
[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.
| Tag | Ink |
|---|---|
[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. |
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
| Tag | Streamed form | Effect |
|---|---|---|
[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:
| Written | Box |
|---|---|
[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.
Links and anchors
| Tag | Streamed form | Effect |
|---|---|---|
[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 } };
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" } };
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 } };
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.
Links and Anchors
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.
Writing a link
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{};
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.
Anchors
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.
Links in an edited text
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
Tagwhen 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 aPushLinkinstead.