Class TextLayout
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
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
public bool DidExceed { get; }
Property Value
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
Height
Gets the layout box's height: the bottom of the last line.
public float Height { get; }
Property Value
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
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
LastBaseline
Gets the last line's baseline; 0 when there are no lines.
public float LastBaseline { get; }
Property Value
Lines
Gets the lines, top to bottom.
public IReadOnlyList<TextLine> Lines { get; }
Property Value
LongestLine
Gets the widest line's width, without its hanging trailing spaces.
public float LongestLine { get; }
Property Value
MaxIntrinsicWidth
Gets the width of the widest line laid out without wrapping (CSS max-content).
public float MaxIntrinsicWidth { get; }
Property Value
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
Paragraph
Gets the paragraph that was laid out.
public Paragraph Paragraph { get; }
Property Value
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
Width
Gets the layout box's width: the width given, or LongestLine when it was unbounded.
public float Width { get; }
Property Value
Methods
GetCaret(TextPosition)
Gets where a caret at a position is drawn.
public Caret GetCaret(TextPosition position)
Parameters
positionTextPositionThe 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
indexintAn 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
indexis 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
yfloatA y in layout coordinates, or in vertical layouts an x.
Returns
GetLineBoundary(int)
Gets the line holding an index, as a range: its text, trailing spaces and hard break.
public TextSegment GetLineBoundary(int index)
Parameters
indexintA 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
indexis 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
startintThe range's first UTF-16 index.
endintThe index just past the range.
heightSelectionHeightWhether 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
indexintA 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
indexis 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
pointVector2A 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
positionTextPositionThe caret's position.
moveCaretMoveHow to move it.
preferredXfloatFor 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
sinkIGlyphOutlineSinkWhere 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
sinkis null.