---
title: "Find caret positions and hit-test text"
url: https://recursiveai.org/onefont/cookbook/caret-positions/
description: "Compute caret positions for shaped text in C#, including inside ligatures, and map a click’s x coordinate back to an index in the string."
version: "1.3.0"
---

# Find caret positions and hit-test text

Compute caret positions for shaped text in C#, including inside ligatures, and map a click’s x coordinate back to an index in the string.

An editor or a text field needs to know where the caret goes between characters, and which index a click lands on. Shaped text makes both harder than adding up character widths: a ligature is one glyph for several letters, and a letter with a combining mark is one character to the user but two in the string. Shaped clusters and grapheme boundaries give the answer.

```csharp
using OneFont;

FontFace face = FontFace.Load(File.ReadAllBytes("NotoSerif-Regular.ttf"));
string text = "office e\u0301clair";            // "office éclair", the é typed as e + combining acute
float scale = 32f / face.UnitsPerEm;           // 32 px

IReadOnlyList<ShapedCluster> clusters = TextShaping.ShapeRun(text, face);
int[] graphemes = TextSegmentation.GetGraphemeBoundaries(text);

// A caret may sit at every grapheme boundary. Inside a cluster that holds several graphemes
// (the ffi ligature), share the cluster's advance between them.
var carets = new SortedDictionary<int, float>();
float pen = 0;
foreach (ShapedCluster c in clusters)
{
    int[] inside = graphemes.Where(b => b >= c.Start && b < c.Start + c.Length).ToArray();
    for (int k = 0; k < inside.Length; k++)
        carets[inside[k]] = pen + c.Advance * scale * k / inside.Length;
    pen += c.Advance * scale;
}
carets[text.Length] = pen;
Console.WriteLine(string.Join("  ", carets.Select(p => $"{p.Key}:{p.Value:F1}")));

// Hit test: the caret nearest to a click.
foreach (float x in new[] { 3f, 25f, 120f, 500f })
{
    int offset = carets.MinBy(p => Math.Abs(p.Value - x)).Key;
    Console.WriteLine($"click at x={x}: index {offset}  {text[..offset]}|{text[offset..]}");
}
```

```text
0:0.0  1:18.5  2:29.2  3:39.8  4:50.5  5:66.3  6:83.4  7:91.7  9:108.8  10:124.6  11:134.5  12:152.5  13:162.8  14:177.8
click at x=3: index 0  |office éclair
click at x=25: index 2  of|fice éclair
click at x=120: index 10  office éc|lair
click at x=500: index 14  office éclair|
```

- **Where a caret may sit.** At every grapheme boundary from `GetGraphemeBoundaries`, never inside one. Index 8, between “e” and its combining acute, isn’t a caret position, so the list jumps from 7 to 9.
- **Inside a ligature.** The ffi ligature is one cluster covering three graphemes. The sample splits its advance evenly, so the caret can stop between the f’s. A font can say where carets go inside a ligature (the `GDEF` ligature caret list), but onefont doesn’t read it. `GetTable("GDEF")` gives you the raw table if you need it.
- **Hit testing.** The nearest caret wins. A click before the start or past the end snaps to the first or last position.
- **Units.** Advances are in design units, scaled here to 32 px.

This covers one left-to-right run. For right-to-left runs, positions run from the right edge: place the clusters in display order first, as in [Right-to-left and mixed-direction text](https://recursiveai.org/onefont/cookbook/right-to-left-text/). For several lines, hit-test the line first by y, then within it.
