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;