git.lucas.co / cce-ui
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 }