elefont/lib.rs
1//! A library that handles caching rendered glyphs on the GPU
2//!
3//! This fits as a layer in your rendering pipeline between font rasterization and shaping and text
4//! rendering. In other words, first you turn a string into a series of font glyphs. Each of those
5//! glyphs is looked up against the cache, and if it hasn't been rendered, it is turned into a
6//! bitmap and uploaded to the GPU. The string is then laid out and rendered by the client
7//! application.
8//!
9//! Scope of this library:
10//! - DO support various font libraries / types of fonts (TTFs, bitmap fonts)
11//! - DO support whatever backend (rendering to an image, GPU frameworks, etc.)
12//! - DON'T handle complex tasks like shaping. The font stack should handle that elsewhere, and
13//! provide this library the glyphs to render
14//! - DON'T handle layout. This can be taken care of by the client
15//! application when rendering.
16//!
17//! Support is available out-of-the-box for software rendering via `image`, rendering via
18//! `rusttype`, and performing automatic unicode normalization. All of these are optional features.
19
20#![cfg_attr(not(feature = "std"), no_std)]
21extern crate alloc;
22
23#[cfg(feature = "image")]
24mod image_impl;
25#[cfg(feature = "rusttype")]
26pub mod rusttype_provider;
27
28#[cfg(not(feature = "unicode-normalization"))]
29use alloc::borrow::ToOwned;
30use alloc::boxed::Box;
31#[cfg(feature = "unicode-normalization")]
32use alloc::string::String;
33use alloc::vec::Vec;
34use hashbrown::HashMap;
35
36/// Any object that can turn characters into glyphs and render them can be a FontProvider
37///
38/// FontProviders can be TTF font rasters, like rusttype (a pure-Rust library for decoding fonts) or
39/// fontkit (a library that delegates to system APIs to handle fonts). Other FontProviders could
40/// include bitmap fonts, or a combination of libraries (like a library to handle shaping and
41/// another library to handle rendering.)
42///
43/// It is assumed that a given font provider will operate at a fixed size. For a variable-sized
44/// source (like a TTF font), the font size can be paired with the font data to produce a single
45/// FontProvider.
46pub trait FontProvider {
47 /// The format of the data generated by the FontProvider
48 fn pixel_type(&self) -> PixelType;
49 /// Convert a single character into a Glyph
50 ///
51 /// Generally you should use [`glyphs`], but when rendering just one character this method can
52 /// be useful
53 fn single_glyph(&self, character: char) -> Glyph;
54 /// Convert the string into glyphs, and push the glyphs into the provided buffer
55 ///
56 /// This is not necessarily the same as running `single_glyph` over every character in the
57 /// string! Text is hard.
58 fn glyphs(&self, string: &str, glyphs: &mut Vec<Glyph>);
59 /// How much space to include between baselines of the given font
60 fn line_height(&self) -> f32;
61 /// Get the metrics of a character (how to space it, where to include it on a line, etc.)
62 fn metrics(&self, glyph: Glyph) -> Metrics;
63 /// Convert a character into image bytes, with the format determined by [`pixel_type`]
64 ///
65 /// [`pixel_type`]: FontProvider::pixel_type
66 fn rasterize(&self, glpyh: Glyph) -> Result<Vec<u8>, CacheError>;
67 /// Optionally expose extra kerning information for glyphs
68 ///
69 /// By default, this is always 0.0. Some font providers may add more information here,
70 /// however.
71 fn kerning(&self, _a: Glyph, _b: Glyph) -> f32 {
72 0.0
73 }
74}
75
76/// Any object that can take the data for glyphs and store it over time
77///
78/// Textures can be image buffers on the CPU (like ones provided by the image crate) or a buffer
79/// on the GPU, through any graphics library.
80pub trait Texture {
81 fn width(&self) -> u32;
82 fn height(&self) -> u32;
83 /// Write the data from a font into a texture
84 fn put_rect(&mut self, pixel: PixelType, data: &[u8], gpu: &TextureGlyph);
85}
86
87/// The main structure for maintaing a cache of rendered glyphs
88///
89/// `FontCache` is specifically an intermediary step. It doesn't understand how to read font files
90/// or how to break up a string into glyphs: that's handled by the [`FontProvider`]. It doesn't
91/// handle sending glyphs to the GPU: if you want to do that, provide a [`Texture`] that stores its
92/// data on the GPU. What it does do is keep track of which glyphs have already been rendered, where
93/// they were stored, and provide a consistent API over a variety of ways of rendering characters.
94pub struct FontCache<T: Texture> {
95 glyph_buffer: Vec<Glyph>,
96 cache: Cache<T>,
97}
98
99struct Cache<T: Texture> {
100 font: Box<dyn FontProvider>,
101 texture: T,
102 map: HashMap<Glyph, TextureGlyph>,
103 h_cursor: u32,
104 v_cursor: u32,
105 current_line_height: u32,
106}
107
108impl<T: Texture> FontCache<T> {
109 /// Create a new FontCache that pulls from the given provider and renders to the provided
110 /// texture
111 pub fn new(font: Box<dyn FontProvider>, texture: T) -> Self {
112 FontCache {
113 glyph_buffer: Vec::new(),
114 cache: Cache {
115 font,
116 texture,
117 map: HashMap::new(),
118 h_cursor: 0,
119 v_cursor: 0,
120 current_line_height: 0,
121 },
122 }
123 }
124
125 /// Forget the position of the characters in the texture, and re-set the cursor.
126 ///
127 /// This doesn't set any data in the Texture! Old glyphs may continue to work, but this is akin
128 /// to a use-after-free.
129 pub fn clear(&mut self) {
130 self.cache.clear();
131 }
132
133 /// Render a glyph to the texture
134 pub fn render_glyph(&mut self, key: Glyph) -> Result<(Metrics, TextureGlyph), CacheError> {
135 self.cache.render_glyph(key)
136 }
137
138 /// Attempt to convert a string into a series of glyphs or errors
139 ///
140 /// Before being converted, the string is normalized if the "unicode-normalilzation" feature is
141 /// activated, and whitespace characters are removed.
142 pub fn render_string<'a>(
143 &'a mut self,
144 string: &str,
145 ) -> impl 'a + Iterator<Item = Result<(Metrics, TextureGlyph), CacheError>> {
146 #[cfg(feature = "unicode-normalization")]
147 let mut string = {
148 use unicode_normalization::UnicodeNormalization;
149 string.nfc().collect::<String>()
150 };
151 #[cfg(not(feature = "unicode-normalization"))]
152 let mut string = string.to_owned();
153 string.retain(|c| !c.is_whitespace());
154 let glyph_buffer = &mut self.glyph_buffer;
155 let cache = &mut self.cache;
156 cache.font.glyphs(&string, glyph_buffer);
157 glyph_buffer
158 .drain(..)
159 .map(move |glyph| cache.render_glyph(glyph))
160 }
161
162 /// Cache a string or return an error if one occurred
163 ///
164 /// This can be useful if the entire domain of the possible glyphs is known beforehand (like a
165 /// bitmap font.) Under the hood, this just calls [`render_string`] and ignores the returned
166 /// glyphs.
167 pub fn cache_string(&mut self, string: &str) -> Result<(), CacheError> {
168 self.render_string(string).map(|r| r.map(|_| ())).collect()
169 }
170
171 /// Swap out the internal texture for another one
172 ///
173 /// This will clear the cache automatically, to avoid holding references to invalid areas of
174 /// the texture
175 pub fn replace_texture(&mut self, mut texture: T) -> T {
176 self.clear();
177 core::mem::swap(&mut self.cache.texture, &mut texture);
178
179 texture
180 }
181
182 pub fn texture(&self) -> &T {
183 &self.cache.texture
184 }
185
186 pub fn font(&self) -> &dyn FontProvider {
187 self.cache.font.as_ref()
188 }
189}
190
191impl<T: Texture> Cache<T> {
192 fn clear(&mut self) {
193 self.map.clear();
194 self.h_cursor = 0;
195 self.v_cursor = 0;
196 self.current_line_height = 0;
197 }
198
199 fn render_glyph(&mut self, glyph: Glyph) -> Result<(Metrics, TextureGlyph), CacheError> {
200 if let Some(tex_glyph) = self.map.get(&glyph) {
201 return Ok((self.font.metrics(glyph), *tex_glyph));
202 }
203 let metrics = self.font.metrics(glyph);
204 let bounds = metrics.bounds.unwrap();
205 if bounds.width > self.texture.width() || bounds.height > self.texture.height() {
206 return Err(CacheError::TextureTooSmall);
207 }
208 if bounds.width + self.h_cursor > self.texture.width() {
209 self.h_cursor = 0;
210 self.v_cursor += self.current_line_height + 1;
211 self.current_line_height = 0;
212 }
213 if bounds.height + self.v_cursor > self.texture.height() {
214 return Err(CacheError::OutOfSpace);
215 }
216 let pixel_type = self.font.pixel_type();
217 let data = self.font.rasterize(glyph)?;
218 let gpu = TextureGlyph {
219 glyph,
220 bounds: Bounds {
221 x: self.h_cursor as i32,
222 y: self.v_cursor as i32,
223 width: bounds.width,
224 height: bounds.height,
225 },
226 };
227 self.texture.put_rect(pixel_type, &data[..], &gpu);
228 self.h_cursor += gpu.bounds.width + 1;
229 self.current_line_height = self.current_line_height.max(gpu.bounds.height);
230 self.map.insert(glyph, gpu);
231
232 Ok((self.font.metrics(glyph), gpu))
233 }
234}
235
236/// The index of the font character to render
237///
238/// Glyphs are what actually gets rendered to the screen. It might be tempting to think of a glyph
239/// like a 'rendered character.' In specific scripts, this is often the case. 'A' and 'a' have
240/// distinct glyphs, and are unconditionally the same glyph. In others, this might not be true. See
241/// ['Text Rendering Hates You'](https://gankra.github.io/blah/text-hates-you) for more information
242/// on why text is complicated.
243#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
244pub struct Glyph(pub u32);
245
246/// The relevant information for a glyph stored on the texture
247#[derive(Copy, Clone, Debug)]
248pub struct TextureGlyph {
249 pub glyph: Glyph,
250 pub bounds: Bounds,
251}
252
253/// The layout information for a glyph
254#[non_exhaustive]
255#[derive(Clone, Debug)]
256pub struct Metrics {
257 pub bounds: Option<Bounds>,
258 pub bearing_x: f32,
259 pub advance_x: f32,
260 pub bearing_y: f32,
261 pub advance_y: f32,
262}
263
264#[derive(Copy, Clone, Debug)]
265pub struct Bounds {
266 pub x: i32,
267 pub y: i32,
268 pub width: u32,
269 pub height: u32,
270}
271
272/// An error generated during a cache operation
273#[derive(Copy, Clone, Debug)]
274pub enum CacheError {
275 /// No matter what, the texture is too small to render the glyph (even when empty)
276 ///
277 /// To fix this error, expand the texture. Make sure to clear the cache if the texture data is
278 /// also invalidated
279 TextureTooSmall,
280 /// The cache cannot store the current request without clearing it first
281 OutOfSpace,
282 /// A glyph was passed to a render method but it could not be rendered
283 ///
284 /// For example, unsized glyphs (glyphs with None for their [`bounds`]) cannot be rendered
285 ///
286 /// [`bounds`]: Metrics::bounds
287 NonRenderableGlyph(Glyph),
288}
289
290#[cfg(feature = "std")]
291use std::{error::Error, fmt};
292
293#[cfg(feature = "std")]
294impl fmt::Display for CacheError {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 match self {
297 CacheError::TextureTooSmall => {
298 write!(f, "The texture is too small to render the given input")
299 }
300 CacheError::OutOfSpace => write!(
301 f,
302 "The cache is out of space, and must be cleared before more rendering"
303 ),
304 CacheError::NonRenderableGlyph(glyph) => {
305 write!(f, "Attempted to render an un-renderable glyph: {:?}", glyph)
306 }
307 }
308 }
309}
310
311#[cfg(feature = "std")]
312impl Error for CacheError {
313 fn source(&self) -> Option<&(dyn Error + 'static)> {
314 None
315 }
316}
317
318/// How the pixels of the rasterized font are represented
319pub enum PixelType {
320 /// A series of values representing the alpha with no associated color
321 Alpha,
322 /// A series of complete color values
323 RGBA,
324}