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"); +}