diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 225f4af..f336ca7 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -679,7 +679,8 @@ fn map_symbol_bullet(c: &str, font: Option<&str>) -> String { }; return m.to_string(); } - c.to_string() + // PowerPoint draws only the first character of a longer `buChar` ("••" draws one dot). + c.graphemes(true).next().unwrap_or("•").to_string() } fn measure_str(face: &FontFace, size: f64, s: &str) -> (f64, Vec<(u32, f64)>) { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index f3d5db5..e014790 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -45,6 +45,20 @@ fn bullets_are_drawn_for_body_text() { assert_eq!(l.para_lines.len(), 2); } +#[test] +fn only_the_first_character_of_a_bullet_is_drawn() { + // "••" (seen in the wild) draws one dot in PowerPoint. + for (char, glyphs) in [("\u{2022}\u{2022}", 1), ("\u{2022}", 1), ("-->", 1)] { + let (p, mut sh) = setup("One", 600.0, 300.0); + sh.text.as_mut().unwrap().paragraphs[0].props.bullet = Some(Bullet::Char { char: char.into() }); + 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(), glyphs, "{char:?}"); + } + // The first grapheme cluster is kept whole. + assert_eq!(map_symbol_bullet("e\u{301}x", None), "e\u{301}"); +} + #[test] fn shrink_on_overflow() { let text = (0..30).map(|i| format!("Line number {i}")).collect::>().join("\n");