GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/lib.rs (25.8K)
1 // Modules under `cfg(not(target_arch = "wasm32"))` are the native renderer
2 // and services (Vulkan, the compositor IPC, file dialogs); those under
3 // `cfg(not(any(target_arch = "wasm32", target_os = "macos")))` are the
4 // Wayland shell's, which macOS replaces with `mac` (AppKit). Everything else
5 // builds for the browser too: `scripts/check-wasm` is the check, and
6 // `scripts/check-mac` the macOS one.
7 pub mod a11y;
8 pub mod color;
9 pub mod compute;
10 pub mod widget;
11 pub use cce_core::config;
12 pub use cce_core::input;
13 pub mod history;
14 pub mod ime;
15 pub mod layout;
16 pub use cce_core::relief_spec;
17 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
18 pub mod wayland;
19 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
20 pub mod protocol;
21 pub mod engine;
22 pub mod scale;
23 pub use cce_core::units;
24 pub mod backend;
25 pub mod context;
26 pub mod draw;
27 pub mod scene;
28 #[cfg(not(target_arch = "wasm32"))]
29 pub mod file_dialog;
30 pub mod icon;
31 #[cfg(not(target_arch = "wasm32"))]
32 pub use cce_core::ipc;
33 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
34 pub mod mcp;
35 pub use cce_core::motion;
36 /// The user's locale (`locale::locale()`), what every font system is built with.
37 pub use cce_core::locale;
38 pub mod l10n;
39 pub mod window_state;
40 pub mod style;
41 pub mod text_input;
42 #[cfg(not(target_arch = "wasm32"))]
43 pub mod vk;
44 #[cfg(target_arch = "wasm32")]
45 pub mod web;
46 #[cfg(target_os = "macos")]
47 pub mod mac;
48
49 #[cfg(test)]
50 mod config_style_tests;
51
52 pub mod colors {
53 pub use crate::color::*;
54 }
55
56 /// The text-shaping library, re-exported so clients need no text dependency of
57 /// their own: `cce_ui::cosmic_text::FontSystem` rather than a per-crate
58 /// `cosmic-text` (previously `glyphon`) entry in every client's Cargo.toml.
59 /// Re-exporting also keeps every client on the one version cce-ui shapes with —
60 /// a `FontSystem` handed across the boundary must be the same type.
61 pub use cosmic_text;
62 /// AccessKit, whose nodes an app pushes for what it draws itself
63 /// (`Application::accessibility`, `a11y::AppNodes`), so it needs no dependency of its own.
64 pub use accesskit;
65
66
67
68 /// `CCE_SCROLL_DEBUG=1` traces the wheel pipeline to stderr: raw coalesced
69 /// axis input (runner), routing decisions (ParametersBg), slider gate/value
70 /// steps, and glide ticks. Diagnostic-only; checked once per process.
71 pub fn scroll_debug() -> bool {
72 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
73 *ON.get_or_init(|| std::env::var_os("CCE_SCROLL_DEBUG").is_some())
74 }
75
76 /// Directory bundled fonts are loaded from: `$CCE_FONTS_DIR`, else `~/Dropbox/Fonts`.
77 /// Resolving via `$HOME` keeps the existing location without a hardcoded username.
78 pub fn fonts_dir() -> String {
79 std::env::var("CCE_FONTS_DIR").unwrap_or_else(|_| {
80 let home = std::env::var("HOME").unwrap_or_default();
81 format!("{home}/Dropbox/Fonts")
82 })
83 }
84
85 /// Directory the bundled cce-icons SVGs are loaded from: `$CCE_ICONS_DIR`, else
86 /// `~/projects/cce/cce-icons/svg`.
87 ///
88 /// The workspace moved out of ~/Dropbox on 2026-08-27: 432k of its 444k files
89 /// were cargo build artifacts, and syncing them kept Dropbox re-hashing a tree
90 /// that regenerates itself. Set `$CCE_ICONS_DIR` if yours lives elsewhere —
91 /// this default is the only path in the toolkit that assumes a checkout
92 /// location.
93 pub fn icons_dir() -> String {
94 std::env::var("CCE_ICONS_DIR").unwrap_or_else(|_| {
95 let home = std::env::var("HOME").unwrap_or_default();
96 format!("{home}/projects/cce/cce-icons/svg")
97 })
98 }
99
100 /// Rasterize a bundled cce-icons SVG (`<name>.svg` under [`icons_dir`]) at
101 /// `px` on its longer side and upload it as a renderer texture. Returns
102 /// `(image id, pixel w, pixel h)` for `PaintCtx::image` / `ImageView`; cached
103 /// per `(name, px)` so widget rebuilds reuse the one upload. `None` when the
104 /// icon is missing or unparsable (callers keep a text fallback).
105 ///
106 /// **The cache is per renderer, not per process.** An image id names an entry
107 /// in one renderer's image table, and a renderer does not outlive its
108 /// session: `window_runner` repairs a lost Wayland transport by opening a new
109 /// session around the same `Application`, which rebuilds the renderer and its
110 /// image table. A draw for an id that table does not hold is skipped rather
111 /// than reported, so a cache that survived the rebuild left every bundled
112 /// glyph in the process silently undrawn — a status bar that reconnected kept
113 /// its numbers and lost its icons, and the same went for every
114 /// [`Button::new_icon`] face, treelist chevron and ramp delete button in the
115 /// DE. Keying the cache on [`vk::renderer_epoch`] makes the first lookup after
116 /// a rebuild a miss, which re-rasterizes and re-uploads into the live
117 /// renderer.
118 ///
119 /// The id cache itself has to stay: this is called from widget rebuilds, so
120 /// uploading per call would burn through the renderer's 256-image budget in
121 /// seconds. Caching only the decode — what [`icon::upload_themed`] does — is
122 /// right for a caller that uploads rarely and owns what it gets back, and
123 /// wrong here.
124 ///
125 /// [`Button::new_icon`]: widget::Button::new_icon
126 pub fn upload_icon(name: &str, px: u32) -> Option<(u32, u32, u32)> {
127 upload_icon_as(name, px, None)
128 }
129
130 /// [`upload_icon`] in a colour: the glyph's pixels multiplied by `rgb`
131 /// (raw sRGB, as a [`TextLabel`](widget::display::TextLabel)'s colour is,
132 /// so a glyph tinted with a label's colour matches the label). The artwork
133 /// is white, so multiplying IS tinting, and what a glyph shades darker (a
134 /// knocked-out mark) stays proportionally darker. Cached per
135 /// `(name, px, rgb)` like [`upload_icon`], and re-uploaded after a renderer
136 /// rebuild the same way.
137 ///
138 /// A `PaintCtx::image` carries alpha and no colour, which is why a colour
139 /// has to be baked into the texture; [`icon_tint`] is the colour a
140 /// toolkit `[f32; 4]` becomes. The `weather-*` glyphs carry colours of
141 /// their own and are drawn with [`upload_icon`].
142 pub fn upload_icon_tinted(name: &str, px: u32, rgb: [u8; 3]) -> Option<(u32, u32, u32)> {
143 upload_icon_as(name, px, Some(rgb))
144 }
145
146 /// The `[u8; 3]` tint a toolkit colour becomes — the conversion a
147 /// `TextLabel` applies to its own colour, so glyph and text match.
148 pub fn icon_tint(color: [f32; 4]) -> [u8; 3] {
149 [
150 (color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
151 (color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
152 (color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
153 ]
154 }
155
156 /// A bundled glyph rasterized at `px`, tinted when `tint` is given: the
157 /// pixels [`upload_icon`] and [`upload_icon_tinted`] upload, for a caller
158 /// that uploads them itself (a renderer with images of its own).
159 pub fn icon_pixels(name: &str, px: u32, tint: Option<[u8; 3]>) -> Option<(Vec<u8>, u32, u32)> {
160 let path = format!("{}/{name}.svg", icons_dir());
161 let data = std::fs::read(&path).ok()?;
162 let (mut rgba, w, h) = rasterize_svg(&data, px)?;
163 if let Some(rgb) = tint {
164 for p in rgba.as_chunks_mut::<4>().0 {
165 for (c, &t) in p[..3].iter_mut().zip(rgb.iter()) {
166 *c = ((*c as u16 * t as u16 + 127) / 255) as u8;
167 }
168 }
169 }
170 Some((rgba, w, h))
171 }
172
173 /// What a bundled glyph's image id holds: `(name, px, tint)`.
174 type IconSource = (String, u32, Option<[u8; 3]>);
175
176 /// Every id [`upload_icon`] and [`upload_icon_tinted`] have handed out, and
177 /// the glyph it holds — see [`icon_source`].
178 static ICON_SOURCES: std::sync::Mutex<Option<std::collections::HashMap<u32, IconSource>>> =
179 std::sync::Mutex::new(None);
180
181 /// The glyph an image id from [`upload_icon`] / [`upload_icon_tinted`]
182 /// holds, or `None` for any other image. What lets a renderer that keeps
183 /// images of its own (the context menu's popup) draw the same glyph from a
184 /// display list painted with the window's ids.
185 pub fn icon_source(id: u32) -> Option<IconSource> {
186 ICON_SOURCES.lock().unwrap().as_ref()?.get(&id).cloned()
187 }
188
189 fn upload_icon_as(name: &str, px: u32, tint: Option<[u8; 3]>) -> Option<(u32, u32, u32)> {
190 use std::collections::HashMap;
191 use std::sync::Mutex;
192 /// The cached ids, and the renderer epoch they were uploaded to.
193 static CACHE: Mutex<Option<(u32, HashMap<IconSource, Option<(u32, u32, u32)>>)>> =
194 Mutex::new(None);
195 let epoch = crate::draw::renderer_epoch();
196 let key = (name.to_string(), px, tint);
197 let mut guard = CACHE.lock().unwrap();
198 let (cached_epoch, cache) = guard.get_or_insert_with(|| (epoch, HashMap::new()));
199 if *cached_epoch != epoch {
200 // The renderer these ids named is gone, and its image table went with
201 // it — so this is a forget, not a teardown; there is nothing to free.
202 cache.clear();
203 *cached_epoch = epoch;
204 }
205 if let Some(hit) = cache.get(&key) {
206 return *hit;
207 }
208 let loaded = icon_pixels(name, px, tint).map(|(rgba, w, h)| {
209 let id = crate::draw::upload_rgba(rgba, w, h);
210 ICON_SOURCES.lock().unwrap().get_or_insert_with(HashMap::new).insert(id, key.clone());
211 (id, w, h)
212 });
213 cache.insert(key, loaded);
214 loaded
215 }
216
217 /// Rasterize SVG bytes at `px` on the longer side: straight (un-premultiplied)
218 /// RGBA8 pixels plus dimensions, ready for `vk::upload_rgba`. Text elements
219 /// resolve through the shared fontdb (a thread-safe static), so callers may
220 /// rasterize off the UI thread and upload later — cce-files' preview service
221 /// does. `None` when the data is unparsable.
222 pub fn rasterize_svg(data: &[u8], px: u32) -> Option<(Vec<u8>, u32, u32)> {
223 let opt = resvg::usvg::Options::default();
224 let fontdb = crate::widget::get_font_db();
225 let tree = resvg::usvg::Tree::from_data(data, &opt, fontdb).ok()?;
226 let size = tree.size();
227 let (sw, sh) = (size.width().max(1.0), size.height().max(1.0));
228 let scale = px as f32 / sw.max(sh);
229 let w = (sw * scale).round().max(1.0) as u32;
230 let h = (sh * scale).round().max(1.0) as u32;
231 let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h)?;
232 resvg::render(
233 &tree,
234 resvg::tiny_skia::Transform::from_scale(scale, scale),
235 &mut pixmap.as_mut(),
236 );
237 // tiny-skia pixels are premultiplied; the upload path takes straight RGBA.
238 let mut rgba = pixmap.take();
239 for p in rgba.as_chunks_mut::<4>().0 {
240 let a = p[3] as f32 / 255.0;
241 if a > 0.0 {
242 p[0] = ((p[0] as f32 / a).min(255.0)) as u8;
243 p[1] = ((p[1] as f32 / a).min(255.0)) as u8;
244 p[2] = ((p[2] as f32 / a).min(255.0)) as u8;
245 }
246 }
247 Some((rgba, w, h))
248 }
249
250 /// Build a cosmic-text `FontSystem` loaded with the bundled CCE fonts (house style).
251 /// System fonts are loaded only if `$CCE_LOAD_SYSTEM_FONTS` is set. Configured
252 /// custom fonts are validated with a warning if missing.
253 pub fn create_font_system() -> cosmic_text::FontSystem {
254 build_font_system(false)
255 }
256
257 /// A `FontSystem` the TOOLKIT owns, for widget GEOMETRY rather than drawing:
258 /// the shaping a widget's own selection, caret and click-to-index math needs on
259 /// a host that never hands one in.
260 ///
261 /// Paint-walk apps shape through their own (`prepare_text`) and the display
262 /// list shapes through the runner's — but a flat-path host consumes
263 /// `all_quads`, so nothing ever shaped for the widgets it draws and `TextBox`
264 /// fell back to `measure_text_width("M")`: an SVG-rasterized INKED extent, not
265 /// an advance, which walks off the glyphs a few px per character.
266 ///
267 /// Created on FIRST USE, so an app that shapes for itself never pays for it,
268 /// and from the same bundle [`create_font_system`] gives the renderer. The
269 /// shaped-buffer cache behind it is keyed by text/size/family and shared per
270 /// thread, so in practice this reads the very buffers the draw already built.
271 pub fn geometry_font_system() -> &'static std::sync::Mutex<cosmic_text::FontSystem> {
272 static GEOMETRY_FONT_SYSTEM: std::sync::OnceLock<std::sync::Mutex<cosmic_text::FontSystem>> =
273 std::sync::OnceLock::new();
274 GEOMETRY_FONT_SYSTEM.get_or_init(|| std::sync::Mutex::new(create_font_system()))
275 }
276
277 /// Like [`create_font_system`] but always also loads installed system fonts, for
278 /// apps that must see every font on the system (e.g. the font picker) or want
279 /// them as fallbacks. Additive — bundled CCE fonts are still loaded.
280 pub fn create_font_system_with_system_fonts() -> cosmic_text::FontSystem {
281 build_font_system(true)
282 }
283
284 /// Rescan the font directories — the CCE fonts dir, the fallback faces and the
285 /// system's (fontconfig's) — for fonts installed or removed since `fs` was
286 /// loaded, and bring every `FontSystem` in the process up to date with them:
287 /// `fs` now, the rest (the engine's renderer among them) the next time they
288 /// shape or draw. Without it a database is the disk as it was at startup.
289 ///
290 /// A font picker's refresh: it reads every font file's metadata, so it takes
291 /// a moment and does not belong in a frame loop. Returns whether anything
292 /// changed.
293 #[cfg(not(target_arch = "wasm32"))]
294 pub fn rescan_fonts(fs: &mut cosmic_text::FontSystem) -> bool {
295 use cosmic_text::fontdb::{Database, Source};
296 use std::collections::HashSet;
297 use std::path::PathBuf;
298 // Each font file once, in the order the database loaded it.
299 fn files(db: &Database) -> Vec<PathBuf> {
300 let mut seen = HashSet::new();
301 db.faces()
302 .filter(|f| !crate::backend::text::is_alias_face(f))
303 .filter_map(|f| match &f.source {
304 Source::File(p) | Source::SharedFile(p, _) => Some(p.clone()),
305 _ => None,
306 })
307 .filter(|p| seen.insert(p.clone()))
308 .collect()
309 }
310 // The same discovery `build_font_system` makes.
311 let mut bundled = Database::new();
312 bundled.load_fonts_dir(fonts_dir());
313 load_fallback_fonts(&mut bundled);
314 let mut all = bundled.clone();
315 all.load_system_fonts();
316 let now = files(&all);
317 let now_set: HashSet<PathBuf> = now.iter().cloned().collect();
318
319 // `fs` as every earlier change leaves it, so the delta is against that.
320 crate::backend::text::sync_font_set(fs);
321 let before: HashSet<PathBuf> = files(fs.db()).into_iter().collect();
322 let added: Vec<PathBuf> = now.into_iter().filter(|p| !before.contains(p)).collect();
323 let removed: HashSet<PathBuf> = before.difference(&now_set).cloned().collect();
324 let rescan = crate::backend::text::Rescan::new(added, removed, files(&bundled).into_iter().collect());
325 if rescan.is_empty() {
326 return false;
327 }
328 crate::backend::text::push_rescan(rescan);
329 crate::backend::text::sync_font_set(fs);
330 true
331 }
332
333 /// Targeted script-fallback faces loaded alongside the bundled house fonts.
334 /// The bundled set covers Latin; anything else shaped to tofu unless
335 /// `$CCE_LOAD_SYSTEM_FONTS` pulled in the entire system set. Probing a short
336 /// list of well-known files keeps startup cheap while giving cosmic-text's
337 /// unix script fallback (family names "Noto Sans CJK *", "Noto Color Emoji")
338 /// real faces to land on. `$CCE_NO_FALLBACK_FONTS` opts out.
339 #[cfg(not(target_arch = "wasm32"))]
340 fn load_fallback_fonts(db: &mut cosmic_text::fontdb::Database) {
341 if std::env::var("CCE_NO_FALLBACK_FONTS").is_ok() {
342 return;
343 }
344 let home = std::env::var("HOME").unwrap_or_default();
345 let candidates = [
346 // CJK (Arch noto-fonts-cjk; Debian/Fedora paths for good measure)
347 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc".to_string(),
348 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc".to_string(),
349 "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc".to_string(),
350 // emoji (Arch noto-fonts-emoji; other distros; per-user install)
351 "/usr/share/fonts/noto/NotoColorEmoji.ttf".to_string(),
352 "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf".to_string(),
353 "/usr/share/fonts/google-noto-emoji-color-fonts/NotoColorEmoji.ttf".to_string(),
354 format!("{home}/.local/share/fonts/NotoColorEmoji.ttf"),
355 ];
356 for path in &candidates {
357 if std::path::Path::new(path).exists() {
358 let _ = db.load_font_file(path);
359 }
360 }
361 }
362
363 /// Pin the generic families to faces that actually exist. fontdb's defaults
364 /// name Windows faces ("Arial"/"Times New Roman"), so Family::SansSerif /
365 /// Monospace never resolved here and every glyph of generic-family text
366 /// dropped into the per-glyph fallback chain — where Noto Color Emoji sits
367 /// high (cosmic-text common_fallback) and hijacked spaces and digits with
368 /// emoji metrics. Berkeley Mono is the house mono; Noto Sans CJK SC (the
369 /// targeted fallback face) doubles as a full Latin sans. Shared by every
370 /// shell's font system, the browser's included, so a font set resolves the
371 /// same families wherever it is loaded.
372 fn pin_generic_families(db: &mut cosmic_text::fontdb::Database) {
373 fn has_family(db: &cosmic_text::fontdb::Database, fam: &str) -> bool {
374 db.faces()
375 .any(|f| f.families.iter().any(|(n, _)| n == fam))
376 }
377 if has_family(db, "Berkeley Mono") {
378 db.set_monospace_family("Berkeley Mono");
379 }
380 if has_family(db, "Noto Sans CJK SC") {
381 db.set_sans_serif_family("Noto Sans CJK SC");
382 }
383 }
384
385 fn build_font_system(load_system_fonts: bool) -> cosmic_text::FontSystem {
386 let mut db = cosmic_text::fontdb::Database::new();
387 #[cfg(not(target_arch = "wasm32"))]
388 {
389 db.load_fonts_dir(fonts_dir());
390 load_fallback_fonts(&mut db);
391 // On macOS the system set is always loaded: it is a curated one,
392 // and what cosmic-text's fallback list there names ("Apple Color
393 // Emoji", "PingFang SC", …) — where on Linux the probe above finds
394 // the Noto faces in its place.
395 if load_system_fonts || cfg!(target_os = "macos") || std::env::var("CCE_LOAD_SYSTEM_FONTS").is_ok() {
396 db.load_system_fonts();
397 }
398 // An empty database guarantees a panic on the first shaped glyph
399 // (cosmic-text: "no default font found"), so if the bundled dir yielded
400 // nothing (missing $HOME/Dropbox/Fonts — e.g. the greeter running as
401 // root), fall back to system fonts rather than crash.
402 if db.faces().next().is_none() {
403 db.load_system_fonts();
404 }
405 }
406 #[cfg(target_arch = "wasm32")]
407 {
408 let _ = load_system_fonts;
409 page_fonts::load_into(&mut db);
410 }
411
412 pin_generic_families(&mut db);
413
414 // Validate configured custom fonts
415 let font_getters = vec![
416 ("list_font", crate::layout::list_font_parsed().0),
417 ("menubar_font", crate::layout::menubar_font_parsed().0),
418 ("statusbar_font", crate::layout::statusbar_font_parsed().0),
419 ("font_selector_font", crate::layout::font_selector_font_parsed().0),
420 ("button_strip_font", crate::layout::button_strip_font_parsed().0),
421 ("control_label_font", crate::layout::control_label_font_parsed().0),
422 ("control_label_font_detached", crate::layout::control_label_font_detached_parsed().0),
423 ("tree_font", crate::layout::tree_font_parsed().0),
424 ("graph_font", crate::layout::graph_font_parsed().0),
425 ("graph_node_font", crate::layout::graph_node_font_parsed().0),
426 ];
427
428 for (name, family) in font_getters {
429 if !family.is_empty() && family != "Berkeley Mono" && family != "sans-serif" {
430 let mut found = false;
431 for face in db.faces() {
432 for (fam, _) in &face.families {
433 if fam.to_lowercase() == family.to_lowercase() {
434 found = true;
435 break;
436 }
437 }
438 if found {
439 break;
440 }
441 }
442 if !found {
443 eprintln!(
444 "WARNING: Configured font family '{}' for property '{}' was not found in the fonts database. Falling back to default font.",
445 family, name
446 );
447 }
448 }
449 }
450
451 #[cfg(target_arch = "wasm32")]
452 page_fonts::stand_in_for_missing(&mut db);
453
454 cosmic_text::FontSystem::new_with_locale_and_db(crate::locale::locale().to_string(), db)
455 }
456
457 /// The fonts a page hands the browser shell (`web::run`). A page has no font
458 /// directory and no fontconfig, so on wasm every font database the toolkit
459 /// builds — the shell's, its own for widget geometry
460 /// ([`geometry_font_system`]) and the text-measurement one
461 /// (`widget::input::get_font_db`) — is loaded from these, one shared copy of
462 /// each file, with the generic families the page named.
463 #[cfg(target_arch = "wasm32")]
464 pub(crate) mod page_fonts {
465 use std::sync::{Arc, Mutex};
466
467 use cosmic_text::fontdb::{Database, Language};
468
469 type Font = Arc<dyn AsRef<[u8]> + Send + Sync>;
470
471 struct Provided {
472 files: Vec<Font>,
473 serif: Option<String>,
474 sans_serif: Option<String>,
475 monospace: Option<String>,
476 }
477
478 static PROVIDED: Mutex<Provided> =
479 Mutex::new(Provided { files: Vec::new(), serif: None, sans_serif: None, monospace: None });
480
481 /// What every font database built from now on loads: `files` (each a
482 /// font file's bytes), in order, and the families the generic ones name —
483 /// a fontconfig's answer, which on Linux `load_system_fonts` reads.
484 pub(crate) fn provide(files: Vec<Vec<u8>>, serif: Option<String>, sans_serif: Option<String>, monospace: Option<String>) {
485 let mut p = PROVIDED.lock().unwrap();
486 p.files.extend(files.into_iter().map(|f| Arc::new(f) as Font));
487 p.serif = serif.or(p.serif.take());
488 p.sans_serif = sans_serif.or(p.sans_serif.take());
489 p.monospace = monospace.or(p.monospace.take());
490 }
491
492 /// Load the page's fonts into `db`, as `load_system_fonts` loads the
493 /// system's: the files, then the generic families.
494 pub(crate) fn load_into(db: &mut Database) {
495 let p = PROVIDED.lock().unwrap();
496 for font in &p.files {
497 db.load_font_source(cosmic_text::fontdb::Source::Binary(font.clone()));
498 }
499 if let Some(f) = &p.serif {
500 db.set_serif_family(f.clone());
501 }
502 if let Some(f) = &p.sans_serif {
503 db.set_sans_serif_family(f.clone());
504 }
505 if let Some(f) = &p.monospace {
506 db.set_monospace_family(f.clone());
507 }
508 }
509
510 fn has(db: &Database, family: &str) -> bool {
511 db.faces().any(|f| f.families.iter().any(|(n, _)| n.eq_ignore_ascii_case(family)))
512 }
513
514 /// cosmic-text falls back from a family the database lacks through a
515 /// list of well-known families per OS — on Linux "Noto Sans", then
516 /// "DejaVu Sans", "FreeSans", … (its `fallback/unix.rs`) — and has no
517 /// list at all on wasm, where such text lands on whichever face happens
518 /// to come first (Noto Color Emoji, in a set loaded as Linux loads it).
519 /// So here the families the toolkit itself names — the configured
520 /// fonts, and the house default "Berkeley Mono" — are given, when absent,
521 /// the faces of the first family of that Linux list the page provided,
522 /// and the generic sans and serif families likewise unless the page named
523 /// them; "monospace" (the name `fc-match` would have resolved) is given
524 /// the first monospaced one.
525 /// Text then resolves to the face it would on a Linux box with the same
526 /// fonts. A family an app names itself must be among the page's fonts.
527 pub(crate) fn stand_in_for_missing(db: &mut Database) {
528 const SANS: [&str; 6] = ["Noto Sans", "DejaVu Sans", "FreeSans", "Noto Sans Mono", "DejaVu Sans Mono", "FreeMono"];
529 const MONO: [&str; 4] = ["Noto Sans Mono", "DejaVu Sans Mono", "FreeMono", "Liberation Mono"];
530 let first = |db: &Database, list: &[&str]| list.iter().find(|f| has(db, f)).map(|f| f.to_string());
531 let alias = |db: &mut Database, name: &str, to: &str| {
532 if name.is_empty() || has(db, name) {
533 return;
534 }
535 let faces: Vec<_> =
536 db.faces().filter(|f| f.families.iter().any(|(n, _)| n == to)).cloned().collect();
537 for mut face in faces {
538 face.families = vec![(name.to_string(), Language::English_UnitedStates)];
539 db.push_face_info(face);
540 }
541 };
542 if let Some(sans) = first(db, &SANS) {
543 let mut names: Vec<String> = vec!["Berkeley Mono".into()];
544 names.extend(
545 [
546 crate::layout::list_font_parsed().0,
547 crate::layout::menubar_font_parsed().0,
548 crate::layout::statusbar_font_parsed().0,
549 crate::layout::font_selector_font_parsed().0,
550 crate::layout::button_strip_font_parsed().0,
551 crate::layout::control_label_font_parsed().0,
552 crate::layout::control_label_font_detached_parsed().0,
553 crate::layout::tree_font_parsed().0,
554 crate::layout::graph_font_parsed().0,
555 crate::layout::graph_node_font_parsed().0,
556 ]
557 .into_iter()
558 .filter(|f| !matches!(f.as_str(), "sans-serif" | "serif" | "monospace")),
559 );
560 for name in &names {
561 alias(db, name, &sans);
562 }
563 for generic in [cosmic_text::Family::SansSerif, cosmic_text::Family::Serif] {
564 if !has(db, db.family_name(&generic)) {
565 match generic {
566 cosmic_text::Family::SansSerif => db.set_sans_serif_family(sans.clone()),
567 _ => db.set_serif_family(sans.clone()),
568 }
569 }
570 }
571 }
572 if let Some(mono) = first(db, &MONO) {
573 alias(db, "monospace", &mono);
574 }
575 }
576 }
577
578 #[cfg(test)]
579 mod locale_tests {
580 /// The font systems shape with the user's locale, not a literal: what
581 /// cosmic-text orders its fallback by (rfc-accessibility-locale, phase 0).
582 #[test]
583 fn every_font_system_is_built_with_the_users_locale() {
584 let want = crate::locale::locale();
585 assert_eq!(crate::create_font_system().locale(), want);
586 assert_eq!(crate::geometry_font_system().lock().unwrap().locale(), want);
587 }
588 }