Merge pull request #5674 from dotCooCoo/fix/glyph-grapheme-probe

Glyphs: a character with a combining mark reports the glyph it is drawn with
This commit is contained in:
BFlat
2026-11-05 02:20:20 +07:00
committed by GitHub
2 changed files with 60 additions and 5 deletions
+22 -5
View File
@@ -1174,10 +1174,28 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
// The glyph the character is drawn with when it holds no choice, drawn alone in its face: a
// feature of its style (`salt`, a stylistic set) can draw another glyph than the default one.
// The default glyph is then kept as a choice, so that it is drawn. A character that is not laid
// out (overflowing text) is shaped alone in its style.
// out (overflowing text) is shaped alone in its style. A character that starts a longer
// grapheme (a letter and its combining marks) is drawn in the grapheme's cluster, and is shaped
// with the grapheme. Of a cluster of several glyphs (a mark, a vowel sign drawn before the
// letter), the character's glyph is its default glyph or one of its alternates.
let plain = t.plain_text();
let grapheme = plain.get(bytes.start..edit::next_cluster(&plain, bytes.start).max(bytes.end)).map_or_else(|| ch.to_string(), str::to_owned);
let listed = face.alternates(ch);
let own_glyph = |lay: &vectorcraft_text::TextLayout, at: usize| {
let cluster: Vec<u32> = lay
.glyphs
.iter()
.filter(|g| g.byte == at && [bytes.len(), grapheme.len()].contains(&g.len) && g.font_id == face.id())
.map(|g| g.gid)
.collect();
match cluster[..] {
[one] => Some(one),
_ => cluster.into_iter().find(|&g| g == default || listed.iter().any(|a| a.gid == g)),
}
};
let alone_in_face = |lay: &vectorcraft_text::TextLayout| {
let found = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len));
found.map(|g| (g.byte == bytes.start && g.len == bytes.len() && g.font_id == face.id()).then_some(g.gid))
found.map(|_| own_glyph(lay, bytes.start))
};
let unchosen = match holder.as_ref().filter(|_| !styled) {
None => None,
@@ -1187,9 +1205,8 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
let db = vectorcraft_text::FontDb::global();
alone_in_face(&vectorcraft_text::layout(db, &probe)).unwrap_or_else(|| {
let st = CharStyle { glyph_alternate: None, ..run.style.clone() };
let single = TextObject::point(Point::ZERO, &ch.to_string(), st);
let lay = vectorcraft_text::layout(db, &single);
lay.glyphs.iter().find(|g| g.byte == 0 && g.len == bytes.len() && g.font_id == face.id()).map(|g| g.gid)
let single = TextObject::point(Point::ZERO, &grapheme, st);
own_glyph(&vectorcraft_text::layout(db, &single), 0)
})
}
};
+38
View File
@@ -2498,6 +2498,44 @@ fn the_default_glyph_chosen_under_a_substituting_feature_is_drawn() {
assert_eq!(held, [g('a')], "the default glyph is kept as the choice");
}
/// A character followed by a combining mark is drawn in one cluster with the mark: with `salt`,
/// `gid: null` reports the glyph the feature draws, also when the text overflows, and choosing the
/// default glyph draws it.
#[test]
fn the_glyph_drawn_for_a_character_with_a_combining_mark_is_reported() {
use vectorcraft_text::test_fonts::{alternates_family, alternates_font};
let _fonts = crate::adding_fonts();
let db = vectorcraft_text::FontDb::global();
db.add_font(alternates_font(true).expect("the test font builds"));
let face = db.face(alternates_family(true), "Regular").expect("the test face");
let g = |c| face.glyph_for(c);
let mut s = session();
let text = "xa\u{331}y";
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": text, "size": 20, "font": alternates_family(true)})).unwrap();
let id = NodeId(r["id"].as_u64().unwrap());
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": text.len(), "features": ["salt"]})).unwrap();
let lay = vectorcraft_text::layout(db, &obj(&s, id));
let at = lay.glyphs.iter().find(|gl| gl.byte == 1).expect("the a is laid out");
assert_eq!((at.gid, at.len), (g('b'), 3), "salt draws the a as b, in one cluster with its mark");
let state = |s: &Session| (s.doc().unwrap().history.undo.len(), s.doc().unwrap().revision);
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 1, "gid": null})).unwrap();
assert_eq!(r["gid"], json!(g('b')), "the glyph salt draws");
let undo = state(&s).0;
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 1, "gid": g('a')})).unwrap();
assert_eq!(r["gid"], json!(g('a')));
assert_eq!(state(&s).0, undo + 1, "one undo step");
let lay = vectorcraft_text::layout(db, &obj(&s, id));
assert_eq!(lay.glyphs.iter().find(|gl| gl.byte == 1).map(|gl| gl.gid), Some(g('a')), "the a draws its default glyph");
let area = json!({"x": 0, "y": 300, "text": "xa\u{331}", "size": 20, "font": alternates_family(true), "area": {"width": 200, "height": 4}});
let boxed = NodeId(s.execute("text.create", &area).unwrap()["id"].as_u64().unwrap());
s.execute("text.setRangeStyle", &json!({"id": boxed.0, "start": 0, "end": 4, "features": ["salt"]})).unwrap();
assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows");
let r = s.execute("text.setGlyphAlternate", &json!({"id": boxed.0, "start": 1, "gid": null})).unwrap();
assert_eq!(r["gid"], json!(g('b')), "the overflowing a is shaped with its mark");
}
#[test]
fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() {
use vectorcraft_text::test_fonts::{alternates_family, alternates_font};