diff --git a/crates/text/src/behaviour_tests.rs b/crates/text/src/behaviour_tests.rs index b3cb4884..201aa460 100644 --- a/crates/text/src/behaviour_tests.rs +++ b/crates/text/src/behaviour_tests.rs @@ -265,6 +265,37 @@ fn hangul_comes_from_a_korean_font_when_one_is_installed() { } } +#[test] +fn thai_comes_from_a_thai_font_when_one_is_installed() { + // The default (Latin) face has no Thai. With a Thai face (Noto Sans Thai from craft-fonts or + // Linux, Leelawadee UI or Tahoma on Windows, Thonburi on macOS) the fallback draws every Thai + // cluster, marks included, instead of the missing-glyph box. Skips on a machine without one. + // "ภาษาไทย" (the Thai language) and "น้ำ" (water: a tone mark over SARA AM). + let s = "Ab \u{0E20}\u{0E32}\u{0E29}\u{0E32}\u{0E44}\u{0E17}\u{0E22} \u{0E19}\u{0E49}\u{0E33}"; + let chars: Vec = s.chars().collect(); + let is_thai = |c: &char| ('\u{0E00}'..='\u{0E7F}').contains(c); + let draws_all = |f: &Face| chars.iter().filter(|c| is_thai(c)).all(|c| f.has_char(*c)); + let Some(f) = face() else { return }; + if draws_all(&f) { + return; + } + let thai = ["Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI", "Thonburi", "Tahoma"]; + if !thai.iter().any(|t| find_face(t, "Regular").is_some_and(|t| draws_all(&t))) { + return; + } + let l = layout(&text(s, TextAutoResize::WidthAndHeight), None, None); + let glyphs: Vec<_> = l.glyphs.iter().filter(|g| chars.get(g.char_start).is_some_and(is_thai)).collect(); + assert!(!glyphs.is_empty()); + for g in &glyphs { + assert!(g.gid != 0, "char {}: missing glyph in {} {}", g.char_start, g.face.family, g.face.style); + let cluster = chars.get(g.char_start..g.char_end).unwrap_or(&[]); + assert!(cluster.iter().all(|c| g.face.has_char(*c)), "chars {}..{}: drawn by {}, which lacks some", g.char_start, g.char_end, g.face.family); + } + let drawn: std::collections::BTreeSet = glyphs.iter().flat_map(|g| g.char_start..g.char_end).collect(); + let want: std::collections::BTreeSet = (3..chars.len()).filter(|i| chars[*i] != ' ').collect(); + assert_eq!(drawn, want, "every Thai char is in a drawn cluster"); +} + #[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 604f3eee..e6e7b2f2 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -406,7 +406,7 @@ fn names(f: &skrifa::FontRef) -> (Option, Option) { } /// Families tried, in order, for characters a text's own font lacks. -const FALLBACK_FAMILIES: [&str; 20] = [ +const FALLBACK_FAMILIES: [&str; 25] = [ "Inter", "Helvetica Neue", "Arial", @@ -425,6 +425,11 @@ const FALLBACK_FAMILIES: [&str; 20] = [ "Noto Sans CJK KR", "Noto Sans KR", "Noto Sans Arabic", + "Noto Sans Thai", + "Noto Sans Thai UI", + "Leelawadee UI", + "Thonburi", + "Tahoma", "Apple Symbols", "Segoe UI Symbol", ]; diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index f2456d28..9ae9db49 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -263,3 +263,15 @@ fn font_bytes_load_into_the_db() { assert_eq!(db().read().unwrap().face_count(), faces); } } + +#[test] +fn every_platform_has_a_thai_font_in_the_fallbacks() { + // Thai typed into a Latin layer (new text starts in Inter) is drawn by the first fallback that + // has it: Noto Sans Thai (craft-fonts, Linux), Leelawadee UI or Tahoma (Windows), Thonburi + // (macOS). They come after the Latin faces, which go on drawing Latin text. + let at = |family: &str| FALLBACK_FAMILIES.iter().position(|f| *f == family); + let latin = at("Segoe UI").unwrap(); + for thai in ["Noto Sans Thai", "Leelawadee UI", "Tahoma", "Thonburi"] { + assert!(at(thai).is_some_and(|i| i > latin), "{thai}"); + } +}