git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit7ca9b3343e636d14a01d143e4683dbd07db051f0
parent42901a7d1f
authorLucas Galante <[email protected]>
date2026-08-19 15:56
feat: shaped_cluster_offsets — renderer-exact text metrics for hand-rolled fields

Byte-offset -> x mapping of a single-line string, read from the same
shaped buffer (and buffer cache) the renderer draws with. This is the
correct source for caret placement and click->cursor mapping in
app-side text fields: measure_text_width rasterizes an SVG and reports
inked extent through fontdb's family resolution, which disagrees with
cosmic-text's advance and can resolve a different face outright — the
same divergence just fixed inside TextBox (42901a7), now avoidable by
apps that roll their own field (first consumer: cce-browser's URL bar).

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/backend/window_runner.rs | 30 ++++++++++++++++++++++++++++++
 src/engine.rs                |  2 +-
 2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index a463401..601be3a 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -224,6 +224,36 @@ pub fn get_text_buffer_attrs(
     buf
 }
 
+/// Byte-offset → x mapping of single-line `text`, shaped exactly as the renderer draws it —
+/// same buffer cache as the draw, so this is a lookup when the text is already on screen.
+/// Returns ascending `(byte_idx, x)` pairs (one per cluster start, logical px, relative to
+/// the text origin), terminated by `(text.len(), total_advance)`. This is the correct
+/// source for caret placement and click→cursor mapping in hand-rolled text fields:
+/// `measure_text_width` reports SVG-rasterized inked extent through fontdb's family
+/// resolution, which disagrees with cosmic-text's advance and can even resolve a
+/// different face — a caret placed with it drifts off the drawn glyphs.
+pub fn shaped_cluster_offsets(
+    fs: &mut FontSystem,
+    text: &str,
+    size: f32,
+    font: Option<&str>,
+) -> Vec<(usize, f32)> {
+    let scale = crate::scale::scale_factor().max(1.0);
+    let buffer = get_text_buffer(fs, text, size, font);
+    let mut out: Vec<(usize, f32)> = Vec::new();
+    let mut total: f32 = 0.0;
+    if let Some(run) = buffer.layout_runs().next() {
+        for glyph in run.glyphs {
+            if out.last().map_or(true, |&(b, _)| b != glyph.start) {
+                out.push((glyph.start, glyph.x / scale));
+            }
+            total = total.max((glyph.x + glyph.w) / scale);
+        }
+    }
+    out.push((text.len(), total));
+    out
+}
+
 /// Shape a boxed [`Prim::Text`] (word-wrap + alignment) and return `(buffer, vertical_offset)`.
 /// Reuses [`get_text_buffer_attrs`] for all the family resolution — that returns a *clone* of the
 /// cached single-run buffer, so re-applying metrics/size/align here does not touch the cache — then
diff --git a/src/engine.rs b/src/engine.rs
index 7b207e6..2c5ecf5 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -10,5 +10,5 @@ pub use crate::backend::window_runner::{
     extra_quad_vertices, push_extra_quad_vertices, extra_quad_vertices_clipped,
     push_extra_quad_vertices_clipped, circle_vertices, circle_border_vertices,
     arc_background_vertices, push_arc_background_vertices, push_plate_solid_border_vertices,
-    get_text_buffer_laid_out,
+    get_text_buffer_laid_out, shaped_cluster_offsets,
 };