Skip to main content

densa_text/
font.rs

1use std::fmt;
2use std::sync::atomic::{AtomicU32, Ordering};
3use std::sync::{Arc, OnceLock};
4
5use skrifa::instance::{LocationRef, Size};
6use skrifa::{FontRef, MetadataProvider};
7
8/// JetBrains Mono Regular, bundled under the SIL Open Font License 1.1 (`fonts/OFL.txt`).
9/// Monospace (every glyph advances 0.6 em), with accented Latin letters.
10pub const BUNDLED_MONO: &[u8] = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");
11
12/// Identifies one loaded [`Font`] for the lifetime of the process.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct FontId(u32);
15
16/// A glyph index within a font.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct GlyphId(pub u32);
19
20/// Vertical metrics of a font at a given size, in pixels (y up: `descent` is negative).
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct LineMetrics {
23    /// Distance from the baseline to the top of tall glyphs.
24    pub ascent: f32,
25    /// Distance from the baseline to the bottom of descenders; negative.
26    pub descent: f32,
27    /// Extra space the font recommends between lines.
28    pub line_gap: f32,
29}
30
31impl LineMetrics {
32    /// Baseline-to-baseline distance.
33    pub fn line_height(&self) -> f32 {
34        self.ascent - self.descent + self.line_gap
35    }
36}
37
38/// Why font data could not be loaded.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum FontError {
41    /// The bytes are not a readable OpenType/TrueType font.
42    Parse,
43}
44
45impl fmt::Display for FontError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Parse => f.write_str("the data is not a readable OpenType/TrueType font"),
49        }
50    }
51}
52
53impl std::error::Error for FontError {}
54
55static NEXT_FONT_ID: AtomicU32 = AtomicU32::new(1);
56
57/// Font file bytes shared without copying: a `Vec`, a `&'static [u8]`, or another library's
58/// shared buffer (such as a parley/fontique `Blob`).
59pub type FontBytes = Arc<dyn AsRef<[u8]> + Send + Sync>;
60
61/// One face of an OpenType/TrueType font file or collection. Cheap to clone (the bytes are
62/// shared); clones keep the same [`FontId`], so they share cached glyphs.
63#[derive(Clone)]
64pub struct Font {
65    data: FontBytes,
66    index: u32,
67    id: FontId,
68}
69
70impl fmt::Debug for Font {
71    // Hand-written: the shared bytes are opaque (and large).
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("Font")
74            .field("id", &self.id)
75            .field("index", &self.index)
76            .field("bytes", &self.bytes().len())
77            .finish()
78    }
79}
80
81impl Font {
82    /// Loads a font from its file bytes (face 0 of a collection).
83    ///
84    /// # Errors
85    ///
86    /// [`FontError::Parse`] if the bytes are not a readable font.
87    pub fn new(data: impl Into<Arc<[u8]>>) -> Result<Self, FontError> {
88        let data: Arc<[u8]> = data.into();
89        Self::from_shared(Arc::new(data), 0)
90    }
91
92    /// Face `index` of the font file or collection in `data`, sharing the bytes.
93    ///
94    /// Each call creates a new [`FontId`]: load a face once and clone the `Font`.
95    ///
96    /// # Errors
97    ///
98    /// [`FontError::Parse`] if the bytes are not a readable font or have no face `index`.
99    pub fn from_shared(data: FontBytes, index: u32) -> Result<Self, FontError> {
100        FontRef::from_index((*data).as_ref(), index).map_err(|_| FontError::Parse)?;
101        // ORDERING: Relaxed. Only uniqueness matters; no data is published through it.
102        let id = FontId(NEXT_FONT_ID.fetch_add(1, Ordering::Relaxed));
103        Ok(Self { data, index, id })
104    }
105
106    /// The bundled JetBrains Mono ([`BUNDLED_MONO`]). Every call returns the same font
107    /// (same [`FontId`]), so all users share its cached glyphs.
108    ///
109    /// # Errors
110    ///
111    /// Only if the bundled file were corrupt; tests guard against that.
112    pub fn bundled_mono() -> Result<Self, FontError> {
113        static BUNDLED: OnceLock<Result<Font, FontError>> = OnceLock::new();
114        BUNDLED
115            .get_or_init(|| Self::from_shared(Arc::new(BUNDLED_MONO), 0))
116            .clone()
117    }
118
119    /// This font's identity, used to key cached glyphs.
120    pub fn id(&self) -> FontId {
121        self.id
122    }
123
124    /// The face's index within its file (0 unless the file is a collection).
125    pub fn index(&self) -> u32 {
126        self.index
127    }
128
129    /// The whole font file.
130    pub fn bytes(&self) -> &[u8] {
131        (*self.data).as_ref()
132    }
133
134    /// The shared file bytes, for handing to another library without copying.
135    pub fn shared_bytes(&self) -> FontBytes {
136        Arc::clone(&self.data)
137    }
138
139    /// Parsed view of the font. Parsing reads table headers only: no allocation.
140    /// Cannot fail after construction succeeded on the same bytes; callers still degrade
141    /// gracefully instead of panicking.
142    pub(crate) fn font_ref(&self) -> Option<FontRef<'_>> {
143        FontRef::from_index(self.bytes(), self.index).ok()
144    }
145
146    /// The glyph the font draws for `ch`, or `None` if it has no glyph for it.
147    pub fn glyph_id(&self, ch: char) -> Option<GlyphId> {
148        let glyph = self.font_ref()?.charmap().map(ch)?;
149        // Glyph 0 is `.notdef`, the "missing glyph" box: treat it as unmapped.
150        (glyph.to_u32() != 0).then_some(GlyphId(glyph.to_u32()))
151    }
152
153    /// Horizontal advance of `glyph` at `px` pixels per em; 0 if unknown.
154    pub fn advance(&self, glyph: GlyphId, px: f32) -> f32 {
155        self.font_ref()
156            .and_then(|font| {
157                font.glyph_metrics(Size::new(px), LocationRef::default())
158                    .advance_width(skrifa::GlyphId::new(glyph.0))
159            })
160            .unwrap_or(0.0)
161    }
162
163    /// Width and height in pixels of `glyph`'s outline bounding box at `px` pixels per em,
164    /// without rasterizing it; `None` if the font cannot say (then the caller must assume the
165    /// worst). `Some([0.0, 0.0])` for glyphs without an outline.
166    pub fn glyph_extent(&self, glyph: GlyphId, px: f32) -> Option<[f32; 2]> {
167        let font = self.font_ref()?;
168        let metrics = font.glyph_metrics(Size::new(px), LocationRef::default());
169        let bounds = metrics.bounds(skrifa::GlyphId::new(glyph.0))?;
170        Some([
171            (bounds.x_max - bounds.x_min).max(0.0),
172            (bounds.y_max - bounds.y_min).max(0.0),
173        ])
174    }
175
176    /// Ascent, descent and line gap at `px` pixels per em.
177    pub fn line_metrics(&self, px: f32) -> LineMetrics {
178        let Some(font) = self.font_ref() else {
179            return LineMetrics {
180                ascent: 0.0,
181                descent: 0.0,
182                line_gap: 0.0,
183            };
184        };
185        let metrics = font.metrics(Size::new(px), LocationRef::default());
186        LineMetrics {
187            ascent: metrics.ascent,
188            descent: metrics.descent,
189            line_gap: metrics.leading,
190        }
191    }
192}