Skip to content

Commit db622ed

Browse files
committed
fix: Emoji rendering in svg preview
- enabled `raster_images` feature on `resvg` crate to allow resvg to render emoji fonts - Add a custom fallback selection function to make the fallback fonts for emojis more consistent. Signed-off-by: Alan P John <alanpjohn@outlook.com>
1 parent e79429b commit db622ed

4 files changed

Lines changed: 84 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gpui/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,16 @@ chrono.workspace = true
7070
profiling.workspace = true
7171
rand.workspace = true
7272
raw-window-handle = "0.6"
73+
regex.workspace = true
7374
refineable.workspace = true
7475
scheduler.workspace = true
7576
resvg = { version = "0.45.0", default-features = false, features = [
7677
"text",
7778
"system-fonts",
7879
"memmap-fonts",
80+
"raster-images"
7981
] }
82+
util.workspace = true
8083
usvg = { version = "0.45.0", default-features = false }
8184
util_macros.workspace = true
8285
schemars.workspace = true
@@ -148,7 +151,6 @@ gpui_platform.workspace = true
148151
lyon = { version = "1.0", features = ["extra"] }
149152
rand.workspace = true
150153
scheduler = { workspace = true, features = ["test-support"] }
151-
unicode-segmentation.workspace = true
152154
gpui_util = { workspace = true }
153155
proptest = { workspace = true }
154156

crates/gpui/src/svg_renderer.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ use std::{
99
hash::Hash,
1010
sync::{Arc, LazyLock},
1111
};
12+
use util::is_emoji_character;
13+
14+
/// Emoji font families for each platform
15+
/// Hardcoded for simplicity
16+
#[cfg(target_os = "macos")]
17+
const EMOJI_FONT_FAMILIES: &[&str] = &["Apple Color Emoji", ".AppleColorEmojiUI"];
18+
19+
#[cfg(target_os = "windows")]
20+
const EMOJI_FONT_FAMILIES: &[&str] = &["Segoe UI Emoji", "Segoe UI Symbol"];
21+
22+
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
23+
const EMOJI_FONT_FAMILIES: &[&str] = &[
24+
"Noto Color Emoji",
25+
"Emoji One",
26+
"Twitter Color Emoji",
27+
"JoyPixels",
28+
];
1229

1330
/// When rendering SVGs, we render them at twice the size to get a higher-quality result.
1431
pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
@@ -52,10 +69,40 @@ impl SvgRenderer {
5269
default_font_resolver(font, db)
5370
},
5471
);
72+
let default_fallback_selection = usvg::FontResolver::default_fallback_selector();
73+
let fallback_selection = Box::new(
74+
move |ch: char, fonts: &[usvg::fontdb::ID], db: &mut Arc<usvg::fontdb::Database>| {
75+
// Check if this is an emoji character
76+
if is_emoji_character(ch) {
77+
// Build a list of emoji font families to query
78+
let families: SmallVec<[usvg::fontdb::Family; 8]> = EMOJI_FONT_FAMILIES
79+
.iter()
80+
.map(|&name| usvg::fontdb::Family::Name(name))
81+
.collect();
82+
83+
let query = usvg::fontdb::Query {
84+
families: &families,
85+
weight: usvg::fontdb::Weight(400),
86+
stretch: usvg::fontdb::Stretch::Normal,
87+
style: usvg::fontdb::Style::Normal,
88+
};
89+
90+
// Query returns the first matching font from the prioritized list
91+
if let Some(id) = db.query(&query) {
92+
if !fonts.contains(&id) {
93+
return Some(id);
94+
}
95+
}
96+
}
97+
98+
// Fall back to default behavior for non-emoji or when no emoji font found
99+
default_fallback_selection(ch, fonts, db)
100+
},
101+
);
55102
let options = usvg::Options {
56103
font_resolver: usvg::FontResolver {
57104
select_font: font_resolver,
58-
select_fallback: usvg::FontResolver::default_fallback_selector(),
105+
select_fallback: fallback_selection,
59106
},
60107
..Default::default()
61108
};

crates/util/src/util.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,13 @@ pub fn word_consists_of_emojis(s: &str) -> bool {
727727
prev_end == s.len()
728728
}
729729

730+
/// Check if a character is an emoji using Unicode properties.
731+
pub fn is_emoji_character(c: char) -> bool {
732+
let mut buf = [0u8; 4];
733+
let s = c.encode_utf8(&mut buf);
734+
emoji_regex().is_match(s)
735+
}
736+
730737
/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike
731738
/// `str::split`, this is not generic on pattern types and does not return an `Iterator`.
732739
pub fn split_str_with_ranges<'s>(
@@ -969,6 +976,27 @@ mod tests {
969976
}
970977
}
971978

979+
#[test]
980+
fn test_char_is_emoji() {
981+
let chars_to_test = vec![
982+
('👋', true),
983+
('✅', true),
984+
('©', true),
985+
('♥', true),
986+
('a', false),
987+
('Z', false),
988+
('1', false),
989+
(' ', false),
990+
('漢', false),
991+
('中', false),
992+
('カ', false),
993+
];
994+
995+
for (text, expected_result) in chars_to_test {
996+
assert_eq!(is_emoji_character(text), expected_result);
997+
}
998+
}
999+
9721000
#[test]
9731001
fn test_truncate_lines_and_trailoff() {
9741002
let text = r#"Line 1

0 commit comments

Comments
 (0)