Skip to main content

ratatui_wgpu/
fonts.rs

1use std::hash::BuildHasher;
2use std::hash::Hasher;
3use std::hash::RandomState;
4
5use ratatui_core::buffer::Cell;
6use ratatui_core::style::Modifier;
7use rustybuzz::Face;
8
9/// A Font which can be used for rendering.
10#[derive(Clone)]
11pub struct Font<'a> {
12    font: Face<'a>,
13    advance: f32,
14    id: u64,
15}
16
17impl<'a> Font<'a> {
18    /// Create a new Font from data. Returns [`None`] if the font cannot
19    /// be parsed.
20    pub fn new(data: &'a [u8]) -> Option<Self> {
21        let mut hasher = RandomState::new().build_hasher();
22        hasher.write(data);
23
24        Face::from_slice(data, 0).map(|font| {
25            let advance = font
26                .glyph_hor_advance(font.glyph_index('m').unwrap_or_default())
27                .unwrap_or_default() as f32;
28            Self {
29                font,
30                advance,
31                id: hasher.finish(),
32            }
33        })
34    }
35}
36
37impl Font<'_> {
38    pub(crate) fn id(&self) -> u64 {
39        self.id
40    }
41
42    pub(crate) fn font(&'_ self) -> &'_ Face<'_> {
43        &self.font
44    }
45
46    pub(crate) fn char_width(
47        &self,
48        height_px: u32,
49    ) -> u32 {
50        let scale = height_px as f32 / self.font.height() as f32;
51        (self.advance * scale) as u32
52    }
53}
54
55/// A collection of fonts to use for rendering. Supports font fallback.
56///
57/// It is recommended, but not required, that all fonts have the same/very
58/// similar aspect ratio, or you may get unexpected results during rendering due
59/// to fallback.
60pub struct Fonts<'a> {
61    char_width: u32,
62    char_height: u32,
63
64    last_resort: Font<'a>,
65
66    regular: Vec<Font<'a>>,
67    bold: Vec<Font<'a>>,
68    italic: Vec<Font<'a>>,
69    bold_italic: Vec<Font<'a>>,
70}
71
72impl<'a> Fonts<'a> {
73    /// Create a new, empty set of fonts. The provided font will be used as a
74    /// last-resort fallback if no other fonts can render a particular
75    /// character. Rendering will attempt to fake bold/italic styles using this
76    /// font where appropriate.
77    ///
78    /// The provided size_px will be the rendered height in pixels of all fonts
79    /// in this collection.
80    pub fn new(
81        font: Font<'a>,
82        size_px: u32,
83    ) -> Self {
84        Self {
85            char_width: font.char_width(size_px),
86            char_height: size_px,
87            last_resort: font,
88            regular: vec![],
89            bold: vec![],
90            italic: vec![],
91            bold_italic: vec![],
92        }
93    }
94
95    /// The height (in pixels) of all fonts.
96    #[inline]
97    pub fn height_px(&self) -> u32 {
98        self.char_height
99    }
100
101    /// Change the height of all fonts in this collection to the specified
102    /// height in pixels.
103    pub fn set_size_px(
104        &mut self,
105        height_px: u32,
106    ) {
107        self.char_height = height_px;
108
109        self.char_width = std::iter::once(&self.last_resort)
110            .chain(self.regular.iter())
111            .chain(self.bold.iter())
112            .chain(self.italic.iter())
113            .chain(self.bold_italic.iter())
114            .map(|font| font.char_width(height_px))
115            .min()
116            .unwrap_or_default();
117    }
118
119    /// Add a collection of fonts for various styles. They will automatically be
120    /// added to the appropriate fallback font list based on the font's
121    /// bold/italic properties. Note that this will automatically organize fonts
122    /// by relative width in order to optimize fallback rendering quality. The
123    /// ordering of already provided fonts will remain unchanged.
124    pub fn add_fonts(
125        &mut self,
126        fonts: impl IntoIterator<Item = Font<'a>>,
127    ) {
128        let bold_italic_len = self.bold_italic.len();
129        let italic_len = self.italic.len();
130        let bold_len = self.bold.len();
131        let regular_len = self.regular.len();
132
133        for font in fonts {
134            if !font.font().is_monospaced() {
135                warn!("Non monospace font used in add_fonts, this may cause unexpected rendering.");
136            }
137
138            self.char_width = self.char_width.min(font.char_width(self.char_height));
139            if font.font().is_italic() && font.font().is_bold() {
140                self.bold_italic.push(font);
141            } else if font.font().is_italic() {
142                self.italic.push(font);
143            } else if font.font().is_bold() {
144                self.bold.push(font);
145            } else {
146                self.regular.push(font);
147            }
148        }
149
150        self.bold_italic[bold_italic_len..].sort_by_key(|font| font.char_width(self.char_height));
151        self.italic[italic_len..].sort_by_key(|font| font.char_width(self.char_height));
152        self.bold[bold_len..].sort_by_key(|font| font.char_width(self.char_height));
153        self.regular[regular_len..].sort_by_key(|font| font.char_width(self.char_height));
154    }
155
156    /// Add a new collection of fonts for regular styled text. These fonts will
157    /// come _after_ previously provided fonts in the fallback order.
158    pub fn add_regular_fonts(
159        &mut self,
160        fonts: impl IntoIterator<Item = Font<'a>>,
161    ) {
162        self.char_width = self.char_width.min(Self::add_fonts_internal(
163            &mut self.regular,
164            fonts,
165            self.char_height,
166        ));
167    }
168
169    /// Add a new collection of fonts for bold styled text. These fonts will
170    /// come _after_ previously provided fonts in the fallback order.
171    ///
172    /// You do not have to provide these for bold text to be supported. If no
173    /// bold fonts are supplied, rendering will fallback to the regular fonts
174    /// with fake bolding.
175    pub fn add_bold_fonts(
176        &mut self,
177        fonts: impl IntoIterator<Item = Font<'a>>,
178    ) {
179        self.char_width = self.char_width.min(Self::add_fonts_internal(
180            &mut self.bold,
181            fonts,
182            self.char_height,
183        ));
184    }
185
186    /// Add a new collection of fonts for italic styled text. These fonts will
187    /// come _after_ previously provided fonts in the fallback order.
188    ///
189    /// It is recommended, but not required, that you provide italic fonts if
190    /// your application intends to make use of italics. If no italic fonts
191    /// are supplied, rendering will fallback to the regular fonts with fake
192    /// italics.
193    pub fn add_italic_fonts(
194        &mut self,
195        fonts: impl IntoIterator<Item = Font<'a>>,
196    ) {
197        self.char_width = self.char_width.min(Self::add_fonts_internal(
198            &mut self.italic,
199            fonts,
200            self.char_height,
201        ));
202    }
203
204    /// Add a new collection of fonts for bold italic styled text. These fonts
205    /// will come _after_ previously provided fonts in the fallback order.
206    ///
207    /// You do not have to provide these for bold text to be supported. If no
208    /// bold fonts are supplied, rendering will fallback to the italic fonts
209    /// with fake bolding.
210    pub fn add_bold_italic_fonts(
211        &mut self,
212        fonts: impl IntoIterator<Item = Font<'a>>,
213    ) {
214        self.char_width = self.char_width.min(Self::add_fonts_internal(
215            &mut self.bold_italic,
216            fonts,
217            self.char_height,
218        ));
219    }
220}
221
222impl<'a> Fonts<'a> {
223    /// The minimum width (in pixels) across all fonts.
224    pub(crate) fn min_width_px(&self) -> u32 {
225        self.char_width
226    }
227
228    pub(crate) fn count(&self) -> usize {
229        1 + self.bold.len() + self.italic.len() + self.bold_italic.len() + self.regular.len()
230    }
231
232    pub(crate) fn font_for_cell(
233        &'_ self,
234        cell: &Cell,
235    ) -> (&'_ Font<'_>, bool, bool) {
236        if cell.modifier.contains(Modifier::BOLD | Modifier::ITALIC) {
237            self.select_font(
238                cell.symbol(),
239                self.bold_italic
240                    .iter()
241                    .map(|f| (f, false, false))
242                    .chain(self.italic.iter().map(|f| (f, true, false)))
243                    .chain(self.bold.iter().map(|f| (f, false, true)))
244                    .chain(self.regular.iter().map(|f| (f, true, true))),
245                true,
246                true,
247            )
248        } else if cell.modifier.contains(Modifier::BOLD) {
249            self.select_font(
250                cell.symbol(),
251                self.bold
252                    .iter()
253                    .map(|f| (f, false, false))
254                    .chain(self.regular.iter().map(|f| (f, true, false))),
255                true,
256                false,
257            )
258        } else if cell.modifier.contains(Modifier::ITALIC) {
259            self.select_font(
260                cell.symbol(),
261                self.italic
262                    .iter()
263                    .map(|f| (f, false, false))
264                    .chain(self.regular.iter().map(|f| (f, false, true))),
265                false,
266                true,
267            )
268        } else {
269            self.select_font(
270                cell.symbol(),
271                self.regular.iter().map(|f| (f, false, false)),
272                false,
273                false,
274            )
275        }
276    }
277
278    fn select_font<'fonts>(
279        &'fonts self,
280        cluster: &str,
281        fonts: impl IntoIterator<Item = (&'fonts Font<'a>, bool, bool)>,
282        last_resort_fake_bold: bool,
283        last_resort_fake_italic: bool,
284    ) -> (&'fonts Font<'a>, bool, bool) {
285        let mut max = 0;
286        let mut font = None;
287        for (candidate, fake_bold, fake_italic) in fonts.into_iter().chain(std::iter::once((
288            &self.last_resort,
289            last_resort_fake_bold,
290            last_resort_fake_italic,
291        ))) {
292            let (count, last_idx) =
293                cluster
294                    .chars()
295                    .enumerate()
296                    .fold((0, 0), |(mut count, _), (idx, ch)| {
297                        count += usize::from(candidate.font().glyph_index(ch).is_some());
298                        (count, idx)
299                    });
300            if count > max {
301                max = count;
302                font = Some((candidate, fake_bold, fake_italic));
303            }
304
305            if count == last_idx + 1 {
306                break;
307            }
308        }
309
310        *font.get_or_insert((
311            &self.last_resort,
312            last_resort_fake_bold,
313            last_resort_fake_italic,
314        ))
315    }
316
317    fn add_fonts_internal(
318        target: &mut Vec<Font<'a>>,
319        fonts: impl IntoIterator<Item = Font<'a>>,
320        char_height: u32,
321    ) -> u32 {
322        let len = target.len();
323        target.extend(fonts);
324
325        target[len..]
326            .iter()
327            .map(|font| font.char_width(char_height))
328            .min()
329            .unwrap_or(u32::MAX)
330    }
331}