diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 5f0729513..c573debc1 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -1474,10 +1474,12 @@ impl FontDb { let faces = self.read_faces(); let mut order: Vec<&Arc> = faces.iter().filter(|f| f.id != exclude).collect(); // Right-to-left letters prefer a font made for the script, so a word isn't split across - // fonts (that breaks Arabic joining). + // fonts (that breaks Arabic joining). Emoji prefer a colour emoji font. let rank = |f: &FontFace| { if crate::is_rtl(c) || arabic_block(c) { RTL_FALLBACKS.iter().position(|n| f.family.eq_ignore_ascii_case(n)).unwrap_or(RTL_FALLBACKS.len()) + } else if crate::is_emoji(c) { + crate::fallback_rank(c, &f.family) } else { 0 } diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 994a98f29..b82cbc4ca 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -21,7 +21,7 @@ pub use fontdb::{ pub use harfrust::Feature; use harfrust::{Direction, ShapeOptions, Tag, UnicodeBuffer}; pub use kurbo::BezPath; -pub use script::{fallback_rank, needs_layout_tables, script_fallbacks}; +pub use script::{fallback_rank, is_emoji, needs_layout_tables, script_fallbacks}; use skrifa::MetadataProvider; use skrifa::instance::Size; pub use ui_fallback::{SystemUiFont, is_cjk, system_cjk_ui_font, system_cjk_ui_font_for, ui_needs_system_cjk, ui_uncovered_cjk}; diff --git a/crates/fonts/src/script.rs b/crates/fonts/src/script.rs index 1c7c9609a..1727b71af 100644 --- a/crates/fonts/src/script.rs +++ b/crates/fonts/src/script.rs @@ -77,6 +77,9 @@ pub fn needs_layout_tables(c: char) -> bool { /// it: the platforms' own OpenType fonts and the open Noto/SIL families. Apple's older fonts for /// these scripts carry only AAT tables (which the shaper doesn't read), so they come last. pub fn script_fallbacks(c: char) -> &'static [&'static str] { + if is_emoji(c) { + return COLOR_EMOJI; + } match c.script() { Script::Myanmar => &["Myanmar Text", "Noto Sans Myanmar", "Padauk", "Pyidaungsu", "Noto Serif Myanmar", "Myanmar Sangam MN", "Myanmar MN"], Script::Khmer => &["Leelawadee UI", "Khmer UI", "Noto Sans Khmer", "Noto Serif Khmer", "Khmer OS", "Khmer Sangam MN", "Khmer MN"], @@ -89,6 +92,53 @@ pub fn script_fallbacks(c: char) -> &'static [&'static str] { } } +/// The platforms' colour emoji fonts. Emoji prefer them over the monochrome symbol fonts that +/// also cover many emoji (Windows' Segoe UI Symbol has ✅, so it drew in black and white). +const COLOR_EMOJI: &[&str] = &["Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji"]; + +/// Characters drawn as emoji by default (Unicode `Emoji_Presentation`): the BMP ones listed in +/// emoji-data.txt, and the pictograph blocks from U+1F000. +// ponytail: all of U+1F000..U+1FAFF counts as emoji (a few text-default pictographs too); switch to the exact emoji-data ranges if one must draw monochrome +pub fn is_emoji(c: char) -> bool { + matches!( + u32::from(c), + 0x1F000..=0x1FAFF + | 0x231A..=0x231B + | 0x23E9..=0x23EC + | 0x23F0 + | 0x23F3 + | 0x25FD..=0x25FE + | 0x2614..=0x2615 + | 0x2648..=0x2653 + | 0x267F + | 0x2693 + | 0x26A1 + | 0x26AA..=0x26AB + | 0x26BD..=0x26BE + | 0x26C4..=0x26C5 + | 0x26CE + | 0x26D4 + | 0x26EA + | 0x26F2..=0x26F3 + | 0x26F5 + | 0x26FA + | 0x26FD + | 0x2705 + | 0x270A..=0x270B + | 0x2728 + | 0x274C + | 0x274E + | 0x2753..=0x2755 + | 0x2757 + | 0x2795..=0x2797 + | 0x27B0 + | 0x27BF + | 0x2B1B..=0x2B1C + | 0x2B50 + | 0x2B55 + ) +} + /// How well `family` suits `c` as a fallback: 0 for the first preferred family, then the others /// in order; then families named after the script ("Noto Sans Tamil"); then everything else. pub fn fallback_rank(c: char, family: &str) -> usize { @@ -132,4 +182,17 @@ mod tests { assert!(fallback_rank('த', "Noto Sans Tamil") < fallback_rank('த', "Arial")); assert_eq!(fallback_rank('a', "Arial"), fallback_rank('a', "Noto Sans")); } + + #[test] + fn emoji_prefer_colour_emoji_fonts() { + for c in ['✅', '🧠', '⭐', '⌚', '🫶'] { + assert!(is_emoji(c), "{c}"); + assert!(fallback_rank(c, "Segoe UI Emoji") < fallback_rank(c, "Segoe UI Symbol"), "{c}"); + assert!(fallback_rank(c, "Noto Color Emoji") < fallback_rank(c, "Noto Emoji"), "{c}"); + } + // Text-style symbols keep the symbol fonts. + for c in ['✔', '☺', '→', 'a', '1'] { + assert!(!is_emoji(c), "{c}"); + } + } }