Glyphs: text.setGlyphAlternate with gid null reports the glyph drawn

With no glyph choice left on the character, text.setGlyphAlternate
returned the font's default glyph for it: 0 for a character the font
lacks, which a fallback font draws, and the lowercase glyph for a
letter under All Caps. It now reports the glyph the character is drawn
with, as text.glyphAlternates does.
This commit is contained in:
dotCooCoo
2026-11-11 01:05:16 -08:00
parent b37241f5ae
commit 1851f8769a
2 changed files with 57 additions and 7 deletions
+13 -7
View File
@@ -1276,19 +1276,25 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
// out (overflowing text) is shaped alone in its writing direction, with the rest of its grapheme
// in the grapheme's own styles.
let plain = t.plain_text();
let unchosen = holder.as_ref().filter(|_| !styled).and_then(|_| {
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).0)
});
let mut probe = t.clone();
edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None);
let probe_lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe);
let unchosen = holder
.as_ref()
.filter(|_| !styled)
.and_then(|_| own_glyph(&plain, &probe_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) {
None => None,
Some(g) if g == default || face.alternates(ch).iter().any(|a| a.gid == g) => Some(face.choice(ch, g)),
Some(g) => return Err(bad(C, format!("glyph {g} is not an alternate of {ch} in {} {}", face.family, face.style))),
};
let drawn = choice.as_ref().map_or(unchosen.unwrap_or(default), |c| c.gid);
// Without a choice, the glyph drawn in whatever face draws the character, as
// `text.glyphAlternates` reports it (a fallback face's glyph, the capital of All Caps).
let drawn = match &choice {
Some(c) => c.gid,
None => drawn_glyph(&plain, &probe_lay, ch, &bytes, &face).or_else(|| shaped_alone(&probe, ch, &bytes, &face).1).unwrap_or(default),
};
// The run holding the character. The request changes nothing when the choice is the one the
// character's run holds and that run holds the character alone; otherwise the edit splits the
// run, so that the following text does not take the character into a ligature.
+44
View File
@@ -3436,6 +3436,50 @@ fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() {
assert_eq!(query["chosen"], json!(glyph.gid), "an overflowing character reports the fallback glyph it is drawn with");
}
/// `text.setGlyphAlternate` with `gid: null` reports the glyph the character is drawn with, as
/// `text.glyphAlternates` does: the fallback face's glyph for a character the font lacks, laid out
/// or overflowing, and the capital that All Caps draws for an a.
#[test]
fn a_glyph_choice_taken_away_reports_the_glyph_drawn() {
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 mut s = session();
let reset = |s: &mut Session, id: NodeId, start: usize| {
s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": start, "gid": null})).unwrap()["gid"].clone()
};
let chosen = |s: &mut Session, id: NodeId, start: usize| {
s.execute("text.glyphAlternates", &json!({"id": id.0, "start": start})).unwrap()["chosen"].clone()
};
let ch = 'Ƃ';
assert_eq!(face.glyph_for(ch), 0);
let point = json!({"x": 0, "y": 0, "text": ch.to_string(), "font": alternates_family(true)});
let id = NodeId(s.execute("text.create", &point).unwrap()["id"].as_u64().unwrap());
let drawn = vectorcraft_text::layout(db, &obj(&s, id)).glyphs[0].gid;
assert_ne!(drawn, 0);
assert_eq!(reset(&mut s, id, 0), json!(drawn), "the fallback glyph");
assert_eq!(chosen(&mut s, id, 0), json!(drawn));
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");
assert_eq!(reset(&mut s, boxed, 0), json!(drawn), "the overflowing character's fallback glyph");
let caps = NodeId(
s.execute("text.create", &json!({"x": 100, "y": 100, "text": "xaay", "size": 20, "font": alternates_family(true)})).unwrap()["id"]
.as_u64()
.unwrap(),
);
s.execute("text.setRangeStyle", &json!({"id": caps.0, "start": 0, "end": 4, "allCaps": true})).unwrap();
let drawn = vectorcraft_text::layout(db, &obj(&s, caps)).glyphs.iter().find(|g| g.byte == 1).expect("the a is laid out").gid;
assert_ne!(drawn, face.glyph_for('a'), "All Caps draws the a as A");
assert_eq!(reset(&mut s, caps, 1), json!(drawn), "the capital drawn");
assert_eq!(chosen(&mut s, caps, 1), json!(drawn));
}
/// 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.