Skip to main content

max / shop

3.6 KB · 80 lines History Blame Raw
1 //! Asking the system which font has a character the bundled one does not.
2 //!
3 //! Shop bundles one font so that a fresh install has its box drawing and its
4 //! house marks without a font install first, and that font is Quasi Mono:
5 //! Latin, Greek, Cyrillic, box drawing and block elements in full, and no Han,
6 //! kana or hangul at all.
7 //!
8 //! Rather than pick a second font ourselves and ship it, ask the machine. On
9 //! Linux fontconfig is the thing that already knows, and already carries the
10 //! user's own preferences and per-language rules, so a system that has been
11 //! told to prefer a particular CJK face gets that face here too. Shop is
12 //! Wayland-only, so fontconfig is present wherever shop runs, and it is
13 //! consulted only when the bundled font misses, which for ordinary output is
14 //! never.
15 //!
16 //! **What this path is for is scripts, and it is a poor answer for symbols.**
17 //! `Shaper::baseline` takes the line off the primary face alone, deliberately,
18 //! so a fallback face with its own metrics sits its glyphs on a different line.
19 //! For a run of Han that is right; for a box-drawing character it would draw
20 //! borders off the line, which is why the whole cell tier is in the face rather
21 //! than left to fallback. Two blocks are outside it and now arrive this way:
22 //! braille (U+2800) and the legacy-computing sextants, which IosevkaTerm
23 //! carried and Quasi Mono does not. Nothing in the tree emits either - there is
24 //! no `Canvas` and no `Sparkline` anywhere - and the day one does, they are
25 //! generated recipes in the house set rather than a fallback.
26
27 use fontconfig::{CharSet, Fontconfig, Pattern};
28
29 /// A font file the system offered, and which face inside it.
30 pub(crate) struct Match {
31 pub(crate) path: String,
32 pub(crate) index: i32,
33 }
34
35 pub(crate) struct Fallback {
36 /// `None` when fontconfig could not initialise. Every lookup then misses,
37 /// which is the behaviour shop had before this existed: tofu, not a crash.
38 fc: Option<Fontconfig>,
39 }
40
41 impl Fallback {
42 pub(crate) fn new() -> Self {
43 let fc = Fontconfig::new();
44 if fc.is_none() {
45 tracing::warn!("fontconfig unavailable; characters outside the bundled font draw tofu");
46 }
47 Self { fc }
48 }
49
50 /// The font the system would use for `c`, or `None` if nothing has it.
51 ///
52 /// Asks by CHARACTER rather than by family: a charset of one is what makes
53 /// this "who has this glyph" instead of "give me a font I named", which is
54 /// the whole point of deferring to the system.
55 pub(crate) fn find(&self, c: char) -> Option<Match> {
56 let fc = self.fc.as_ref()?;
57 let mut charset = CharSet::new(fc).ok()?;
58 charset.add_char(c).ok()?;
59
60 let mut pat = Pattern::new(fc).ok()?;
61 pat.add_charset(charset).ok()?;
62 // Rank monospace faces first. Not a filter — fontconfig always returns
63 // its best match — but it is the difference between "Noto Sans CJK JP"
64 // and "Noto Sans Mono CJK JP", and only the second one is built to sit
65 // in a grid of cells.
66 pat.add_integer(c"spacing", 100).ok()?;
67 // The user's fontconfig rules, then the built-in defaults. Skipping
68 // these would be asking the library while ignoring its configuration,
69 // which is most of what makes the answer the system's rather than ours.
70 pat.config_substitute().ok()?;
71 pat.default_substitute();
72
73 let found = pat.font_match().ok()?;
74 Some(Match {
75 path: found.filename().ok()?.to_string(),
76 index: found.face_index().unwrap_or(0),
77 })
78 }
79 }
80