Skip to main content

densa_paragraph/
system.rs

1use std::borrow::Cow;
2
3use densa_core::Color;
4use densa_text::{Font, FontBytes, FontError};
5use parley::fontique::{
6    Blob, Collection, CollectionOptions, FallbackKey, FamilyId, GenericFamily, Language, Script,
7    SourceCache,
8};
9use parley::{FontContext, FontData, FontFamily, LayoutContext, LineHeight, StyleProperty};
10use rustc_hash::FxHashMap;
11
12use crate::paragraph::{bytes_key, face_key, FaceKey, Paragraph};
13use crate::style::{
14    check_font_size, check_line_height, check_span, check_width, ParagraphError, ParagraphStyle,
15    Span,
16};
17
18/// Generic families the bundled font stands in for when system fonts are off.
19const GENERICS: [GenericFamily; 9] = [
20    GenericFamily::Serif,
21    GenericFamily::SansSerif,
22    GenericFamily::Monospace,
23    GenericFamily::SystemUi,
24    GenericFamily::UiSerif,
25    GenericFamily::UiSansSerif,
26    GenericFamily::UiMonospace,
27    GenericFamily::UiRounded,
28    GenericFamily::Emoji,
29];
30
31/// Generic families that stay the bundled font with system fonts on, so turning them on only
32/// adds characters: text the bundled font could draw looks the same.
33const MONOSPACE: [GenericFamily; 2] = [GenericFamily::Monospace, GenericFamily::UiMonospace];
34
35/// Han locales whose system fallback fonts are tried, in order, after the default one.
36const HAN_LOCALES: [&str; 4] = ["zh-TW", "zh-HK", "ja", "ko"];
37
38/// Simplified Chinese families tried last. fontique's Windows backend answers the default Han
39/// key (which is simplified Chinese) with Yu Gothic UI, a Japanese font without many
40/// simplified forms; these fill them in on Windows, macOS and Linux.
41const SIMPLIFIED_CHINESE: [&str; 5] = [
42    "Microsoft YaHei UI",
43    "Microsoft YaHei",
44    "PingFang SC",
45    "Noto Sans CJK SC",
46    "Source Han Sans SC",
47];
48
49/// Makes the default Han fallback a chain of every locale's font, so a Han character any
50/// installed CJK font has is drawn: no single font has the characters of every locale, and
51/// fontique gives each script and locale one. Parley also falls back to this chain for
52/// punctuation in every script.
53fn chain_han_fallbacks(collection: &mut Collection) {
54    let han = Script::from_bytes(*b"Hani");
55    let mut chain: Vec<FamilyId> = collection
56        .fallback_families(FallbackKey::new(han, None))
57        .collect();
58    let add = |chain: &mut Vec<FamilyId>, family: FamilyId| {
59        if !chain.contains(&family) {
60            chain.push(family);
61        }
62    };
63    for tag in HAN_LOCALES {
64        let Ok(locale) = Language::parse(tag) else {
65            continue;
66        };
67        let families: Vec<FamilyId> = collection
68            .fallback_families(FallbackKey::new(han, Some(&locale)))
69            .collect();
70        for family in families {
71            add(&mut chain, family);
72        }
73    }
74    for name in SIMPLIFIED_CHINESE {
75        if let Some(family) = collection.family_id(name) {
76            add(&mut chain, family);
77        }
78    }
79    collection.set_fallbacks(FallbackKey::new(han, None), chain.into_iter());
80}
81
82/// Fonts plus layout scratch space: builds [`Paragraph`]s.
83///
84/// Create one per application (or per UI thread) and keep it: it caches font data and reuses
85/// layout buffers. Every face a paragraph uses becomes one `densa_text::Font`, so all
86/// paragraphs (and the bundled font's other users, such as the grid) share cached glyphs.
87/// Faces stay loaded for the system's lifetime (system CJK fonts can be tens of MB each).
88pub struct TextSystem {
89    fonts: FontContext,
90    layouts: LayoutContext<Color>,
91    /// Every face any paragraph used, kept (with its font file) for the system's lifetime.
92    faces: FxHashMap<FaceKey, Font>,
93}
94
95impl std::fmt::Debug for TextSystem {
96    // Hand-written: parley's contexts do not implement Debug.
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("TextSystem")
99            .field("faces", &self.faces.len())
100            .finish_non_exhaustive()
101    }
102}
103
104impl TextSystem {
105    /// The text system applications want: the bundled JetBrains Mono for `monospace` (the
106    /// default family), and the operating system's fonts for other families and for every
107    /// character the bundled font lacks (Korean, Japanese, Chinese, symbols, ...).
108    ///
109    /// Without the `system-fonts` feature (on by default) this is [`bundled`](Self::bundled).
110    ///
111    /// # Errors
112    /// Only if the bundled font were corrupt.
113    pub fn new() -> Result<Self, FontError> {
114        Self::build(cfg!(feature = "system-fonts"))
115    }
116
117    /// A text system with only the bundled JetBrains Mono, which also stands in for every
118    /// generic family (`monospace`, `sans-serif`, ...): the same text lays out the same on
119    /// every machine, for tests and screenshots. Characters it lacks are drawn as its
120    /// missing-glyph box; add fonts with [`register_font`](Self::register_font).
121    ///
122    /// # Errors
123    /// Only if the bundled font were corrupt.
124    pub fn bundled() -> Result<Self, FontError> {
125        Self::build(false)
126    }
127
128    fn build(system_fonts: bool) -> Result<Self, FontError> {
129        let collection = Collection::new(CollectionOptions {
130            shared: false,
131            system_fonts,
132        });
133        let mut fonts = FontContext {
134            collection,
135            source_cache: SourceCache::default(),
136        };
137        let bundled = Font::bundled_mono()?;
138        let blob = Blob::new(bundled.shared_bytes());
139        let registered = fonts.collection.register_fonts(blob, None);
140        let Some(&(family, _)) = registered.first() else {
141            return Err(FontError::Parse);
142        };
143        let generics: &[GenericFamily] = if system_fonts { &MONOSPACE } else { &GENERICS };
144        for &generic in generics {
145            fonts
146                .collection
147                .set_generic_families(generic, std::iter::once(family));
148        }
149        if system_fonts {
150            chain_han_fallbacks(&mut fonts.collection);
151        }
152        let mut faces = FxHashMap::default();
153        // The bundled font keeps its process-wide identity, shared with its other users.
154        faces.insert(bytes_key(bundled.bytes(), 0), bundled);
155        Ok(Self {
156            fonts,
157            layouts: LayoutContext::new(),
158            faces,
159        })
160    }
161
162    /// Adds the fonts in `data` (a font file or collection); returns their family names.
163    ///
164    /// # Errors
165    /// [`FontError::Parse`] if `data` holds no readable font.
166    pub fn register_font(&mut self, data: FontBytes) -> Result<Vec<String>, FontError> {
167        let registered = self.fonts.collection.register_fonts(Blob::new(data), None);
168        if registered.is_empty() {
169            return Err(FontError::Parse);
170        }
171        let mut names = Vec::with_capacity(registered.len());
172        for (family, _) in &registered {
173            // `family_name` borrows the collection mutably: copy each name out.
174            if let Some(name) = self.fonts.collection.family_name(*family) {
175                names.push(name.to_owned());
176            }
177        }
178        Ok(names)
179    }
180
181    /// Shapes `text` with `style`, `spans` overriding parts of it, then breaks and aligns it.
182    ///
183    /// Allocates (shaping is not a per-frame operation); drawing the result does not.
184    ///
185    /// # Errors
186    /// [`ParagraphError`] for a size, line height or width out of range, a span that does not
187    /// lie on character boundaries within `text`, or a font that cannot be read.
188    pub fn paragraph(
189        &mut self,
190        text: &str,
191        style: &ParagraphStyle,
192        spans: &[Span],
193    ) -> Result<Paragraph, ParagraphError> {
194        check_font_size(style.size_px)?;
195        check_line_height(style.line_height)?;
196        let max_width = check_width(style.max_width)?;
197        for span in spans {
198            check_span(text, span)?;
199        }
200
201        let mut builder = self
202            .layouts
203            .ranged_builder(&mut self.fonts, text, 1.0, true);
204        let family = FontFamily::Source(Cow::Borrowed(&style.family));
205        builder.push_default(StyleProperty::FontFamily(family));
206        builder.push_default(StyleProperty::FontSize(style.size_px));
207        let line_height = LineHeight::FontSizeRelative(style.line_height);
208        builder.push_default(StyleProperty::LineHeight(line_height));
209        builder.push_default(StyleProperty::Brush(style.color));
210        for span in spans {
211            if let Some(color) = span.color {
212                builder.push(StyleProperty::Brush(color), span.range.clone());
213            }
214            if let Some(size) = span.size_px {
215                builder.push(StyleProperty::FontSize(size), span.range.clone());
216            }
217        }
218        let layout = builder.build(text);
219
220        let faces = &mut self.faces;
221        let mut resolve = |data: &FontData| -> Option<Font> {
222            let key = face_key(data);
223            if let Some(font) = faces.get(&key) {
224                return Some(font.clone());
225            }
226            let (bytes, _) = data.data.clone().into_raw_parts();
227            let font = Font::from_shared(bytes, data.index).ok()?;
228            faces.insert(key, font.clone());
229            Some(font)
230        };
231        Paragraph::new(layout, text.len(), style.align, max_width, &mut resolve)
232    }
233}