GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
text: flat-path widgets shape, so selection and caret stop drifting
A TextBox records its per-glyph x offsets in `prepare_text`, and its
selection highlight, its caret, and its click->index mapping all read them.
Nothing called it on the flat path: a host there consumes `all_quads`, and
only apps that shape for themselves (cce-files, the TreeList) ever passed a
FontSystem in. So all three fell back to `char_width`, which without a shaped
advance is `measure_text_width("M")` — an SVG-rasterized INKED extent, not an
advance. The two disagree by a fraction of a pixel per character and the error
accumulates: measured on the settings app's Homepage field, the highlight ran
8.09 px/char against the glyphs' real 8.45, ending a full glyph short of the
text it was supposed to cover.
`render_widget` now shapes each widget it lays out, through a FontSystem the
toolkit creates on FIRST USE (an app that shapes for itself never pays for
it). The shaped-buffer cache behind it is keyed by text/size/family and shared
per thread, so this reads the very buffers the draw already built rather than
re-shaping anything. `shaped_cluster_offsets` has documented this exact trap
for hand-rolled fields since it was written — the toolkit's own field was
falling into it wherever the host was flat.
Verified in a headless shadow session with CCE_FONTS_DIR set (bundled fonts
don't load there, and proportional fallback invalidates any metrics check).
Selecting all of "example.com": the highlight was 144..233 with glyph ink
running past it to 236; it is now 274..367 with the final glyph inside it and
the caret flush after it. Clicking mid-text at x=316 puts the caret at exactly
316 — the shaped boundary after "examp" — so click->index->caret round-trips
on the same advances the renderer draws with.
src/layout.rs | 13 +++++++++++++
src/lib.rs | 20 ++++++++++++++++++++
2 files changed, 33 insertions(+)
diff --git a/src/layout.rs b/src/layout.rs
index f54795c..74b6a4d 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3556,6 +3556,19 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
ctx.register_widget(w_id, w as *mut T as *mut (dyn WidgetHost + 'static));
}
w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
+
+ // Shape, which on this path nobody else does. A flat host consumes
+ // `all_quads`, so `prepare_text` — where a TextBox records the per-glyph x
+ // offsets its selection highlight, caret and click->index mapping all read
+ // — was never called for the widgets it draws. Those three then fell back
+ // to `measure_text_width("M")`, an SVG-rasterized INKED extent rather than
+ // an advance, so the highlight under-ran the glyphs by a few px per
+ // character (a full glyph by the end of "example.com"). Hosts that shape
+ // for themselves (cce-files, the TreeList) just re-read the shared buffer
+ // cache here.
+ if let Ok(mut fs) = crate::geometry_font_system().lock() {
+ w.prepare_text(&mut fs);
+ }
let (style_r, corners) = w.corner_style();
let r = if corners != (false, false, false, false) { style_r } else { 0.0 };
let (wx, mut wy, www, mut whh) = w.rect();
diff --git a/src/lib.rs b/src/lib.rs
index 04f4a8e..963675d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -125,6 +125,26 @@ pub fn create_font_system() -> cosmic_text::FontSystem {
build_font_system(false)
}
+/// A `FontSystem` the TOOLKIT owns, for widget GEOMETRY rather than drawing:
+/// the shaping a widget's own selection, caret and click-to-index math needs on
+/// a host that never hands one in.
+///
+/// Paint-walk apps shape through their own (`prepare_text`) and the display
+/// list shapes through the runner's — but a flat-path host consumes
+/// `all_quads`, so nothing ever shaped for the widgets it draws and `TextBox`
+/// fell back to `measure_text_width("M")`: an SVG-rasterized INKED extent, not
+/// an advance, which walks off the glyphs a few px per character.
+///
+/// Created on FIRST USE, so an app that shapes for itself never pays for it,
+/// and from the same bundle [`create_font_system`] gives the renderer. The
+/// shaped-buffer cache behind it is keyed by text/size/family and shared per
+/// thread, so in practice this reads the very buffers the draw already built.
+pub fn geometry_font_system() -> &'static std::sync::Mutex<cosmic_text::FontSystem> {
+ static GEOMETRY_FONT_SYSTEM: std::sync::OnceLock<std::sync::Mutex<cosmic_text::FontSystem>> =
+ std::sync::OnceLock::new();
+ GEOMETRY_FONT_SYSTEM.get_or_init(|| std::sync::Mutex::new(create_font_system()))
+}
+
/// Like [`create_font_system`] but always also loads installed system fonts, for
/// apps that must see every font on the system (e.g. the font picker) or want
/// them as fallbacks. Additive — bundled CCE fonts are still loaded.