Skip to content

ui::TextLayout

Header: winui/render/TextLayout.hpp.

Paragraphs of differently styled runs broken into lines, with the geometry selection needs: the engine behind TextControl inlines, RichText and a multi-line TextBox.

Built only on TextMeasurer::measure(), so it lays out the same with every measurer. Lines break greedily after runs of spaces (a trailing space hangs), a word spanning runs stays whole, and under Wrap an overlong word is split between characters. Each piece between break opportunities is measured once and cached, so laying out again at another width measures nothing. All offsets address text(): every run's text in order, paragraphs joined by one \n. Mixed font sizes on one line share a bottom edge rather than a baseline.

Call order: set_content(), then layout(), then the queries and render().

Example:

ui::TextStyle body{.font_size = 14.0f, .horizontal = ui::TextAlign::Leading};
ui::TextStyle bold = body;
bold.weight           = ui::FontWeight::Bold;

ui::TextLayout layout; // a member of the element that shows the text
layout.set_content({
    {{{"Hello ", bold}, {"world, this wraps.", body}}},
    {{{"Second paragraph", body}}, ui::Edges{.top = 8}},
});

// In measure_override(), with the measurer it is handed:
ui::TextLayoutOptions options;
options.max_width  = 120.0f;
options.wrapping   = ui::TextWrapping::Wrap;
options.base_style = body;
layout.layout(options, measurer);

// In pointer_override():
const std::size_t caret = layout.hit_test({30.0f, 5.0f}); // click -> offset
const ui::Rect    bar   = layout.caret_rect(caret);       // where to draw the caret

// In render_override(), with the renderer it is handed:
ui::TextLayout::Paint paint;
paint.foreground = ui::colors::Black;
paint.selection  = ui::TextHighlight{0, 5, ui::colors::White, std::nullopt};
layout.render(renderer, {10.0f, 20.0f}, paint);

TextLayout

TextLayout() = default

Empty: no content, no lines.

TextLayout

TextLayout(const TextLayout& other)

A copy of the content and the last layout, indexing its own copy of the runs.

operator=

TextLayout& operator=(const TextLayout& other)

As the copy constructor.

TextLayout

TextLayout(TextLayout&&) noexcept = default

Moves; the runs stay where they are, so the index moves along.

operator=

TextLayout& operator=(TextLayout&&) noexcept = default

As the move constructor.

~TextLayout

~TextLayout() = default

set_content

bool set_content(std::vector<TextLayoutParagraph> paragraphs)

Replaces the content. Measurements are kept when only colors or tags changed, and everything is kept when nothing did. Returns whether the text or its formatting changed.

content

const std::vector<TextLayoutParagraph>& content() const

The paragraphs from the last set_content().

layout

void layout(const TextLayoutOptions& options, const TextMeasurer& measurer, std::size_t start = 0)

Lays the content out from start (an offset into text(); an overflow container's continuation), dropping whatever came before it. A start inside a paragraph drops that paragraph's top margin.

size

Size size() const

The widest line (with its paragraph's margins) and every line's height with the margins.

lines

const std::vector<Line>& lines() const

The lines of the last layout(), top to bottom.

fragments

const std::vector<Fragment>& fragments() const

Every drawn piece of the last layout(), grouped per line (see Line::fragment_begin).

text

const std::string& text() const

The whole content as one plain UTF-8 string: runs in order, paragraphs joined by \n. Every offset this class takes or returns indexes into it.

length

std::size_t length() const

Byte length of text().

run

const TextLayoutRun& run(std::size_t index) const

Run index of the paragraphs' runs, flattened in order (Fragment::run indexes this).

run_count

std::size_t run_count() const

Number of runs across all paragraphs.

start

std::size_t start() const

The start offset the last layout() was given; 0 unless this is an overflow continuation.

fitting_lines

std::size_t fitting_lines(float height) const

How many lines from the first fit whole in height: the lines a height-limited block shows before the rest overflows. May be 0.

height_of

float height_of(std::size_t count) const

The height the first count lines take, their paragraphs' margins included.

hit_test

std::size_t hit_test(Point p) const

The caret offset nearest p (layout coordinates): the line under it (the first above the text, the last below), then the nearer edge of the character under it.

caret_rect

Rect caret_rect(std::size_t offset) const

The caret at offset: a zero-width rect as tall as its line. An offset where a line wraps is drawn at the start of the next line.

range_rects

std::vector<Rect> range_rects(std::size_t begin, std::size_t end) const

The rects [begin, end) covers, one per line it touches.

tag_at

std::optional<std::size_t> tag_at(Point p) const

The tag of the run under p; unset when there is none or p is between runs.

line_of

std::size_t line_of(std::size_t offset) const

The index of the line holding offset (as caret_rect() places it).

offset_at_x

std::size_t offset_at_x(std::size_t line, float x) const

The offset on line line nearest the horizontal position x.

render

void render(Renderer& renderer, Point origin, const Paint& paint) const

Draws the laid-out lines with their top-left at origin: highlight and selection backgrounds first, then the text (one draw_text() per run per line, one per word under Justify), then the recolored highlighted/selected text clipped to its range.

struct Fragment

One drawn piece of a run on one line. Rect is relative to the layout's origin.

Field Type Default
rect Rect Bounds of the piece in layout coordinates (DIPs).
run std::size_t 0 index into runs() (the paragraphs' runs, flattened)
begin std::size_t 0 offsets into text()
end std::size_t 0 one past the piece's last byte in text()
text std::string the piece's own text, as drawn

struct Line

One laid-out line: its box, its range of text() and its range of fragments().

Field Type Default
top float 0.0f top edge in layout coordinates, margins included
height float 0.0f line box height in DIPs
left float 0.0f where the first fragment starts
width float 0.0f content width, a trailing space excluded
begin std::size_t 0 offsets into text(): the line's characters
end std::size_t 0 one past its last, a hard break's \n excluded
next std::size_t 0 where the next line starts
fragment_begin std::size_t 0 first index into fragments()
fragment_end std::size_t 0 one past the last index into fragments()
paragraph std::size_t 0 index into content()
hard_break bool false ended by \n or the paragraph's end

struct Paint

Per-draw state for render(): colors, the visible line window, highlights and selection.

Field Type Default
foreground Color Color of runs that don't set their own.
first_line std::size_t 0 Only lines [first_line, last_line) are drawn, shifted up so first_line's top sits at the origin; an unset last_line draws to the end. This is how a block shows its part before an overflow takes over.
last_line std::optional<std::size_t>
highlights std::span<const TextHighlight> Drawn under the selection, each background first, then its foreground over its text. A non-owning view; the highlights must outlive the render() call.
selection std::optional<TextHighlight> The selection, drawn last over the highlights; unset draws none.
colors std::span<const TextColorSpan> Colors for ranges of the text, sorted by begin and not overlapping: the runs' text is drawn in pieces at their edges. Cheaper than foreground highlights for many ranges. A non-owning view; the spans must outlive the render() call.
tag_color Callback<std::optional<Color>(std::size_t tag) const> Overrides a run's color by its tag (hyperlink hover and press); called for runs with a tag, and nullopt keeps the run's color.