diff --git a/crates/text/src/behaviour_tests.rs b/crates/text/src/behaviour_tests.rs index 201aa460..6eb7a922 100644 --- a/crates/text/src/behaviour_tests.rs +++ b/crates/text/src/behaviour_tests.rs @@ -296,6 +296,28 @@ fn thai_comes_from_a_thai_font_when_one_is_installed() { assert_eq!(drawn, want, "every Thai char is in a drawn cluster"); } +#[test] +fn symbols_come_from_a_symbol_font_when_one_is_installed() { + // ⌖ (U+2316) and ⤒ (U+2912) are in no Latin text face. With a symbol font that has them (Apple + // Symbols on macOS, Segoe UI Symbol on Windows, Noto Sans Symbols 2 and Noto Sans Math on + // Linux) the fallback draws them instead of the missing-glyph box (#1156). Each character + // skips on a machine with no installed fallback family that has it. + let Some(f) = face() else { return }; + let families = ["Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols", "Noto Sans Symbols 2", "Noto Sans Symbols2", "Noto Sans Math"]; + for c in ['\u{2316}', '\u{2912}'] { + if f.has_char(c) || !families.iter().any(|k| find_face(k, "Regular").is_some_and(|k| k.has_char(c))) { + continue; + } + let t = text(&format!("A {c}"), TextAutoResize::WidthAndHeight); + let l = layout(&t, None, None); + let drawn: Vec<_> = l.glyphs.iter().filter(|g| g.char_start == 2).collect(); + assert!(!drawn.is_empty(), "U+{:04X} has a glyph", c as u32); + for g in drawn { + assert!(g.face.has_char(c), "U+{:04X} drawn by {} {}, which lacks it", c as u32, g.face.family, g.face.style); + } + } +} + #[test] fn break_opportunities() { // (before, after, may break between, derivation) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index e6e7b2f2..6131077d 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -405,8 +405,10 @@ fn names(f: &skrifa::FontRef) -> (Option, Option) { (family, style) } -/// Families tried, in order, for characters a text's own font lacks. -const FALLBACK_FAMILIES: [&str; 25] = [ +/// Families tried, in order, for characters a text's own font lacks. The symbol fonts come last: +/// Apple Symbols (macOS), Segoe UI Symbol (Windows) and the Noto symbol and math fonts (Linux), for +/// signs such as ⌖ and ⤒ that no text face has. +const FALLBACK_FAMILIES: [&str; 29] = [ "Inter", "Helvetica Neue", "Arial", @@ -432,6 +434,10 @@ const FALLBACK_FAMILIES: [&str; 25] = [ "Tahoma", "Apple Symbols", "Segoe UI Symbol", + "Noto Sans Symbols", + "Noto Sans Symbols 2", + "Noto Sans Symbols2", + "Noto Sans Math", ]; /// A system font file entry not yet loaded. diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index 9ae9db49..c9094218 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -192,6 +192,19 @@ fn fallback_faces_follow_the_runs_style() { } } +#[test] +fn every_platform_has_a_symbol_font_in_the_fallbacks() { + // Symbols such as ⌖ (U+2316) and ⤒ (U+2912) are in no text face (#1156): macOS has Apple + // Symbols, Windows Segoe UI Symbol, and Linux the Noto symbol and math fonts (fontconfig names + // "Noto Sans Symbols 2"; some packages name it "Noto Sans Symbols2"). + for family in ["Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols", "Noto Sans Symbols 2", "Noto Sans Symbols2", "Noto Sans Math"] { + assert!(FALLBACK_FAMILIES.contains(&family), "{family} is a fallback"); + } + // Text faces come first: a symbol font only draws what they lack. + let at = |f: &str| FALLBACK_FAMILIES.iter().position(|x| *x == f).unwrap(); + assert!(at("Noto Sans") < at("Noto Sans Symbols") && at("DejaVu Sans") < at("Noto Sans Math")); +} + #[cfg(all(unix, not(target_os = "macos")))] #[test] fn user_font_dirs_follow_xdg_data_home() {