From f0335d2f179a5922c0313ea0f466b23d5753a532 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 17:12:12 -0800 Subject: [PATCH] Glyphs: a fallback face's consonant glyph is reported, not its vowel sign's When a fallback face draws Ka with the vowel sign I, the vowel sign's glyph comes first in Ka's cluster, and text.glyphAlternates reported it for Ka, laid out or overflowing. The query now finds the face that draws the character and takes that face's glyph for it by the same rule as for the requested face. --- crates/engine/src/cmd/textedit.rs | 37 +++++++++++++++++++---------- crates/engine/src/tests/textedit.rs | 34 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 9ba932edf..33fa1a514 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -1203,8 +1203,8 @@ 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). Also the first glyph drawn for the character, which can be in a fallback -/// face. +/// (overflowing text). Also the glyph drawn for the character in whatever face draws it +/// ([`drawn_glyph`]). 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(); @@ -1219,8 +1219,27 @@ 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); - 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) + let (alone, at) = (single.plain_text(), 0..bytes.len()); + (own_glyph(&alone, &lay, ch, &at, face).flatten(), drawn_glyph(&alone, &lay, ch, &at, face)) +} + +/// The glyph `lay` draws for the character `ch` at `bytes` of `plain` in whatever face draws it: +/// the glyph of `face` ([`own_glyph`]); else, when a fallback face draws the character, that face's +/// glyph for it by the same rule (its vowel sign drawn first is not reported); else the first glyph +/// drawn for the character. `None` when the character is not laid out. +fn drawn_glyph( + plain: &str, + lay: &vectorcraft_text::TextLayout, + ch: char, + bytes: &std::ops::Range, + face: &vectorcraft_text::FontFace, +) -> Option { + if let Some(own) = own_glyph(plain, lay, ch, bytes, face).flatten() { + return Some(own); + } + let first = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len))?; + let fallback = (first.font_id != face.id()).then(|| vectorcraft_text::FontDb::global().face_by_id(first.font_id)).flatten(); + fallback.and_then(|f| own_glyph(plain, lay, ch, bytes, &f).flatten()).or(Some(first.gid)) } fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result { @@ -1304,18 +1323,10 @@ fn glyph_alternates(s: &mut Session, p: &Value) -> Result { let (ch, bytes, face) = glyph_target(&t, p, C)?; let default = face.glyph_for(ch); 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; 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 => { - let (own, first) = shaped_alone(&t, ch, &bytes, &face); - own.or(first) - } - } - .unwrap_or(default); + let chosen = drawn_glyph(&t.plain_text(), &lay, ch, &bytes, &face).or_else(|| shaped_alone(&t, ch, &bytes, &face).1).unwrap_or(default); let alternates: Vec = face.alternates(ch).iter().map(|a| json!({"gid": a.gid, "feature": String::from_utf8_lossy(&a.feature)})).collect(); Ok(json!({"char": ch.to_string(), "font": face.family, "style": face.style, "default": default, "chosen": chosen, "alternates": alternates})) } diff --git a/crates/engine/src/tests/textedit.rs b/crates/engine/src/tests/textedit.rs index 045a88dee..9f607d78a 100644 --- a/crates/engine/src/tests/textedit.rs +++ b/crates/engine/src/tests/textedit.rs @@ -2800,3 +2800,37 @@ fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() { 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"); } + +/// A fallback face draws Ka with the vowel sign I in one cluster, the vowel sign first. The glyph +/// reported for Ka is the fallback face's glyph for Ka, laid out or overflowing, not the vowel +/// sign's. +#[test] +fn the_fallback_glyph_of_a_consonant_after_its_vowel_sign_is_reported() { + use vectorcraft_text::test_fonts::{MATRA_FAMILY, alternates_family, alternates_font, matra_font}; + let _fonts = crate::adding_fonts(); + let db = vectorcraft_text::FontDb::global(); + db.add_font(alternates_font(true).expect("the test font builds")); + db.add_font(matra_font().expect("the test font builds")); + let requested = db.face(alternates_family(true), "Regular").expect("the test face"); + let text = "क\u{93F}"; + assert_eq!(requested.glyph_for('क'), 0); + let mut s = session(); + let create = |s: &mut Session, height: f64| { + let spec = json!({"x": 0, "y": 0, "text": text, "font": alternates_family(true), "area": {"width": 200, "height": height}}); + NodeId(s.execute("text.create", &spec).unwrap()["id"].as_u64().unwrap()) + }; + let open = create(&mut s, 200.0); + let glyphs = vectorcraft_text::layout(db, &obj(&s, open)).glyphs; + let fallback = db.face_by_id(glyphs[0].font_id).expect("the fallback face"); + assert_ne!(fallback.id(), requested.id()); + let (ka, sign) = (fallback.glyph_for('क'), fallback.glyph_for('\u{93F}')); + let drawn: Vec<_> = glyphs.iter().map(|g| (g.gid, g.byte)).collect(); + assert_eq!(drawn, [(sign, 0), (ka, 0)], "the vowel sign is drawn first, in Ka's cluster ({} or {MATRA_FAMILY})", fallback.family); + let query = s.execute("text.glyphAlternates", &json!({"id": open.0, "start": 0})).unwrap(); + assert_eq!(query["chosen"], json!(ka), "the fallback face's glyph for Ka"); + + let boxed = create(&mut s, 2.0); + 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!(ka), "an overflowing Ka reports the fallback face's glyph for Ka"); +}