ratatui_wgpu/
fonts.rs

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