From 8118b59d19f935b8d8309d34a82a314a63eef508 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 16:22:51 -0800 Subject: [PATCH] Glyphs: the Glyphs query reports the fallback glyph of an overflowing character When the requested font has no glyph for a character, the text draws it from a fallback face, and text.glyphAlternates reports that glyph. For a character that overflows its area, the query shaped the character alone but kept only a glyph of the requested face, so it reported the requested face's default glyph (0). It now takes the first glyph drawn for the character when the requested face draws none, as it does for laid-out text. --- crates/engine/src/cmd/textedit.rs | 18 ++++++++++++------ crates/engine/src/tests/textedit.rs | 6 ++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 4d9ffaef9..9ba932edf 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -1203,8 +1203,9 @@ fn own_glyph( /// The glyph of `face` drawn for the character `ch` at `bytes` of `t` when the character is shaped /// alone in the writing direction of `t` and the direction its paragraph sets it in, with the rest /// of its grapheme in the grapheme's own styles: the glyph of a character that is not laid out -/// (overflowing text). -fn shaped_alone(t: &TextObject, ch: char, bytes: &std::ops::Range, face: &vectorcraft_text::FontFace) -> Option { +/// (overflowing text). Also the first glyph drawn for the character, which can be in a fallback +/// face. +fn shaped_alone(t: &TextObject, ch: char, bytes: &std::ops::Range, face: &vectorcraft_text::FontFace) -> (Option, Option) { use vectorcraft_doc::ParaDirection; let plain = t.plain_text(); let end = edit::next_cluster(&plain, bytes.start).max(bytes.end); @@ -1218,7 +1219,8 @@ fn shaped_alone(t: &TextObject, ch: char, bytes: &std::ops::Range, face: let rtl = vectorcraft_text::char_is_rtl(plain.get(start..stop).unwrap_or_default(), t.para_at(paragraph).direction, bytes.start - start); single.para.direction = Some(if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight }); let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &single); - own_glyph(&single.plain_text(), &lay, ch, &(0..bytes.len()), face).flatten() + let first = lay.glyphs.iter().find(|g| g.byte == 0 && g.len > 0).map(|g| g.gid); + (own_glyph(&single.plain_text(), &lay, ch, &(0..bytes.len()), face).flatten(), first) } fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result { @@ -1253,7 +1255,7 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result { let mut probe = t.clone(); edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None); let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe); - own_glyph(&plain, &lay, ch, &bytes, &face).unwrap_or_else(|| shaped_alone(&probe, ch, &bytes, &face)) + own_glyph(&plain, &lay, ch, &bytes, &face).unwrap_or_else(|| shaped_alone(&probe, ch, &bytes, &face).0) }); let substituted = unchosen.is_some_and(|g| g != default); let choice = match gid.filter(|g| *g != default || substituted) { @@ -1304,10 +1306,14 @@ fn glyph_alternates(s: &mut Session, p: &Value) -> Result { let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); let first = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len)).map(|g| g.gid); // A character that is not laid out (overflowing text) reports the glyph it is drawn with when - // it is shaped alone, as `text.setGlyphAlternate` finds it. + // it is shaped alone, as `text.setGlyphAlternate` finds it; a fallback face's glyph when it is + // drawn in one. let chosen = match own_glyph(&t.plain_text(), &lay, ch, &bytes, &face) { Some(own) => own.or(first), - None => shaped_alone(&t, ch, &bytes, &face), + None => { + let (own, first) = shaped_alone(&t, ch, &bytes, &face); + own.or(first) + } } .unwrap_or(default); let alternates: Vec = face.alternates(ch).iter().map(|a| json!({"gid": a.gid, "feature": String::from_utf8_lossy(&a.feature)})).collect(); diff --git a/crates/engine/src/tests/textedit.rs b/crates/engine/src/tests/textedit.rs index a35ca13a8..045a88dee 100644 --- a/crates/engine/src/tests/textedit.rs +++ b/crates/engine/src/tests/textedit.rs @@ -2793,4 +2793,10 @@ fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() { let query = s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap(); assert_eq!(query["chosen"], json!(glyph.gid), "the query reports the visible fallback glyph"); assert!(query["alternates"].as_array().unwrap().is_empty()); + + let area = json!({"x": 0, "y": 300, "text": ch.to_string(), "font": alternates_family(true), "area": {"width": 200, "height": 2}}); + let boxed = NodeId(s.execute("text.create", &area).unwrap()["id"].as_u64().unwrap()); + assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows"); + let query = s.execute("text.glyphAlternates", &json!({"id": boxed.0, "start": 0})).unwrap(); + assert_eq!(query["chosen"], json!(glyph.gid), "an overflowing character reports the fallback glyph it is drawn with"); }