Working with Text in Controls
A layout somebody else already built
Introduction
A control keeps its text as a ControlText, and the TextEngine lays it
out: it shapes the text, measures it for the layout pass and draws it in the paint. This manual is
the control's side of that - the properties a control's text is stated through, how text goes into
a control and comes out of it, the events a control asks and answers, the anchors that place the
text inside the control's box, and the cache behind the engine.
What a Text says - its runs, styles, colours, paragraphs and links - is written as
the Writing ClaFi Text manual describes.
ClaFi.Core.TextEngine for the engine that draws and
measures, ClaFi.Core.TextEngine.Layout for TextLayout, and
ClaFi.Core.Foundation for the controls and WithTextLayout.
Text on a Control
The properties
Every control built on RichControl takes its text as properties, and each text has
a type of its own, so a control tells which one it was handed by the type alone:
| Property | Read and written through | Holds |
|---|---|---|
Text, a string | text() | The control's own text. Several in one argument list are appended in order. |
TooltipText | tooltipText() | What the control offers as its tooltip. |
PlaceHolderText | - | On a TextBox and a combo box item: what is drawn, muted, while the text is empty. The box is measured to fit it. |
TextFormat | textFormat(), setTextFormat | What the text is laid out over - see Text Format. |
WordWrap | wordWrap(), setWordWrap | Whether the text is broken to its box - see the Layout manual. |
HorizontalTextAnchor, VerticalTextAnchor | textAnchor(), setHorizontalTextAnchor, setVerticalTextAnchor | Where the text stands in its box - see Text Anchors. |
Label& m_status{ m_strip.createBody<Label>(
Text{ InkGrade::Muted, L"Ready" },
TooltipText{ L"What the page is showing" },
TextFormat{ TextStyleId::SubBody }
) };
Writing it later
text() hands the control's text out live, as a ControlText: a
Text that carries a stamp, which every write moves on. A layout or a measurement that
kept the stamp it was made from tells an unchanged text from a new one by two numbers, without
reading either. A write changes no pixel and no size by itself; the caller asks for the pass the
change needs.
// A text of another size: the form lays out again.
m_status.text() = std::move(status);
m_status.invalidateFormAlign();
// A box whose size is stated: a repaint is the whole of what a new reading costs.
m_readout.text() = std::move(reading);
m_readout.invalidate();
text(), never through a Text& taken from it.
A ControlText reached as a Text is data like any other, and a write through
that reference leaves the stamp where it was - a control that recognises its text by the stamp, one
that holds its own layout, goes on drawing the old words.
Copying and Moving Text
Values and ranges
A Text is a value. Assigning one copies it or moves it, markers and all, so a text
built for a control is moved in rather than copied, and a text taken out of a control is a copy that
owes the control nothing.
m_title.text() = Text{ TextStyleId::Heading, name }; // moved in
Text kept = m_title.text(); // a copy
selectedText(range) answers a range of a text as a text of its own. It states at its
start every run the range stood inside - the style, the colour, the paragraph's properties - copies
the range's own markers, and closes at its end whatever it left open, so it says on its own what it
said in place. replaceText(range, text) puts a text into another over a range, its
markers with it. The two are what the edit history is made of: a step keeps the text it took out,
and an undo puts it back.
The clipboard
A TextBox copies its selection as ClaFiText - the selected text with
its markers - and the clipboard offers plain text out of the conversion it keeps between the two, so
an application that has never heard of ClaFi pastes the characters. A paste takes
ClaFiText first and plain text after it, which is the whole of what decides whether
formatting survives a paste.
A copy carries the text alone. A control's TextFormat is how the control shows its
text rather than part of what the text says, so a range copied out of a CodeBox pastes
in whatever style the box it goes into lays its text out in.
Text Events
A control is asked for its text rather than read. Every measurement and every paint gathers it
through GetTextEvent, so a handler can write what a control shows without the control
holding it:
| Event | On | Raised |
|---|---|---|
OnGetTextGetTextEvent | Every control | When the control's text is gathered. A handler streams into event.text, after the control's own text. |
OnGetChildTextGetChildTextEvent | A parent | When a child's text is gathered. The child answers first, and the parent's handlers stream after it into the same text. |
OnGetTooltipGetTooltipEvent | Every control | When a tooltip is about to stand over the control. The TooltipText is written first and the handlers after it; where neither wrote anything and the control's own text is cut, the tooltip repeats that text over it. |
OnCaretMoveCaretMoveEvent | TextBox | The caret came to rest somewhere new. Where it stands is asked of the box, through caretLineColumn(). |
OnTextEditTextEditEvent | TextBox | The text was changed through the box - typed, deleted, pasted, cut, undone or redone. Raised last, once the box is done with the edit. |
OnLinkClick, OnGetLinkTooltip | TextBox | A link was followed, or its tooltip is about to show - see Links in a TextBox. |
A gather is asked in one of two phases, and event.phase() says which:
Calculate for a measurement, Paint for what is drawn. A control may be
measured from a line it never shows - a reading answers the room its widest value needs, and paints
the value it holds - and a text answered only in the paint takes no room at all:
OnEvent{ [](GetTextEvent& event) {
// Measured as nothing, so the tabs beside the title keep the room.
if (event.phase() == EventPhase::Paint)
event.text << event.control().appContext().appName();
} }
A control's own text is named by the gather rather than copied into it, so asking for a text nothing writes to costs a pointer. A handler that writes adds to it, and the answer is then built.
Text Format
A TextFormat is markers alone: what a control's text is laid out over. It states
the values a text runs on from - a colour, a style, an alignment - and holds no characters. The
engine reads it ahead of the text's own markers, and it never becomes part of the text.
// Built like a Text: the stream, or the same sequence as one expression.
TextFormat format{ TextAlign::Center, InkGrade::Muted };
format << TextStyleId::SubBody;
What it holds
| Streamed | Holds |
|---|---|
Ink, InkGrade, InkColor, Color, PushThemeColor{ ink }, PushCustomColor{ c } | The colour the text runs in, spelt as the stream spells it. |
TextStyleId | The style. |
PushFontSize{ f }, PushFontFamily{ L"..." } | A size or a family over the style's. |
TextOp::PushBold, TextOp::PushItalic | Weight and slant. |
TextAlign, ParaIndent{ f }, ParaLineSpacing{ f } | Paragraph geometry, for every paragraph of the text. |
TextFormat | Each value the other format holds. |
Anything that puts a character in - a string, an icon, a space, a tab stop - does not compile,
and neither does a pop, a link or an anchor. A value that only converts to one of the above is
refused as well, so a wchar_t is never taken for a Color. A script is not
held: how far one is raised depends on the size in force where it opens. A TextOp
other than PushBold and PushItalic is refused when it is streamed.
One of each kind. A value replaces what the format already holds of its kind, and kinds that differ combine. The values are kept in the order of their kinds rather than the order they were stated, so two formats that say the same thing are equal, and share a layout in the cache, however they were built. Bold and italic, once stated, stay stated.
TextFormat format{ TextAlign::Center, TextOp::PushBold };
format << TextFormat{ InkColor::Red, TextAlign::Left };
// Left, red and bold.
Under the text
The text's own markers stand over the format. A push the text makes is read after the format's. A paragraph property is read when the paragraph's newline is reached, so one the text states replaces the format's for the paragraph it stands in and for every paragraph after it.
The format is the floor of every stack. A pop the text holds no push for leaves what the format opened standing. For a text whose pushes and pops pair up, a format is the same as its values written at the start of the text.
A format is how a control shows its text, not part of what the text says. A selection, the clipboard, the edit history and a document see the text alone: a range copied out of a centred box pastes uncentred. A control with a format and no text has no text.
On a control
Every control built on RichControl takes a TextFormat property and
answers it from textFormat() - or none, while it is empty. Several in one argument list
compose kind by kind, a later one standing over an earlier one, and setTextFormat
replaces the whole of it. Stating one invalidates nothing; the caller asks for the pass.
A class states its format ahead of the arguments it forwards, the way it states any other
property. Along a chain of constructors the caller's value wins for every kind it states, then the
most derived class's, and so on down to the base. A CodeBox is laid out in the
monospace style that way, and the text handed to it states no style of its own:
TextBox{ params, WordWrap::No, TextFormat{ TextStyleId::Code }, std::forward<Args>(args)... }
setTextFormat in a constructor body. The body runs after the
caller's properties were bound, and the call replaces them.
The format reaches everything that lays the control's text out: the paint and the measurement, a
TextBox's placeholder, the tooltip that repeats a cut text over it
(GetTooltipEvent::format), and an in-place editor, which lays its own text out over the
covered control's (EditTarget::textFormat). A grid column states one for all of its
cells - see the Grid manual.
Text Anchors
A control lays its text out in a box, and two anchors say where in that box the text stands.
| Property | Values | Default |
|---|---|---|
VerticalTextAnchor | Top, Center, Bottom | Top |
HorizontalTextAnchor | None, Left, Center, Right | None |
VerticalTextAnchor moves the text down the box as a whole, by the height it
measured.
HorizontalTextAnchor::None makes no block. Each line is placed across the whole
width of the box by its paragraph's TextAlign, and nothing moves it after - which is
what a page, a grid cell and any text whose lines align to the edges of their box want.
Left, Center and Right make one: the text measures a block as
wide as its widest line, each line is aligned inside that block, and the anchor moves the block as a
whole. A wrapped text's block is never wider than the width it was broken at.
| The text | None | Left | Center | Right |
|---|---|---|---|---|
One line, aligned Right | at the right edge | at the left edge - the block is the line | centred | at the right edge |
Two lines, aligned Center | each centred in the box | centred on one another, the block at the left | each centred in the box | centred on one another, the block at the right |
The two compose. A document tile centres its block under its mark and every line of a name that wraps inside the block:
HorizontalTextAnchor::Center,
TextFormat{ TextAlign::Center },
A tooltip that repeats an anchored control's cut text repeats the block, starting where the
control put it. The engine is told the anchor with every drawText, and a control that
holds its own layout tells it to the layout with
setHorizontalAnchor.
Drawing and Measuring
The engine builds the layout. A control hands it a text, a rectangle, the colours it resolved, and where in that rectangle the text block belongs:
DrawTextResult drawText(ControlPaintContext&, const FloatRect&, const Text&,
TextAnchor, const EditProps* = nullptr, bool wrap = true,
const TextFormat* = nullptr);
ScaledSize calculateText(const FormContext&, const Text&,
MaxSize = { k_maxFloat, k_maxFloat }, bool editable = false, bool wrap = true,
const TextFormat* = nullptr);
The format is laid out under the text - see Text Format. None and an empty
one are the same answer, and isTextTrimmed takes one the same way.
Colours arrive with the draw. A run names inks, and the paint context the draw is handed is what resolves them - so the same layout draws correctly through whichever backend owns the form it appears in.
With CLAFI_TEXT_MOVING set to 1 in Source/Y-Core/System/Switches.h,
drawText and TextLayout::draw take a TextRenderMode after the
EditProps, and the backends rasterize each run the way it asks. The switch is 0, and
every run is drawn as Static. The mode is a question about movement, not about
quality:
| Mode | For |
|---|---|
Static | Text that cannot move. Everything that makes a glyph crisp, because nothing will ask it to be anywhere else. |
Movable | Text that can move, while it is not moving. It gives up the crispness permanently rather than at the moment movement starts - every setting that buys crispness ties the raster to sub-pixel position, so switching it around a movement makes the text visibly change at both ends, at rest. |
Moving | While the position is actually changing: outlines filled as geometry instead of a glyph rasterized afresh each frame. That is the expensive half, so it is not paid by a control that is merely capable of moving. |
Passing an EditProps makes the layout editable, which is a property of the text and
not a convenience: an editable text keeps the empty paragraph a trailing newline opens, because the
caret has to be able to stand on it. Measured as not editable, that row is missing from the size the
control is given.
Positions in a text are answered without a layout at all -
nextWord, prevWord, wordAt, paragraphAt. A
caret is measured on a TextLayout by whoever holds one, because an edited text is a
different text on every key, and routing that through the shared cache would store a copy per press.
A paragraph range includes the newline that ends it: a selection that leaves it behind cuts the text
and leaves an empty row where it was.
A text box holds the layout it draws from rather than taking one from the cache. Its paint, its caret and every position it measures run on that one layout, so a caret is never read off one build and drawn on another.
A control that holds its own layout
When a control needs one
WithTextLayout<HostClass> gives a control a TextLayout of its own
to shape in, instead of drawing through the engine's cache. It is spelt in the base list and
nowhere else:
class TextBox : public WithTextLayout<Label>
// A label that rewrites its words needs nothing of its own:
using CaretReadout = WithTextLayout<Label>;
The cache is addressed by what a text says, which is what lets a hundred buttons saying one word share a single shaping. It is also what a text rewritten every frame misses on every paint - the value under a slider's thumb, a frame rate, a caret position in the corner of a scroll box. Each new value takes a fresh entry and pushes out a layout that was being shared, so a drag of a few seconds empties the cache and leaves every label on the form to be shaped again.
A control here shapes its own text in place: no hash, no comparison against another control's words, nothing evicted, and nothing of its own in there to be evicted from. Editing is the second reason and the section above states it - an edited text is a different text on every key, and the paint and the caret have to read one build.
The two seams
Two virtuals on Control decide which layout an answer comes out of, and they are a
pair:
virtual DrawTextResult drawText(PaintEvent&, const FloatRect& textBounds, const Text&);
virtual ScaledSize measureText(AlignEvent&, ScaledSize asked, const Text&);
Everything around them stays with the framework. paintText keeps the rect, the
gather and the flags the result raises; calculateText keeps the gather, the rule that a
run which does not wrap is measured against no width at all, and the clamp to the box.
Bringing the layout up to date
Both seams, and everything else that asks the layout a question, go through one call - which is what keeps a position from being read off one build and drawn on another:
TextLayout& syncedLayout(const FormContext&, const FloatRect& textBounds) const;
TextLayout& syncedLayout(const FormContext&, const FloatRect& textBounds, const Text&) const;
virtual void takeText(const Text&) const;
The one-argument overload gathers a text itself, the way a paint does. Asking twice is free: a gather names a control's own text rather than copying it, so an answer that is that object is recognised by address, and an answer with no life of its own - one a parent composed, or a handler built - is compared instead. Either way an unchanged text rebuilds nothing, and a control measured four times in one pass shapes once.
Only the size of the rect is taken. Where it sits is the caller's business and the anchor's, so a control carried sideways under a viewport does not rebuild for every pixel it moves.
A layout held here stands outside the cache, so it answers invalidateLayouts()
for itself: it compares layoutGeneration() against the number it was built at, and
invalidates when the two have parted.
The control's format reaches the layout the same way, on every call.
TextLayout::setFormat keeps a copy and compares the one it is stated next, so an
unchanged format invalidates nothing, and a measurement kept from an earlier call is answered again
only while the format matches the one it was made over.
takeText states the whole of a text the layout has not been told about,
which is what a text rewritten from nothing needs. A host that knows what changed overrides it and
says so instead - see Editing a layout in place.
Positions on a held layout
Whoever holds a layout asks it where things are. The word-level questions above need no layout; these do, because the answers are about lines, and lines exist only once a text has been broken to a width.
| Call | Answers |
|---|---|
caretPos(FloatPoint) | The CaretHit a point names: an insertion position, and the affinity that says which of two rows a position on a break belongs to. |
getCaretRect(CaretHit) | Where that position stands. |
posOnNextRow(CaretHit, ScrollDirection, targetX) | The position one row up or down. targetX is the column a run of arrow keys is holding, so a short row passed through does not pull the caret in with it. |
rowStart(CaretHit), rowEnd(CaretHit) | The two ends of the row the position stands on - where Home and End go. |
lineColumnAt(size_t) | Which line the position is on and how far into it. |
struct TextLineColumn { size_t line{ 1 }; size_t column{ 1 }; };
lineColumnAt is a binary search over the paragraphs and reads no text, so a status
readout can ask it on every key press of a fifty-thousand-line document. The row calls walk the
paragraphs, which is the cost of an answer that depends on where the lines were broken.
All of these are in the layout's own coordinates - the box is built at the origin. A control
crosses anchoredOrigin in both directions: subtracting it from a point handed in,
adding it to a rect handed back.
Editing a layout in place
A layout is normally told a whole text. A caller that knows what changed can say so instead, and then a paragraph the edit did not reach keeps the shaping it already has:
struct TextEdit { TextRange replaced; size_t insertedLength; };
[[nodiscard]] bool applyTextEdit(const Text&, TextEdit);
[[nodiscard]] TextEdit mergedEdits(TextEdit first, TextEdit second);
The edit is stated in the coordinates of the text before it. What the layout does with it is find the paragraphs it reaches, re-shape those, and move the rest: their characters begin later or earlier, and their lines stand lower or higher. Typing a newline, deleting one and pasting a thousand lines are all the same operation - how many paragraphs the slice became is read off the rebuilt text rather than worked out from what the edit was.
false states the whole text with setText; the layout is already
invalidated for it.
One record carries one edit, and key repeat outruns the paint loop - so several edits can reach
the text before anything asks the layout about it. mergedEdits folds a second into a
standing record, answering the hull of the two. A hull can name text neither edit touched, which
costs a paragraph re-shaped that did not have to be, or a refusal - never a wrong answer.
A caller must also know that the record describes this text. What makes an edit and a text one story is that the lengths agree: the text the layout holds, plus what the edit inserted, less what it replaced. A control whose text arrives from its parent edits its own, and the two are not the same text.
Links in a TextBox
A link and an anchor are written as the Writing ClaFi Text manual
describes, and what a link does is up to the control holding the text: a
TextBox follows it, and a label draws it and nothing more. The control underlines the
link under the pointer when a click there would follow it.
Following a link in a TextBox
| Box | Follows a link on | A plain click on a link |
|---|---|---|
| Read-only | A click | Follows it. |
| One the user types into | Ctrl+click | Places the caret, as it does anywhere else in the text. |
The same rule decides when a link is underlined and shows the hand pointer, so what stands under the pointer always says what a click there will do. A press that ends in a selection was a drag, and follows nothing.
Where a followed link goes, in order:
#name, and the box's own text holds that anchor - the box goes to it. The anchor's text is selected and its line brought to the top of the view, the way a browser lands on a fragment. No event is raised.- Any other target -
OnLinkClickis raised with it. A handler that answers the target stops the event. - Nothing stopped it - a target starting with
http:,https:ormailto:is opened through the shell, whatever the case of its letters. Every other target goes nowhere.
box.onLinkClick([](LinkClickEvent& event) {
if (!event.target.starts_with(L"clafi:"))
return;
openPage(event.target.substr(6));
event.stopPropagation();
});
LinkClickEvent carries target, as the text states it, and
stamp, the press that followed the link. A #name the box's text does not
hold is raised like any other target, which is how a link reaches an anchor on another page: the
handler opens that page and calls goToAnchor on its box.
file: link, a
path or a scheme of the application's own does nothing until a handler answers it.
Back and forward
// false, and nothing moves, where the text holds no such anchor
bool goToAnchor(std::wstring_view name);
void goBack();
void goForward();
bool canGoBack() const;
bool canGoForward() const;
Every jump is remembered. Before it goes to an anchor the box keeps where it stood - the
selection, and the position the view showed at its top - and Alt+Left goes back to it,
Alt+Right forward again, the way a browser's history does. goToAnchor is
what records a place, whether a link called it or the application did. With nothing to go back or
forward to, the key is left unhandled and reaches whatever stands above the box.
An edit made through the box moves every remembered place with the text, so going back after typing lands where the user was. An undo or a redo drops the history. A text the host writes leaves it standing, and a place past the end of that text is held to its end when it is gone to.
Link tooltips
Every link has a tooltip, whether a click would follow it or not: the underline says a click would, and the tooltip says where it goes, which a reader wants in a box they type into as much as in one they read. It stands over the line the pointer is on, from where the link starts on it, so it covers neither the link nor the words around it. Where the screen has no room above the line, it stands under it. While the pointer is on a link, its tooltip stands in for the box's own.
What the box writes when nothing else does:
| Target | Tooltip |
|---|---|
#name, held by the box's text | The paragraph the anchor stands in, with its formatting - in the Ledger, the clue a reference names. A paragraph longer than six lines is cut at the end of the sixth and ends in an ellipsis, in the styles still open where it stands. |
| Any other target | The target as the text states it. |
In a box the user types into, a muted line follows: Ctrl+click to follow. A plain click there places the caret, and nothing else on screen says the link can be followed at all.
OnGetLinkTooltip is raised first. Its event carries target,
range - the text the link covers - and tooltip, the tooltip itself with
its placement and anchor already stated. What a handler writes into tooltip.text is the
whole of the tooltip, and the box then writes nothing.
box.onGetLinkTooltip([](GetLinkTooltipEvent& event) {
if (event.target.starts_with(L"clafi:"))
event.tooltip.text << L"Opens the " << event.target.substr(6) << L" page";
});
Links on a held layout
A control drawing links of its own - one that holds its own layout - asks the layout which link is under a point, and tells it which one to underline:
[[nodiscard]] std::optional<LinkHit> linkAt(FloatPoint);
void setHoveredLink(TextRange); // an empty range underlines nothing
TextRange hoveredLink() const;
struct LinkHit { LinkSpan link; size_t pos; }; // the link, and the character under the point
linkAt takes a point in the layout's own coordinates, the ones caretPos
takes. A hit test names the nearest character even past the end of a line, so the point is held to
the box that character's glyph stands in: past the end of a line, between two lines or below the
text, there is no link. pos is the character under the point, which says which line of
a wrapped link the pointer is on.
The hovered link is a draw-time question, the way colours are: stating one shapes nothing, and the caller repaints. A new text drops it, since the range was stated in the text it was found in.
LinkSpan is TextSpan<std::wstring_view> - the range a link covers
and its target - and BakedText::links() answers every one in text order. The target is
a view into the text the spans were baked from, and holds while that text stands unchanged.
Do and Don't
Do
- Build a
Textper paint. An identical text hits the layout cache, so rebuilding costs a hash and a compare. - Write a control's text through
text()and ask for the pass the change needs. - Pass the anchor that matches how the control positions its text.
- Leave the horizontal anchor at
Nonefor text whose lines align to the edges of their box. - State what every text of a control shares as its
TextFormat, ahead of the arguments a constructor forwards, rather than writing it into each text. - Answer your own link targets in
OnLinkClickand stop the event. Of the rest, the box opens only http, https and mailto.
Don't
- Don't hold a
Textto avoid rebuilding it unless it is expensive to assemble - the cache is behind the engine, not in your control. - Don't write through a
Text&taken fromtext(). The stamp stays where it was. - Don't measure editable text as static and then draw it editable. The two answer different heights.
- Don't call
setTextFormatin a constructor body. It replaces what the caller passed. - Don't put a text that is rewritten as the user watches through the cache. A control whose words change often derives from
WithTextLayoutand shapes them in place.
How It Works
Markers to spans
BakedText::rebuild walks the markers once and turns a stack-based description into
absolute ranges: ColorSpan, WeightSpan, StyleSpan,
SizeSpan, FamilySpan, ParagraphStyle, a LinkSpan
for every link, and a BakedInlineObject for every icon, flex space and tab stop. That is the form the native
layout is fed.
A colour span holds ColorDef, which is
std::variant<Ink, Color> - the name of a colour rather than a colour.
Colours are deliberately absent from what a layout is built from: they reach the native layout when
it is drawn, so a theme change re-draws the same shaping rather than paying for it again. Alignment
is absent for the same reason: where finished lines sit inside their box is set on a built layout,
not shaped from.
A format is walked first, at the start of the text. The depth of every stack is noted there and a pop stops at it, which is what keeps a format's values under the whole text.
The layout cache
The engine keeps up to 512 layouts, keyed on a text rather than on a box: the text's hash, the hash of the format it is laid out over, the scale factor, whether it is editable, and - for a text carrying a flex space - the event phase. A flex space is the one thing laid out differently for a measurement and for a paint, filling the box it is drawn in and contributing nothing to a size. Floats are held as their bits, because this is an identity question and not a numeric one.
A key names a bucket, and the bucket holds one layout per width the text has been broken at. Lines broken at one width came out no wider than the widest of them, and every width between the two breaks them the same way - none has to split, each already fitting, and none can join, because the breaker refused the next word at the wider of the two. So a layout is asked whether it accepts the width being requested, and the first that does answers it. That is what lets one build serve both passes: a control is sized to what it measured, so the paint asks at a width inside the interval of the build the measurement just made. A width outside every interval takes a layout of its own, up to four to a text, and the least recently used of them goes.
The box a layout stands in is not the width its lines were broken at, and moving lines into another width costs no shaping. A paragraph that came out as one line is placed by an offset the layout applies as it is drawn or hit; one of several lines is aligned by its native layout once, in the width it was broken at, and then moved as a whole. A wrapped text is never placed wider than it was broken at nor narrower than its widest line, so every line of it moves by the same amount. Justified lines are stretched to the width they stand in, and those alone are told a new one.
The height is in none of this. It decides which line the text is cut off at and which lines fade, and that is read off the finished lines - so a box of another height keeps the shaping and reads the fit again.
A hash is a way to find an entry, not proof it is the right one - two texts landing on one would
draw the wrong words - so a hit is confirmed by comparing the Text itself, and its
format against the copy the layout keeps. The cache owns the text's copy, because every caller's text is a temporary built for one paint while the layout keeps
a pointer to the one it was given.
Text::hash stands in for the whole Text, colour markers and all. A
colour is no part of a build, but it is part of what makes one Text a different
Text, so two runs naming different inks hold a cache entry each rather than sharing a
key and evicting one another on every alternation. For anything that changes what a build would
produce without being part of the key - the font table above all - invalidateLayouts()
drops the cache and moves layoutGeneration() on, and a layout held outside the cache
compares that number against the one it was built at.
Paragraphs on cells
A paragraph set in one monospace font is not shaped at all. Every glyph of such a font is one cell wide, so the x of a column is the column times the cell, a tab reaches the next multiple of four columns, the line is one row of the face's height, and a hit test is a division. The layout decides this per paragraph, before building anything: one font over the whole of it - a span of family, size, weight or style ending inside the paragraph makes a paragraph in two fonts - no inline object, no script, not justified, and every character either a tab or one the font has a glyph for at exactly the cell advance. A paragraph that qualifies is one row of columns times cell and holds no native layout. Anything else is shaped natively, that paragraph alone: one line of Arabic in a source file costs that line and no other.
What this buys is the shaping. A document of source is one pass over its characters instead of a
native layout per line, and what the layout keeps is a paragraph record per line rather than a
native object each - the lines of every paragraph live in one pool on the layout, so a document of
a hundred thousand rows allocates once for them. The pixels are the ones the native layout would
draw: DirectWrite's natural measuring mode already places a monospace run at column times advance,
the tab stops are the same four cells, and the glyphs reach the same draw primitive -
DrawGlyphRun on Direct2D, the glyph cache and the compositor on the CPU backends. The
cell and the line height are the platform's own measurement of the font, the same one the tab
stops are read from.
What a glyph table alone cannot lay out stays native, and the layout refuses it without asking the font: the controls and the format characters, combining marks, the right-to-left scripts, the scripts that reorder or stack, and anything past the basic plane. A wrapping layout keeps a paragraph on cells while its ink fits the width it breaks at - a line that fits is never broken - and hands one that does not to the native layout. Left, centre and right alignment are an offset inside the width the paragraph is placed in, applied as the paragraph is drawn or hit rather than told to a layout, so a box that moves costs nothing.
Nothing in the API says which paragraphs are on cells. [code] text, a
CodeBox, a HexView line and a label in a monospace family all take the
path when they qualify, and a caller reads the same positions and draws the same pixels either
way.
The link underline
The layout draws the underline of the hovered link: one segment per line the link reaches, under the glyphs that line shows, in the colour the segment's first glyph was drawn in. It stands below the baseline by a share of the room the line has there, in the thin stroke; the font's own underline metrics are not read. It is drawn over the glyphs, so a selection band does not cover it, and it does not fade with the glyphs of a line the box cuts.
Where it would cross ink, it is cut. The native layout answers where the link's glyphs put ink
between the line's top and bottom edges - INativeTextLayout::appendInkAcross, and
INativeMonoFont::inkAcross for a paragraph on cells - reading the same glyph coverage
the CPU path draws from, a row at a time, and counting a pixel as ink past a fifth covered. Each
stretch is widened by the line's thickness on both sides and cut out. A piece left no longer than
the line is thick is not drawn, since it would read as a dot between two descenders. On Direct2D the
glyphs of a hovered link are rasterized into the CPU glyph cache for the purpose.
Nothing is kept. The ink is asked for on every paint while a link is hovered, and that is a line or two of one link.