GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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,
};