From 9d32bc4da9cb222fabdf53cc8652000c1f56ac98 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:17:11 -0500 Subject: [PATCH] fix(text): keep private-use bullets in named Unicode symbol fonts; .notdef falls back to a dot Private-use codes are mapped by code only when no bullet font is named or it is a legacy symbol font. A bullet that the cascaded face still cannot draw uses the dot instead of .notdef. --- crates/text/src/shape.rs | 4 ++++ crates/text/src/symbol.rs | 4 ++-- crates/text/src/tests.rs | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index c759aa4b..869678f2 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -91,6 +91,10 @@ pub(crate) fn shape_para( None => map_symbol_bullet(char, props.bullet_font.as_deref()), }; bullet_face(db, &mut st, &ch); + // A face the cascade still picks may lack the bullet: draw the dot rather than .notdef. + let chars: Vec = ch.chars().collect(); + let ch = if deckcraft_fonts::covers_cluster(&st.face, &chars) { ch } else { "•".into() }; + bullet_face(db, &mut st, &ch); Some((ch, st)) } Some(Bullet::AutoNum { scheme, start_at }) => { diff --git a/crates/text/src/symbol.rs b/crates/text/src/symbol.rs index 8811cdda..8c6b94e2 100644 --- a/crates/text/src/symbol.rs +++ b/crates/text/src/symbol.rs @@ -276,8 +276,8 @@ pub fn map_symbol_bullet(c: &str, font: Option<&str>) -> String { return m.to_string(); } // Only the legacy symbol fonts: "Noto Sans Symbols" and the like are Unicode fonts whose - // characters (❑ ❖) are drawn as they are. - if f.contains("wingdings") || f == "symbol" || ('\u{f000}'..='\u{f0ff}').contains(&first) { + // characters (❑ ❖) are drawn as they are. Private-use codes are mapped only when no font is named. + if f.contains("wingdings") || f == "symbol" || (font.is_none() && ('\u{f000}'..='\u{f0ff}').contains(&first)) { let code = (first as u32) & 0xff; // Wingdings 2 and 3 put different shapes at the same codes. if f.starts_with("wingdings 2") { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 07dbc7b0..472f800a 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -180,7 +180,7 @@ fn only_the_first_character_of_a_bullet_is_drawn() { #[test] fn bullet_in_an_unavailable_unicode_symbol_font_keeps_its_character() { // Google Slides bullets in "Noto Sans Symbols" (not installed) draw ❑ ❖, not a generic dot (#1347). - for c in ["\u{2751}", "\u{2756}"] { + for c in ["\u{2751}", "\u{2756}", "\u{f0a7}"] { assert_eq!(map_symbol_bullet(c, Some("Noto Sans Symbols")), c); } // The legacy symbol fonts still map their codes. @@ -192,6 +192,7 @@ fn bullet_in_an_unavailable_unicode_symbol_font_keeps_its_character() { let l = lay(&p, &sh, 600.0, 300.0); let bullet = l.runs.iter().find(|r| r.chars == (0, 0)).expect("bullet run"); assert_eq!(bullet.glyphs.len(), 1); + assert_ne!(bullet.glyphs[0].0, 0, "the bullet is drawn, not .notdef"); } #[test]