azul_layout/font/
mod.rs

1#![cfg(feature = "font_loading")]
2
3use azul_css::{AzString, U8Vec};
4use rust_fontconfig::{FcFontCache, FontSource};
5
6pub mod loading;
7
8// serif
9#[cfg(target_os = "windows")]
10const KNOWN_SYSTEM_SERIF_FONTS: &[&str] = &["Times New Roman"];
11#[cfg(target_os = "linux")]
12const KNOWN_SYSTEM_SERIF_FONTS: &[&str] = &[
13    // <ask fc-match first>
14    "Times",
15    "Times New Roman",
16    "DejaVu Serif",
17    "Free Serif",
18    "Noto Serif",
19    "Bitstream Vera Serif",
20    "Roman",
21    "Regular",
22];
23#[cfg(target_os = "macos")]
24const KNOWN_SYSTEM_SERIF_FONTS: &[&str] = &["Times", "New York", "Palatino"];
25
26// monospace
27#[cfg(target_os = "windows")]
28const KNOWN_SYSTEM_MONOSPACE_FONTS: &[&str] = &[
29    "Segoe UI Mono",
30    "Courier New",
31    "Cascadia Code",
32    "Cascadia Mono",
33];
34#[cfg(target_os = "linux")]
35const KNOWN_SYSTEM_MONOSPACE_FONTS: &[&str] = &[
36    // <ask fc-match first>
37    "Source Code Pro",
38    "Cantarell",
39    "DejaVu Sans Mono",
40    "Roboto Mono",
41    "Ubuntu Monospace",
42    "Droid Sans Mono",
43];
44#[cfg(target_os = "macos")]
45const KNOWN_SYSTEM_MONOSPACE_FONTS: &[&str] = &[
46    "SF Mono",
47    "Menlo",
48    "Monaco",
49    "Oxygen Mono",
50    "Source Code Pro",
51    "Fira Mono",
52];
53
54// sans-serif
55#[cfg(target_os = "windows")]
56const KNOWN_SYSTEM_SANS_SERIF_FONTS: &[&str] = &[
57    "Segoe UI", // Vista and newer, including Windows 10
58    "Tahoma",   // XP
59    "Microsoft Sans Serif",
60    "MS Sans Serif",
61    "Helv",
62];
63#[cfg(target_os = "linux")]
64const KNOWN_SYSTEM_SANS_SERIF_FONTS: &[&str] = &[
65    // <ask fc-match first>
66    "Ubuntu",
67    "Arial",
68    "DejaVu Sans",
69    "Noto Sans",
70    "Liberation Sans",
71];
72#[cfg(target_os = "macos")]
73const KNOWN_SYSTEM_SANS_SERIF_FONTS: &[&str] = &[
74    "San Francisco",  // default on El Capitan and newer
75    "Helvetica Neue", // default on Yosemite
76    "Lucida Grande",  // other
77];
78
79#[cfg(target_family = "wasm")]
80const KNOWN_SYSTEM_MONOSPACE_FONTS: &[&str] = &[];
81#[cfg(target_family = "wasm")]
82const KNOWN_SYSTEM_SANS_SERIF_FONTS: &[&str] = &[];
83#[cfg(target_family = "wasm")]
84const KNOWN_SYSTEM_SERIF_FONTS: &[&str] = &[];
85
86// italic / oblique / fantasy: same as sans-serif for now, but set the oblique flag
87
88/// Returns the font file contents from the computer + the font index
89pub fn load_system_font(id: &str, fc_cache: &FcFontCache) -> Option<(U8Vec, i32)> {
90    use rust_fontconfig::{FcFontPath, FcPattern, PatternMatch};
91
92    let mut patterns = Vec::new();
93
94    match id {
95        "monospace" => {
96            #[cfg(target_os = "linux")]
97            {
98                if let Some(gsettings_pref) = linux_get_gsettings_font("monospace-font-name") {
99                    patterns.push(FcPattern {
100                        name: Some(gsettings_pref),
101                        ..FcPattern::default()
102                    });
103                }
104                if let Some(fontconfig_pref) = linux_get_fc_match_font("monospace") {
105                    patterns.push(FcPattern {
106                        name: Some(fontconfig_pref),
107                        ..FcPattern::default()
108                    });
109                }
110            }
111
112            for monospace_font_name in KNOWN_SYSTEM_MONOSPACE_FONTS.iter() {
113                patterns.push(FcPattern {
114                    name: Some(monospace_font_name.to_string()),
115                    ..FcPattern::default()
116                });
117            }
118
119            patterns.push(FcPattern {
120                monospace: PatternMatch::True,
121                ..FcPattern::default()
122            });
123        }
124        "fantasy" | "oblique" => {
125            #[cfg(target_os = "linux")]
126            {
127                if let Some(fontconfig_pref) = linux_get_fc_match_font("sans-serif") {
128                    patterns.push(FcPattern {
129                        name: Some(fontconfig_pref),
130                        oblique: PatternMatch::True,
131                        ..FcPattern::default()
132                    });
133                }
134            }
135            for serif_font in KNOWN_SYSTEM_SERIF_FONTS.iter() {
136                patterns.push(FcPattern {
137                    name: Some(serif_font.to_string()),
138                    oblique: PatternMatch::True,
139                    ..FcPattern::default()
140                });
141            }
142
143            patterns.push(FcPattern {
144                oblique: PatternMatch::True,
145                ..FcPattern::default()
146            });
147        }
148        "italic" => {
149            #[cfg(target_os = "linux")]
150            {
151                if let Some(fontconfig_pref) = linux_get_fc_match_font("italic") {
152                    patterns.push(FcPattern {
153                        name: Some(fontconfig_pref),
154                        italic: PatternMatch::True,
155                        ..FcPattern::default()
156                    });
157                }
158            }
159            for serif_font in KNOWN_SYSTEM_SERIF_FONTS.iter() {
160                patterns.push(FcPattern {
161                    name: Some(serif_font.to_string()),
162                    italic: PatternMatch::True,
163                    ..FcPattern::default()
164                });
165            }
166
167            patterns.push(FcPattern {
168                italic: PatternMatch::True,
169                ..FcPattern::default()
170            });
171        }
172        "sans-serif" => {
173            #[cfg(target_os = "linux")]
174            {
175                if let Some(gsettings_pref) = linux_get_gsettings_font("font-name") {
176                    patterns.push(FcPattern {
177                        name: Some(gsettings_pref),
178                        ..FcPattern::default()
179                    });
180                }
181                if let Some(fontconfig_pref) = linux_get_fc_match_font("sans-serif") {
182                    patterns.push(FcPattern {
183                        name: Some(fontconfig_pref),
184                        ..FcPattern::default()
185                    });
186                }
187            }
188
189            for sans_serif_font in KNOWN_SYSTEM_SANS_SERIF_FONTS.iter() {
190                patterns.push(FcPattern {
191                    name: Some(sans_serif_font.to_string()),
192                    ..FcPattern::default()
193                });
194            }
195        }
196        "serif" => {
197            #[cfg(target_os = "linux")]
198            {
199                if let Some(fontconfig_pref) = linux_get_fc_match_font("serif") {
200                    patterns.push(FcPattern {
201                        name: Some(fontconfig_pref),
202                        ..FcPattern::default()
203                    });
204                }
205            }
206
207            for serif_font in KNOWN_SYSTEM_SERIF_FONTS.iter() {
208                patterns.push(FcPattern {
209                    name: Some(serif_font.to_string()),
210                    ..FcPattern::default()
211                });
212            }
213        }
214        other => {
215            patterns.push(FcPattern {
216                name: Some(other.clone().into()),
217                ..FcPattern::default()
218            });
219
220            patterns.push(FcPattern {
221                family: Some(other.clone().into()),
222                ..FcPattern::default()
223            });
224        }
225    }
226
227    // always resolve to some font, even if the font is wrong it's better
228    // than if the text doesn't show up at all
229    patterns.push(FcPattern::default());
230
231    for pattern in patterns {
232        // TODO: handle font fallbacks via s.fallbacks
233        let font_source = fc_cache
234            .query(&pattern, &mut Vec::new())
235            .and_then(|s| fc_cache.get_font_by_id(&s.id));
236
237        let font_source = match font_source {
238            Some(s) => s,
239            None => continue,
240        };
241
242        match font_source {
243            FontSource::Memory(m) => {
244                return Some((m.bytes.clone().into(), m.font_index as i32));
245            }
246            FontSource::Disk(d) => {
247                use std::{fs, path::Path};
248                if let Ok(bytes) = fs::read(Path::new(&d.path)) {
249                    return Some((bytes.into(), d.font_index as i32));
250                }
251            }
252        }
253    }
254
255    None
256}
257
258#[cfg(all(target_os = "linux", feature = "std"))]
259fn linux_get_gsettings_font(font_name: &'static str) -> Option<String> {
260    // Execute "gsettings get org.gnome.desktop.interface font-name" and parse the output
261    std::process::Command::new("gsettings")
262        .arg("get")
263        .arg("org.gnome.desktop.interface")
264        .arg(font_name)
265        .output()
266        .ok()
267        .map(|output| output.stdout)
268        .and_then(|stdout_bytes| String::from_utf8(stdout_bytes).ok())
269        .map(|stdout_string| stdout_string.lines().collect::<String>())
270        .map(|s| parse_gsettings_font(&s).to_string())
271}
272
273fn parse_gsettings_font(input: &str) -> &str {
274    use std::char;
275    let input = input.trim();
276    let input = input.trim_matches('\'');
277    let input = input.trim_end_matches(char::is_numeric);
278    let input = input.trim();
279    input
280}
281
282#[cfg(all(target_os = "linux", feature = "std"))]
283fn linux_get_fc_match_font(font_name: &'static str) -> Option<String> {
284    // Execute "fc-match serif" and parse the output
285    std::process::Command::new("fc-match")
286        .arg(font_name)
287        .output()
288        .ok()
289        .map(|output| output.stdout)
290        .and_then(|stdout_bytes| String::from_utf8(stdout_bytes).ok())
291        .map(|stdout_string| stdout_string.lines().collect::<String>())
292        .and_then(|s| Some(parse_fc_match_font(&s)?.to_string()))
293}
294
295// parse:
296// DejaVuSans.ttf: "DejaVu Sans" "Book"
297// DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"
298fn parse_fc_match_font(input: &str) -> Option<&str> {
299    let input = input.trim();
300    let mut split_iterator = input.split(":");
301    split_iterator.next()?;
302
303    let fonts_str = split_iterator.next()?; // "DejaVu Sans" "Book"
304    let fonts_str = fonts_str.trim();
305    let mut font_iterator = input.split("\" \"");
306    let first_font = font_iterator.next()?; // "DejaVu Sans
307
308    let first_font = first_font.trim();
309    let first_font = first_font.trim_start_matches('"');
310    let first_font = first_font.trim_end_matches('"');
311    let first_font = first_font.trim();
312
313    Some(first_font)
314}