fix(fonts): emoji prefer a colour emoji font on Windows

Fallback put Segoe UI Symbol before Segoe UI Emoji, and Symbol has
monochrome glyphs for some emoji (✅), so they drew in black and white
and tests_color_glyphs failed on Windows. Characters with default emoji
presentation now rank the colour emoji fonts first, both when loading a
system fallback and when choosing among loaded faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaan Ilter
2026-10-15 12:44:48 +01:00
co-authored by Claude Opus 5.5
parent fd0bb2fb1e
commit 624ce0bdc2
3 changed files with 67 additions and 2 deletions
+3 -1
View File
@@ -1474,10 +1474,12 @@ impl FontDb {
let faces = self.read_faces();
let mut order: Vec<&Arc<FontFace>> = 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
}
+1 -1
View File
@@ -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};
+63
View File
@@ -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}");
}
}
}