diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index bb98a58..93c52e4 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -492,9 +492,7 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, st.baseline = 0.0; // Symbol fonts (Wingdings) aren't available: map common private-use bullets. let ch = map_symbol_bullet(char, props.bullet_font.as_deref()); - if !st.face.covers(ch.chars().next().unwrap_or('•')) { - st.face = db.face("Inter", "Regular"); - } + bullet_face(db, &mut st, &ch); Some((ch, st)) } Some(Bullet::AutoNum { scheme, start_at }) => { @@ -504,8 +502,10 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, } rp.size = Some(lead.size.unwrap_or(18.0) * props.bullet_size.unwrap_or(1.0)); rp.underline = None; - let st = make_style(ctx, &rp, scale, opts.prompt_color); - Some((autonum(scheme, start_at.saturating_sub(1).saturating_add(number.unwrap_or(1))), st)) + let mut st = make_style(ctx, &rp, scale, opts.prompt_color); + let label = autonum(scheme, start_at.saturating_sub(1).saturating_add(number.unwrap_or(1))); + bullet_face(db, &mut st, &label); + Some((label, st)) } _ => None, } @@ -513,6 +513,15 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, ParaShaped { cells, styles, faces, props, lead_size, lead_face, bullet, level: para.level, rtl } } +/// A face that draws the bullet `label` when the bullet's own can't (circled numbers ①, arrows +/// and other symbols missing from the text font), by the usual fallback. +fn bullet_face(db: &FontDb, st: &mut Style, label: &str) { + let chars: Vec = label.chars().collect(); + if !chars.is_empty() && !deckcraft_fonts::covers_cluster(&st.face, &chars) { + st.face = db.cascade(&chars, std::slice::from_ref(&st.face), st.style_name); + } +} + /// Shape a logical paragraph into cells: bidi levels, per-grapheme faces, items of one level, /// script, face and shaping style, each shaped with the whole paragraph as context. /// A run's outer shadow and glow (inner shadows aren't drawn on text), at the layout's `scale`. diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index cd78b90..1b9a58c 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -602,3 +602,25 @@ fn emoji_take_a_colour_face_when_one_is_installed() { assert!(l.runs.iter().filter(|r| r.cells.iter().all(|(_, c)| c.is_ascii())).all(|r| !r.face.is_color()), "the letters keep a text face"); } } + +#[test] +fn a_bullet_the_text_font_lacks_falls_back_to_a_face_that_has_it() { + // circleNumDbPlain numbers ①②…: the text font usually has no circled digits; PowerPoint draws + // them from a fallback font instead of a missing-glyph box. Skipped when no installed or + // bundled face has ①. + let db = FontDb::global(); + if !db.cascade(&['①'], &[], "Regular").covers('①') { + return; + } + let (p, mut sh) = setup("One\nTwo", 600.0, 300.0); + for para in &mut sh.text.as_mut().unwrap().paragraphs { + para.props.bullet = Some(Bullet::AutoNum { scheme: "circleNumDbPlain".into(), start_at: 1 }); + } + let l = lay(&p, &sh, 600.0, 300.0); + let bullets: Vec<_> = l.runs.iter().filter(|r| r.cells.is_empty()).collect(); + assert_eq!(bullets.len(), 2); + for b in bullets { + assert!(b.face.covers('①'), "drawn with {}, which lacks the circled digit", b.face.family); + assert!(b.glyphs.iter().all(|g| g.0 != 0), "no missing-glyph box"); + } +}