|
| 1 | +# Selection and Bidi Drag |
| 2 | + |
| 3 | +Once you can hit-test a point and place a caret, the next step is painting selection ranges. Fonts returns selection geometry as a list of rectangles in visual order so editor-style UIs can paint browser-shaped selections without reimplementing bidi or line-box rules. |
| 4 | + |
| 5 | +For the underlying types — [`TextHit`](xref:SixLabors.Fonts.TextHit), [`CaretPosition`](xref:SixLabors.Fonts.CaretPosition), [`CaretPlacement`](xref:SixLabors.Fonts.CaretPlacement), and [`CaretMovement`](xref:SixLabors.Fonts.CaretMovement) — see [Hit Testing and Caret Movement](texthittesting.md). |
| 6 | + |
| 7 | +### The shape of a selection |
| 8 | + |
| 9 | +[`GetSelectionBounds(...)`](xref:SixLabors.Fonts.TextMetrics.GetSelectionBounds*) returns `ReadOnlyMemory<FontRectangle>`. Use `.Span` when drawing, and store the memory itself if the selection needs to be retained alongside other layout state. |
| 10 | + |
| 11 | +```csharp |
| 12 | +using System; |
| 13 | +using SixLabors.Fonts; |
| 14 | + |
| 15 | +ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(anchor, focus); |
| 16 | + |
| 17 | +foreach (FontRectangle rectangle in selection.Span) |
| 18 | +{ |
| 19 | + FillSelectionRectangle(rectangle); |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +A single logical selection can be visually discontinuous inside one line when it crosses bidi runs. Returning multiple rectangles allows browser-style selection where the unselected visual gap stays unpainted. |
| 24 | + |
| 25 | +Do not sort, union, or merge the returned rectangles unless the UI explicitly wants a different visual. |
| 26 | + |
| 27 | +### Pointer selection |
| 28 | + |
| 29 | +For pointer drags, hit-test both endpoints and pass the hits to the selection API. The [`TextHit`](xref:SixLabors.Fonts.TextHit) overload converts both endpoints to logical insertion indices for you. |
| 30 | + |
| 31 | +```csharp |
| 32 | +using System.Numerics; |
| 33 | +using SixLabors.Fonts; |
| 34 | + |
| 35 | +TextHit anchor = metrics.HitTest(new Vector2(downX, downY)); |
| 36 | +TextHit focus = metrics.HitTest(new Vector2(moveX, moveY)); |
| 37 | + |
| 38 | +ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(anchor, focus); |
| 39 | +``` |
| 40 | + |
| 41 | +This keeps trailing-edge and bidi handling inside the library. |
| 42 | + |
| 43 | +### Keyboard selection |
| 44 | + |
| 45 | +For keyboard selection, keep an anchor caret fixed and move the focus caret. Shift+Right-style behavior updates only the focus caret. |
| 46 | + |
| 47 | +```csharp |
| 48 | +using SixLabors.Fonts; |
| 49 | + |
| 50 | +CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); |
| 51 | +CaretPosition focus = anchor; |
| 52 | + |
| 53 | +focus = metrics.MoveCaret(focus, CaretMovement.Next); |
| 54 | + |
| 55 | +ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(anchor, focus); |
| 56 | +``` |
| 57 | + |
| 58 | +Selecting whole words via keyboard is the same shape: move the focus by `NextWord` or `PreviousWord`. |
| 59 | + |
| 60 | +### Word selection |
| 61 | + |
| 62 | +For double-click word selection, find the word containing the hit and ask for its selection bounds. |
| 63 | + |
| 64 | +```csharp |
| 65 | +using SixLabors.Fonts; |
| 66 | + |
| 67 | +TextHit hit = metrics.HitTest(doubleClickPosition); |
| 68 | +WordMetrics word = metrics.GetWordMetrics(hit); |
| 69 | + |
| 70 | +ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(word); |
| 71 | +``` |
| 72 | + |
| 73 | +The [`GraphemeMetrics`](xref:SixLabors.Fonts.GraphemeMetrics) overload selects exactly one grapheme, which is useful for caret-region overlays: |
| 74 | + |
| 75 | +```csharp |
| 76 | +using SixLabors.Fonts; |
| 77 | + |
| 78 | +GraphemeMetrics grapheme = metrics.GraphemeMetrics[index]; |
| 79 | +ReadOnlyMemory<FontRectangle> selection = metrics.GetSelectionBounds(grapheme); |
| 80 | +``` |
| 81 | + |
| 82 | +### Bidi drag selection |
| 83 | + |
| 84 | +Consider a left-to-right paragraph whose source text is: |
| 85 | + |
| 86 | +```text |
| 87 | +Tall שלום עرب |
| 88 | +``` |
| 89 | + |
| 90 | +The right-to-left run can paint with Arabic before Hebrew. When a user drags from the left edge of `Tall` toward the Hebrew word, the visual selection can become split: |
| 91 | + |
| 92 | +```text |
| 93 | +[Tall ] עرب [שלום] |
| 94 | +``` |
| 95 | + |
| 96 | +Application code should not manually decide which physical edge of the Hebrew glyph means "before" or "after". The hit-test result already carries the logical insertion index, and the selection result is already split into the visual rectangles that should be painted. |
| 97 | + |
| 98 | +```csharp |
| 99 | +using SixLabors.Fonts; |
| 100 | + |
| 101 | +TextHit anchor = metrics.HitTest(mouseDown); |
| 102 | +TextHit focus = metrics.HitTest(mouseMove); |
| 103 | + |
| 104 | +ReadOnlyMemory<FontRectangle> rectangles = metrics.GetSelectionBounds(anchor, focus); |
| 105 | +``` |
| 106 | + |
| 107 | +Just paint every rectangle. The library produces the correct visual gaps. |
| 108 | + |
| 109 | +### Hard line breaks |
| 110 | + |
| 111 | +Hard line breaks that end non-empty lines are trimmed with trailing breaking whitespace. Hard line breaks that own a blank line remain in the metrics and contribute their own selection rectangle so the blank line still highlights when the selection crosses it. |
| 112 | + |
| 113 | +For text with two hard breaks in the middle: |
| 114 | + |
| 115 | +```text |
| 116 | +Tall عرب שלום |
| 117 | +
|
| 118 | +Small مرحبا שלום |
| 119 | +``` |
| 120 | + |
| 121 | +A full selection paints three visual rows: the first text line, the blank line, and the second text line. The line break that ends a non-empty line does not add a separate painted box; the line break that owns the blank line does. Callers should not special-case this — paint the rectangles `GetSelectionBounds` returns. |
| 122 | + |
| 123 | +Consumers that inspect individual graphemes can use [`GraphemeMetrics.IsLineBreak`](xref:SixLabors.Fonts.GraphemeMetrics.IsLineBreak) to identify the blank-line hard breaks that remain in the metrics. |
| 124 | + |
| 125 | +In `TextInteractionMode.Editor`, a hard break that ends the text produces an additional blank line so a selection can extend past the final newline; `TextInteractionMode.Paragraph` omits that trailing blank line. See [Hit Testing and Caret Movement](texthittesting.md) for the full mode comparison. |
| 126 | + |
| 127 | +### Per-line selection |
| 128 | + |
| 129 | +[`LineLayout`](xref:SixLabors.Fonts.LineLayout) exposes the same selection overloads when the caller knows the selection is line-local: |
| 130 | + |
| 131 | +```csharp |
| 132 | +using SixLabors.Fonts; |
| 133 | + |
| 134 | +LineLayout line = layouts.Span[lineIndex]; |
| 135 | + |
| 136 | +ReadOnlyMemory<FontRectangle> selection = line.GetSelectionBounds(anchor, focus); |
| 137 | +ReadOnlyMemory<FontRectangle> wordSelection = line.GetSelectionBounds(word); |
| 138 | +``` |
| 139 | + |
| 140 | +Use the full [`TextMetrics`](xref:SixLabors.Fonts.TextMetrics) overloads for selections that can cross line boundaries; use [`LineLayout`](xref:SixLabors.Fonts.LineLayout) only when interaction is bounded to one line. |
| 141 | + |
| 142 | +### Stable line-box geometry |
| 143 | + |
| 144 | +Per-line selection uses the line-box height rather than per-glyph height, which matches normal text editor and browser behavior: selecting mixed font sizes on the same line paints a consistent line-height rectangle rather than one rectangle per glyph height. The selection geometry stays visually stable across mixed fonts and font sizes. |
| 145 | + |
| 146 | +For a wider tour of the measurement model and how line metrics are derived, see [Measuring Text](measuringtext.md). |
| 147 | + |
| 148 | +### Practical guidance |
| 149 | + |
| 150 | +- Paint the selection rectangles returned by the API instead of reconstructing selection geometry yourself. |
| 151 | +- Keep anchor and focus as logical text positions; let the metrics map them into visual rectangles. |
| 152 | +- Use editor interaction mode when selections must include terminal blank lines. |
| 153 | +- Test mixed LTR/RTL selections with real strings, not only simple Latin text. |
0 commit comments