GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix: ASCII text in a monospaced face shapes Basic — no ligatures
cosmic-text's Advanced shaping applies the font's default-on OpenType
features, and in a monospaced face the ligatures among them break the
grid: Chivo Mono's liga substitutes f+i with a single-advance fi glyph,
so the bar's window titles drew two letters squeezed into one cell
("file" with a cramped fi). cosmic-text 0.12 exposes no per-feature
control, so the lever is the shaping mode.
shaping_for (exported via engine) picks Basic exactly when the text is
pure ASCII and the resolved face is monospaced per fontdb — mono faces
carry no kerning to lose, and ligatures are the only thing Advanced
adds them on ASCII. Proportional faces keep Advanced (kerning and
ligatures are wanted there — a font preview must not misrepresent the
face), and non-ASCII text keeps real shaping everywhere.
Both the measure path (StyledLabel) and the draw path (Prim::Text)
funnel through get_text_buffer_attrs, so widths and pixels stay in
agreement.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 50 +++++++++++++++++++++++++++++++++++++++++++-
src/engine.rs | 2 +-
2 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 9fc2b2d..f35670b 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -130,6 +130,53 @@ fn find_cased_family(fs: &FontSystem, name: &str) -> Option<String> {
None
}
+thread_local! {
+ /// Family name → is-monospaced, resolved once per family from fontdb's
+ /// face metadata (the post table's isFixedPitch, as fontdb records it).
+ static MONO_FAMILY_CACHE: std::cell::RefCell<std::collections::HashMap<String, bool>> =
+ std::cell::RefCell::new(std::collections::HashMap::new());
+}
+
+fn family_is_monospaced(fs: &FontSystem, name: &str) -> bool {
+ MONO_FAMILY_CACHE.with(|cache| {
+ if let Some(&mono) = cache.borrow().get(name) {
+ return mono;
+ }
+ let lower = name.to_lowercase();
+ let mono = fs
+ .db()
+ .faces()
+ .find(|face| face.families.iter().any(|(f, _)| f.to_lowercase() == lower))
+ .map(|face| face.monospaced)
+ .unwrap_or(false);
+ cache.borrow_mut().insert(name.to_string(), mono);
+ mono
+ })
+}
+
+/// The shaping mode for one text run: ASCII-only text in a MONOSPACED face
+/// shapes `Basic`, everything else `Advanced`.
+///
+/// `Basic` bypasses OpenType substitution and positioning, and for ASCII in a
+/// mono face that is exactly right: a mono font's ligatures are the one thing
+/// `Advanced` adds there, and they break the grid — Chivo Mono's `liga`
+/// squeezes f+i into a single-advance fi glyph, which is why the bar's window
+/// titles rendered "file" with a cramped fi — while mono faces carry no
+/// kerning to lose. Proportional faces keep `Advanced` (their kerning and
+/// ligatures are wanted — a font preview must not misrepresent the face), and
+/// any non-ASCII text keeps real shaping (combining marks, emoji, complex
+/// scripts) whatever the face.
+pub fn shaping_for(fs: &FontSystem, text: &str, family: &cosmic_text::Family) -> cosmic_text::Shaping {
+ if text.is_ascii() {
+ if let cosmic_text::Family::Name(name) = family {
+ if family_is_monospaced(fs, name) {
+ return cosmic_text::Shaping::Basic;
+ }
+ }
+ }
+ cosmic_text::Shaping::Advanced
+}
+
pub fn get_text_buffer(fs: &mut FontSystem, text: &str, size: f32, font: Option<&str>) -> Buffer {
get_text_buffer_attrs(fs, text, size, font, crate::scene::paint::TextAttrs::default())
}
@@ -259,7 +306,8 @@ pub fn get_text_buffer_attrs(
if let Some(w) = text_attrs.weight {
attrs = attrs.weight(cosmic_text::Weight(w));
}
- buf.set_text(fs, text, attrs, cosmic_text::Shaping::Advanced);
+ let shaping = shaping_for(fs, text, &family);
+ buf.set_text(fs, text, attrs, shaping);
buf.shape_until_scroll(fs, true);
BUFFER_CACHE.with(|cache| {
diff --git a/src/engine.rs b/src/engine.rs
index 2c5ecf5..106fe9d 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, shaped_cluster_offsets,
+ get_text_buffer_laid_out, shaped_cluster_offsets, shaping_for,
};