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.

Module imports: 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:

PropertyRead and written throughHolds
Text, a stringtext()The control's own text. Several in one argument list are appended in order.
TooltipTexttooltipText()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.
TextFormattextFormat(), setTextFormatWhat the text is laid out over - see Text Format.
WordWrapwordWrap(), setWordWrapWhether the text is broken to its box - see the Layout manual.
HorizontalTextAnchor, VerticalTextAnchortextAnchor(), setHorizontalTextAnchor, setVerticalTextAnchorWhere 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();
Write through 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:

EventOnRaised
OnGetText
GetTextEvent
Every controlWhen the control's text is gathered. A handler streams into event.text, after the control's own text.
OnGetChildText
GetChildTextEvent
A parentWhen a child's text is gathered. The child answers first, and the parent's handlers stream after it into the same text.
OnGetTooltip
GetTooltipEvent
Every controlWhen 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.
OnCaretMove
CaretMoveEvent
TextBoxThe caret came to rest somewhere new. Where it stands is asked of the box, through caretLineColumn().
OnTextEdit
TextEditEvent
TextBoxThe text was changed through the box - typed, deleted, pasted, cut, undone or redone. Raised last, once the box is done with the edit.
OnLinkClick, OnGetLinkTooltipTextBoxA 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

StreamedHolds
Ink, InkGrade, InkColor, Color, PushThemeColor{ ink }, PushCustomColor{ c }The colour the text runs in, spelt as the stream spells it.
TextStyleIdThe style.
PushFontSize{ f }, PushFontFamily{ L"..." }A size or a family over the style's.
TextOp::PushBold, TextOp::PushItalicWeight and slant.
TextAlign, ParaIndent{ f }, ParaLineSpacing{ f }Paragraph geometry, for every paragraph of the text.
TextFormatEach 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)... }
Never call 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.

PropertyValuesDefault
VerticalTextAnchorTop, Center, BottomTop
HorizontalTextAnchorNone, Left, Center, RightNone

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 textNoneLeftCenterRight
One line, aligned Rightat the right edgeat the left edge - the block is the linecentredat the right edge
Two lines, aligned Centereach centred in the boxcentred on one another, the block at the lefteach centred in the boxcentred 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.

Always pass the anchor. A layout is built at the origin of a box of the given size, and the anchor is the whole of what says where in the bounds that box was put. A call that leaves it out draws against a box the text does not occupy. Where the anchors put the text is Text Anchors.

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:

ModeFor
StaticText that cannot move. Everything that makes a glyph crisp, because nothing will ask it to be anywhere else.
MovableText 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.
MovingWhile 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>;
Using it is the declaration. A control whose text changes often derives from it, and a control whose text is written once and then stands does not. The base list says it once, and nothing else has to be kept in step.

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.

Override both or neither. A control that replaces one measures on one layout and draws on another.

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.

CallAnswers
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 }; };
A row is what the breaker made; a line is what the newlines cut. So a row moves with the width the text was broken at, and a paragraph broken across five rows carries one line number, the column running along the whole of it. Both are counted from one, which is what a reader counts from.

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.

It can decline, and the answer says so. A layout with nothing shaped yet, or an edit that does not fit the text it comes with. A caller that gets 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.

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.

BoxFollows a link onA plain click on a link
Read-onlyA clickFollows it.
One the user types intoCtrl+clickPlaces 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:

  1. #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.
  2. Any other target - OnLinkClick is raised with it. A handler that answers the target stops the event.
  3. Nothing stopped it - a target starting with http:, https: or mailto: 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.

The shell opens three schemes and no more. A text can come out of a file or off the clipboard and name anything, and the shell runs a program it is handed. A file: link, a path or a scheme of the application's own does nothing until a handler answers it.
// 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.

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:

TargetTooltip
#name, held by the box's textThe 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 targetThe 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";
    });
A tooltip breaks its lines at 480 design pixels, and its window is sized to the widest line it came to - so a short tooltip stays short, and a long one wraps rather than running out to the edge of the screen. That holds for every tooltip except one standing over a control's own text, which repeats the lines that control broke. A preview is cut at six lines of that width.

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 Text per 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 None for 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 OnLinkClick and stop the event. Of the rest, the box opens only http, https and mailto.

Don't

  • Don't hold a Text to 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 from text(). 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 setTextFormat in 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 WithTextLayout and 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.

One glyph per character. A paragraph on cells draws each character as its own glyph, so a font's ligatures are not applied to it. Consolas and DejaVu Sans Mono carry none; Cascadia Code and Fira Code do, and a text set in one of those shows its characters unjoined.

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.

All manuals Top