//! Asking the system which font has a character the bundled one does not. //! //! Shop bundles one font so that a fresh install has its box drawing and its //! house marks without a font install first, and that font is Quasi Mono: //! Latin, Greek, Cyrillic, box drawing and block elements in full, and no Han, //! kana or hangul at all. //! //! Rather than pick a second font ourselves and ship it, ask the machine. On //! Linux fontconfig is the thing that already knows, and already carries the //! user's own preferences and per-language rules, so a system that has been //! told to prefer a particular CJK face gets that face here too. Shop is //! Wayland-only, so fontconfig is present wherever shop runs, and it is //! consulted only when the bundled font misses, which for ordinary output is //! never. //! //! **What this path is for is scripts, and it is a poor answer for symbols.** //! `Shaper::baseline` takes the line off the primary face alone, deliberately, //! so a fallback face with its own metrics sits its glyphs on a different line. //! For a run of Han that is right; for a box-drawing character it would draw //! borders off the line, which is why the whole cell tier is in the face rather //! than left to fallback. Two blocks are outside it and now arrive this way: //! braille (U+2800) and the legacy-computing sextants, which IosevkaTerm //! carried and Quasi Mono does not. Nothing in the tree emits either - there is //! no `Canvas` and no `Sparkline` anywhere - and the day one does, they are //! generated recipes in the house set rather than a fallback. use fontconfig::{CharSet, Fontconfig, Pattern}; /// A font file the system offered, and which face inside it. pub(crate) struct Match { pub(crate) path: String, pub(crate) index: i32, } pub(crate) struct Fallback { /// `None` when fontconfig could not initialise. Every lookup then misses, /// which is the behaviour shop had before this existed: tofu, not a crash. fc: Option, } impl Fallback { pub(crate) fn new() -> Self { let fc = Fontconfig::new(); if fc.is_none() { tracing::warn!("fontconfig unavailable; characters outside the bundled font draw tofu"); } Self { fc } } /// The font the system would use for `c`, or `None` if nothing has it. /// /// Asks by CHARACTER rather than by family: a charset of one is what makes /// this "who has this glyph" instead of "give me a font I named", which is /// the whole point of deferring to the system. pub(crate) fn find(&self, c: char) -> Option { let fc = self.fc.as_ref()?; let mut charset = CharSet::new(fc).ok()?; charset.add_char(c).ok()?; let mut pat = Pattern::new(fc).ok()?; pat.add_charset(charset).ok()?; // Rank monospace faces first. Not a filter — fontconfig always returns // its best match — but it is the difference between "Noto Sans CJK JP" // and "Noto Sans Mono CJK JP", and only the second one is built to sit // in a grid of cells. pat.add_integer(c"spacing", 100).ok()?; // The user's fontconfig rules, then the built-in defaults. Skipping // these would be asking the library while ignoring its configuration, // which is most of what makes the answer the system's rather than ours. pat.config_substitute().ok()?; pat.default_substitute(); let found = pat.font_match().ok()?; Some(Match { path: found.filename().ok()?.to_string(), index: found.face_index().unwrap_or(0), }) } }