Skip to main content

smudge_text/
system.rs

1//! Per-thread ownership of parley's font and layout contexts.
2//!
3//! parley's contexts are not `Send`, so each thread owns one system in a
4//! thread-local. Widgets reach it through [`TextSystem::with`], which creates
5//! a system on first use and reuses it afterwards.
6//!
7//! A system created by [`TextSystem::bundled_only`] sees the embedded fonts
8//! and nothing else, which is what tests and wasm build on. A system created
9//! by [`TextSystem::with_system_fonts`] sees the fonts installed on the host
10//! as well. A generic family such as sans serif then resolves to the host's
11//! choices first and to the bundled face last, while the default text style
12//! names the bundled family directly, so it never depends on the host.
13
14use std::cell::RefCell;
15
16/// The font and layout contexts a thread lays text out with.
17///
18/// One system per thread keeps parley's non-`Send` contexts out of shared
19/// state. The system is inert state: creating, replacing or dropping one
20/// touches nothing but this thread.
21pub struct TextSystem {
22    /// The registered fonts and the queries against them.
23    pub(crate) fonts: parley::FontContext,
24    /// Scratch space reused across layouts.
25    pub(crate) layouts: parley::LayoutContext<()>,
26}
27
28impl TextSystem {
29    /// Creates a system that sees only the bundled fonts.
30    ///
31    /// Without the `bundled-fonts` feature the system starts empty and every
32    /// glyph lays out as missing.
33    pub fn bundled_only() -> TextSystem {
34        let collection = fontique::Collection::new(fontique::CollectionOptions {
35            shared: false,
36            system_fonts: false,
37        });
38        let source_cache =
39            fontique::SourceCache::new(fontique::SourceCacheOptions { shared: false });
40
41        let mut system = TextSystem {
42            fonts: parley::FontContext {
43                collection,
44                source_cache,
45            },
46            layouts: parley::LayoutContext::new(),
47        };
48        system.register_bundled_fonts();
49        system
50    }
51
52    /// Creates a system that sees the system fonts plus the bundled ones as a
53    /// fallback.
54    ///
55    /// System fonts are discovered only when the `system-fonts` feature is
56    /// enabled, which is impossible on wasm. Without it this behaves like
57    /// [`TextSystem::bundled_only`].
58    pub fn with_system_fonts() -> TextSystem {
59        let mut system = TextSystem {
60            fonts: parley::FontContext::default(),
61            layouts: parley::LayoutContext::new(),
62        };
63        system.register_bundled_fonts();
64        system
65    }
66
67    /// Installs this system for the current thread, replacing whatever the
68    /// thread owned before.
69    ///
70    /// # Panics
71    ///
72    /// Panics if the thread's system is in use, because a system cannot be
73    /// replaced while a layout borrows it.
74    pub fn install(self) {
75        CURRENT.with(|slot| {
76            let mut slot = slot.try_borrow_mut().expect(
77                "the thread's text system cannot be replaced while it is in \
78                 use",
79            );
80            *slot = Some(self);
81        });
82    }
83
84    /// Runs `f` with the current thread's system, creating one on first use:
85    /// [`TextSystem::with_system_fonts`] on native targets and
86    /// [`TextSystem::bundled_only`] on wasm.
87    ///
88    /// # Panics
89    ///
90    /// Panics if called from inside another `with` call on the same thread,
91    /// for example from a [`crate::TextLayout`] constructor, because a system
92    /// cannot be handed out twice at once.
93    pub fn with<R>(f: impl FnOnce(&mut TextSystem) -> R) -> R {
94        CURRENT.with(|slot| {
95            let mut slot = slot.try_borrow_mut().expect(
96                "TextSystem::with cannot run inside another with call on the \
97                 same thread",
98            );
99            let system = slot.get_or_insert_with(default_for_platform);
100            f(system)
101        })
102    }
103
104    /// Registers the bundled faces and makes each one the last choice of its
105    /// generic family, so that text without an explicit family still reaches
106    /// them when the host offers nothing else.
107    #[cfg(feature = "bundled-fonts")]
108    fn register_bundled_fonts(&mut self) {
109        use std::sync::Arc;
110
111        let register = |system: &mut TextSystem, data: &'static [u8]| {
112            let blob = fontique::Blob::new(Arc::new(data));
113            system.fonts.collection.register_fonts(blob, None)
114        };
115
116        let sans = register(self, crate::fonts::SANS_REGULAR);
117        let sans_bold = register(self, crate::fonts::SANS_BOLD);
118        let mono = register(self, crate::fonts::MONO_REGULAR);
119
120        let sans_family = sans
121            .first()
122            .map(|(family, _)| *family)
123            .expect("a font file yields at least one family");
124        let sans_bold_family = sans_bold
125            .first()
126            .map(|(family, _)| *family)
127            .expect("a font file yields at least one family");
128        debug_assert_eq!(
129            sans_family, sans_bold_family,
130            "the bundled sans faces share a family name"
131        );
132        let mono_family = mono
133            .first()
134            .map(|(family, _)| *family)
135            .expect("a font file yields at least one family");
136
137        self.fall_back_to(fontique::GenericFamily::SansSerif, sans_family);
138        self.fall_back_to(fontique::GenericFamily::SystemUi, sans_family);
139        self.fall_back_to(fontique::GenericFamily::Monospace, mono_family);
140    }
141
142    /// Puts `family` at the end of the families `generic` resolves to, unless
143    /// it is already among them.
144    ///
145    /// The collection lists registered families ahead of the system's own
146    /// choices, so appending would put the bundled face first. Rebuilding the
147    /// list keeps the system's order and makes the bundled face its fallback.
148    #[cfg(feature = "bundled-fonts")]
149    fn fall_back_to(&mut self, generic: fontique::GenericFamily, family: fontique::FamilyId) {
150        let collection = &mut self.fonts.collection;
151        let mut families: Vec<_> = collection.generic_families(generic).collect();
152        if !families.contains(&family) {
153            families.push(family);
154        }
155        collection.set_generic_families(generic, families.into_iter());
156    }
157
158    /// Keeps the generic families from resolving to anything when no fonts
159    /// are bundled.
160    #[cfg(not(feature = "bundled-fonts"))]
161    fn register_bundled_fonts(&mut self) {}
162}
163
164thread_local! {
165    /// The system this thread owns, if it has one already.
166    static CURRENT: RefCell<Option<TextSystem>> = const { RefCell::new(None) };
167}
168
169/// The system a thread creates when nothing was installed.
170fn default_for_platform() -> TextSystem {
171    #[cfg(target_arch = "wasm32")]
172    {
173        TextSystem::bundled_only()
174    }
175    #[cfg(not(target_arch = "wasm32"))]
176    {
177        TextSystem::with_system_fonts()
178    }
179}
180
181#[cfg(test)]
182#[path = "system_tests.rs"]
183mod tests;