Table of Contents

Class TextLayout

Namespace
Egami.Fonts.Layout
Assembly
Egami.Fonts.dll

A paragraph laid out at a width: its lines of glyph runs and the size of its layout box. Positions are in canvas units relative to the box's top-left corner, y down; nothing is rounded to pixels. Immutable and safe to share between threads.

public sealed class TextLayout
Inheritance
TextLayout
Inherited Members
Extension Methods

Examples

ParagraphStyle style = new() { DefaultStyle = new TextStyle { FontFamilies = [family], Size = 16 } };
Paragraph paragraph = Paragraph.Create("A paragraph is shaped once and laid out at any width.", style);

// Each width gives an immutable layout: lines of glyph runs, positioned from the box's top-left corner.
TextLayout narrow = paragraph.Layout(120);
TextLayout wide = paragraph.Layout(400);
foreach (TextLine line in narrow.Lines)
{
    string text = paragraph.Text[line.Start..line.End];   // the line's text, its trailing space included
    float baseline = line.Baseline;                        // y of the baseline
    foreach (LayoutRun run in line.Runs)
    {
        GlyphRun? glyphs = run.Glyphs;                     // glyphs at their layout positions
    }
}

// No wrapping: the box is as wide as the longest line.
TextLayout unbounded = paragraph.Layout(float.PositiveInfinity);

Remarks

In vertical layouts (WritingMode) the width given is the lines' length, which runs down the box: Height is that length and Width the lines' thicknesses added up. Positions along a line (a line's Left and Width, a run's X, a caret's X) are then y, and positions across the lines (a line's Top, Baseline and Bottom) are x; glyph runs, ink, placeholder and selection rectangles stay in x and y.

Properties

Decorations

Gets the decoration lines, line by line: underlines and overlines (with skip-ink gaps cut) and line-throughs, each over a stretch of text with one decoration. Vertical layouts have none yet.

public IReadOnlyList<DecorationShape> Decorations { get; }

Property Value

IReadOnlyList<DecorationShape>

Examples

TextStyle underlined = new()
{
    FontFamilies = [family],
    Decoration = new TextDecoration { Lines = TextDecorationLines.Underline, Style = TextDecorationStyle.Wavy },
};
TextLayout layout = Paragraph.Create("Typography, quickly", new ParagraphStyle { DefaultStyle = underlined }).Layout(400);

foreach (DecorationShape line in layout.Decorations)
{
    // Top and Thickness place the line; Segments skip the descenders of y, p, g and q.
    System.Console.WriteLine($"{line.Line} at {line.Top}: {line.Segments.Count} pieces");
}

// Or everything as outlines, to fill with any path renderer.
PathRecorder path = new();
layout.ToPath(path);

DidExceed

Gets whether MaxLines or MaxHeight cut text off.

public bool DidExceed { get; }

Property Value

bool

FirstBaseline

Gets the first line's baseline, for aligning the box on it; 0 when there are no lines.

public float FirstBaseline { get; }

Property Value

float

Height

Gets the layout box's height: the bottom of the last line.

public float Height { get; }

Property Value

float

HiddenRange

Gets the text a line limit hid, the ellipsis's room included, or null when nothing was cut.

public TextSegment? HiddenRange { get; }

Property Value

TextSegment?

InkBounds

Gets the union of the glyphs' ink and the placeholders' boxes, which may reach past the layout box.

public TextBounds InkBounds { get; }

Property Value

TextBounds

LastBaseline

Gets the last line's baseline; 0 when there are no lines.

public float LastBaseline { get; }

Property Value

float

Lines

Gets the lines, top to bottom.

public IReadOnlyList<TextLine> Lines { get; }

Property Value

IReadOnlyList<TextLine>

LongestLine

Gets the widest line's width, without its hanging trailing spaces.

public float LongestLine { get; }

Property Value

float

MaxIntrinsicWidth

Gets the width of the widest line laid out without wrapping (CSS max-content).

public float MaxIntrinsicWidth { get; }

Property Value

float

MinIntrinsicWidth

Gets the width of the widest piece that cannot break (CSS min-content): the narrowest box the text fits without overflowing. Anywhere lowers it to the widest grapheme cluster.

public float MinIntrinsicWidth { get; }

Property Value

float

Paragraph

Gets the paragraph that was laid out.

public Paragraph Paragraph { get; }

Property Value

Paragraph

PlaceholderRects

Gets one box per placeholder of the rich text, in order; an empty box for one hidden by a line limit.

public IReadOnlyList<TextBounds> PlaceholderRects { get; }

Property Value

IReadOnlyList<TextBounds>

Width

Gets the layout box's width: the width given, or LongestLine when it was unbounded.

public float Width { get; }

Property Value

float

Methods

GetCaret(TextPosition)

Gets where a caret at a position is drawn.

public Caret GetCaret(TextPosition position)

Parameters

position TextPosition

The position; an index inside a grapheme cluster moves to the cluster's start.

Returns

Caret

The caret: its primary segment for the position's affinity, and a secondary one at a bidi boundary.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

Exceptions

ArgumentOutOfRangeException

The index is negative or past the end of the text.

GetLineAt(int)

Finds the line holding a UTF-16 index.

public int GetLineAt(int index)

Parameters

index int

An index from 0 to the text's length; the end of the text belongs to the last line.

Returns

int

The line's index in Lines, or -1 when the index is hidden by a line limit or there are no lines.

Exceptions

ArgumentOutOfRangeException

index is negative or past the end of the text.

GetLineAtY(float)

Finds the line at a position across the lines; points outside the lines take the nearest.

public int GetLineAtY(float y)

Parameters

y float

A y in layout coordinates, or in vertical layouts an x.

Returns

int

The line's index in Lines, or -1 when there are no lines.

GetLineBoundary(int)

Gets the line holding an index, as a range: its text, trailing spaces and hard break.

public TextSegment GetLineBoundary(int index)

Parameters

index int

A UTF-16 index; the end of the text belongs to the last line.

Returns

TextSegment

The line's range, or an empty one at 0 when there are no lines.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

Exceptions

ArgumentOutOfRangeException

index is negative or past the end of the text.

GetSelectionRects(int, int, SelectionHeight)

Gets the rectangles that cover a range of text: one per run of visually adjacent clusters on each line.

public IReadOnlyList<TextBounds> GetSelectionRects(int start, int end, SelectionHeight height = SelectionHeight.Line)

Parameters

start int

The range's first UTF-16 index.

end int

The index just past the range.

height SelectionHeight

Whether the rectangles cover the line boxes or the fonts' ascent and descent.

Returns

IReadOnlyList<TextBounds>

The rectangles, line by line, left to right.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

Exceptions

ArgumentOutOfRangeException

The range is not inside the text.

GetWordBoundary(int)

Gets the word (UAX #29 segment) holding an index: a word, or the spaces or punctuation between words.

public TextSegment GetWordBoundary(int index)

Parameters

index int

A UTF-16 index; the end of the text gives the last segment.

Returns

TextSegment

The segment.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

Exceptions

ArgumentOutOfRangeException

index is negative or past the end of the text.

HitTest(Vector2)

Finds the caret position nearest a point; points outside the lines go to the nearest line, and points left or right of a line to its visual ends.

public TextPosition HitTest(Vector2 point)

Parameters

point Vector2

A point in layout coordinates.

Returns

TextPosition

The position, at a grapheme cluster boundary.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

MoveCaret(TextPosition, CaretMove, float)

Moves a caret.

public TextPosition MoveCaret(TextPosition position, CaretMove move, float preferredX = NaN)

Parameters

position TextPosition

The caret's position.

move CaretMove

How to move it.

preferredX float

For LineUp and LineDown, the x to keep across lines of different lengths; NaN for the caret's own.

Returns

TextPosition

The new position; at the ends of the text, the end itself.

Examples

// A click: the nearest caret position, then where to draw the caret.
TextPosition position = layout.HitTest(new System.Numerics.Vector2(40, 30));
Caret caret = layout.GetCaret(position);
CaretLine bar = caret.Primary;                     // x, top and bottom; Secondary at a bidi boundary

// Arrow keys: logical moves, and line moves that keep the column.
TextPosition right = layout.MoveCaret(position, CaretMove.NextGrapheme);
TextPosition down = layout.MoveCaret(position, CaretMove.LineDown, preferredX: bar.X);

// A selection: rectangles to fill, one per visually contiguous piece on each line.
IReadOnlyList<TextBounds> highlight = layout.GetSelectionRects(position.Index, down.Index);
Egami.Fonts.Unicode.TextSegment word = layout.GetWordBoundary(position.Index); // double-click

Exceptions

ArgumentOutOfRangeException

The index is negative or past the end of the text, or the move is not defined.

ToPath(IGlyphOutlineSink)

Writes the whole layout as outlines: every glyph (color glyphs as their outlines) at its place, then the decorations.

public void ToPath(IGlyphOutlineSink sink)

Parameters

sink IGlyphOutlineSink

Where the outlines go, in layout coordinates with y down.

Examples

TextStyle underlined = new()
{
    FontFamilies = [family],
    Decoration = new TextDecoration { Lines = TextDecorationLines.Underline, Style = TextDecorationStyle.Wavy },
};
TextLayout layout = Paragraph.Create("Typography, quickly", new ParagraphStyle { DefaultStyle = underlined }).Layout(400);

foreach (DecorationShape line in layout.Decorations)
{
    // Top and Thickness place the line; Segments skip the descenders of y, p, g and q.
    System.Console.WriteLine($"{line.Line} at {line.Top}: {line.Segments.Count} pieces");
}

// Or everything as outlines, to fill with any path renderer.
PathRecorder path = new();
layout.ToPath(path);

Exceptions

ArgumentNullException

sink is null.