sext/
lib.rs

1pub mod colours;
2
3use crate::colours::TextColour;
4use fontdue::layout::GlyphPosition;
5use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
6use fontdue::Font;
7use fontdue::FontSettings;
8use std::collections::HashMap;
9use std::sync::Arc;
10use log::debug;
11
12/// The main text renderer struct, which holds a single font and its cache.
13/// Try not to clone this as it may end up containing a large amount of data.
14/// Instead, you might want to wrap this in an `Arc` or some other pointer type.
15#[derive(Clone)]
16pub struct TextRenderer<T> {
17    pub font: Arc<Font>,
18    pub layout: Arc<Layout>,
19    glyph_caches: HashMap<u16, GlyphCache<T>>,
20}
21
22/// Internal struct, contains a `HashMap` of `TextColour` to a `HashMap` of `char` to (raw glyph data, `DrawableSurface`).
23/// This is because, historically as SDL2 surfaces were used, it was important to keep the raw glyph data alive so that
24/// less memory copying was required for SDL2 surfaces. It is thus recommended that you do not copy the raw glyph data,
25/// and instead attempt to borrow it within your `DrawableSurface` implementation. (which we didn't do in our test implementation cause we were lazy)
26#[derive(Clone)]
27#[allow(dead_code)] // listen i'll use it at some point okay!
28struct GlyphCache<T> {
29    pub size: f32,
30    pub surface_map: HashMap<TextColour, HashMap<char, (Vec<u8>, T)>>,
31}
32
33/// A "surface" that you can draw pixels to.
34/// Historically, this was an SDL2 surface, but it has been abstracted out to allow for other backends.
35pub trait DrawableSurface {
36    /// This function will be called to "paste" a glyph upon the surface.
37    /// The `x` and `y` coordinates are where the top left of the glyph should be pasted.
38    /// The `width` and `height` are the dimensions of the area that the glyph should be rendered.
39    /// KEEP IN MIND THAT THIS MAY NOT BE THE SAME AS THE ACTUAL GLYPH DIMENSIONS.
40    /// `data` is in reference to another `DrawableSurface` that contains the glyph data.
41    fn paste(&mut self, x: usize, y: usize, width: usize, height: usize, data: &Self);
42    /// This function takes in raw RGBA bytes and creates a `DrawableSurface` from them.
43    /// The `width` and `height` are the dimensions of the surface.
44    /// The `data` parameter is a slice of bytes that contains the RGBA data.
45    /// The `colour` parameter is a `TextColour` that will be used to colour the surface.
46    /// There is little reason to actually care about the `colour` parameter, as it is only used for caching.
47    /// Check the tests section of this library for an example of how to use this function.
48    fn from_raw_mask(width: usize, height: usize, data: &[u8], colour: TextColour) -> Self;
49}
50
51/// Enum for the different (1) possible errors that you could get while constructing a TextRenderer.
52#[derive(Debug, Clone, Copy)]
53pub enum TextRendererError {
54    FontNotFound,
55}
56
57/// Internal function to convert the fontdue grayscale bitmaps to our superior RGBA bitmaps
58fn cache_glyph<T>(font: Arc<Font>, glyph: GlyphPosition, colour: TextColour, make_t: impl FnOnce(&[u8]) -> T) -> (Vec<u8>, T) {
59    debug!("caching glyph: {:?}", glyph);
60    let (_metrics, mut bitmap) = font.rasterize_config(glyph.key);
61    let mut coloured_pixels = Vec::new();
62    for pixel in bitmap.iter_mut() {
63        coloured_pixels.push(colour.r); // u8
64        coloured_pixels.push(colour.g); // u8
65        coloured_pixels.push(colour.b); // u8
66        coloured_pixels.push(*pixel); // u8
67    }
68    // create T from bitmap
69    let t = make_t(&coloured_pixels);
70    (coloured_pixels, t)
71}
72
73impl<T> TextRenderer<T> where T: DrawableSurface, T: Clone {
74    /// Loads a font from a specified path and creates a `TextRenderer` instance.
75    /// Will return `TextRendererError::FontNotFound` if the font could not be found.
76    /// Will also return a `TextRendererError::FontNotFound` if the font could not be loaded, because i haven't added other errors yet.
77    pub fn load(font_path: &str) -> Result<Self, TextRendererError> {
78        let font_data = std::fs::read(font_path).map_err(|_| TextRendererError::FontNotFound)?;
79        let font = Font::from_bytes(font_data, FontSettings::default())
80            .map_err(|_| TextRendererError::FontNotFound)?;
81        let layout = Layout::new(CoordinateSystem::PositiveYDown);
82        Ok(TextRenderer {
83            font: Arc::new(font),
84            layout: Arc::new(layout),
85            glyph_caches: HashMap::new(),
86        })
87    }
88
89    /// Same as `draw_string`, but forces each character to be rendered at the same width.
90    /// This can cause some minor visual artifacts, but is useful for some cases where i'm lazy.
91    /// Notable warning: this will currently cause each character to have a kerning of 0.
92    pub fn draw_string_monospaced(
93        &mut self,
94        string: &str,
95        x: f32,
96        y: f32,
97        size: f32,
98        colour: TextColour,
99        surface: &mut T
100    ) {
101        let mut layout_settings = LayoutSettings::default();
102        layout_settings.x = x;
103        layout_settings.y = y;
104        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
105        layout.reset(&layout_settings);
106        layout.append(&[self.font.clone()], &TextStyle::new(string, size, 0));
107        let glyphs = layout.glyphs();
108        for (glyph, i) in glyphs.iter().zip(0..) {
109            let bitmap = self.get_glyph_surface(*glyph, glyph.width, glyph.height, colour);
110            // draw to surface
111            surface.paste(
112                (x + (size / 2.0) * i as f32) as usize,
113                (y + glyph.y) as usize,
114                (size / 2.0) as usize,
115                glyph.height as usize,
116                &bitmap,
117            );
118        }
119    }
120
121    /// Draws a string using the default settings and fontdue's layout engine.
122    /// In the future, this will probably have added systems for typesetting, but for now you'll have
123    /// to live without being able to set the kerning of your text.
124    pub fn draw_string(
125        &mut self,
126        string: &str,
127        x: f32,
128        y: f32,
129        size: f32,
130        colour: TextColour,
131        surface: &mut T
132    ) {
133        let mut layout_settings = LayoutSettings::default();
134        layout_settings.x = x;
135        layout_settings.y = y;
136        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
137        layout.reset(&layout_settings);
138        layout.append(&[self.font.clone()], &TextStyle::new(string, size, 0));
139        let glyphs = layout.glyphs();
140        for glyph in glyphs.iter() {
141            let bitmap = self.get_glyph_surface(*glyph, glyph.width, glyph.height, colour);
142            // draw to surface
143            surface.paste(
144                (x + glyph.x) as usize,
145                (y + glyph.y) as usize,
146                glyph.width as usize,
147                glyph.height as usize,
148                &bitmap,
149            );
150        }
151    }
152
153    /// Internal function to get the glyph drawable from either the cache or the font
154    fn get_glyph_surface(
155        &mut self,
156        glpyh: GlyphPosition,
157        width: usize,
158        height: usize,
159        colour: TextColour,
160    ) -> T {
161        let size = height as u16;
162        // check if glyph cache exists
163        // if not create it
164        self.glyph_caches.entry(size).or_insert(GlyphCache {
165            size: size as f32,
166            surface_map: HashMap::new(),
167        });
168        // get glyph cache
169        // check if colour exists
170        // if not create it
171        let glyph_cache = self.glyph_caches.get_mut(&size).unwrap();
172        glyph_cache.surface_map.entry(colour).or_insert_with(|| HashMap::new());
173        // get colour map
174        // check if glyph exists
175        // if not create it
176        let colour_map = glyph_cache.surface_map.get_mut(&colour).unwrap();
177        if let std::collections::hash_map::Entry::Vacant(e) = colour_map.entry(glpyh.parent) {
178            e.insert(cache_glyph(self.font.clone(), glpyh, colour, |data| T::from_raw_mask(width, height, data, colour)));
179        }
180        // get glyph surface
181        let glyph_surface = colour_map.get(&glpyh.parent).unwrap();
182        // return glyph surface
183        glyph_surface.1.clone()
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use std::io::Write;
190    use super::*;
191
192    #[derive(Debug, Clone)]
193    struct TestSurface {
194        width: usize,
195        height: usize,
196        data: Vec<u8>,
197    }
198
199    impl DrawableSurface for TestSurface {
200        fn paste(&mut self, x: usize, y: usize, width: usize, height: usize, data: &Self) {
201            println!("paste: x: {}, y: {}, width: {}, height: {}, data: {:?}", x, y, width, height, data);
202            // data contains an rgba bitmap
203            let data_pitch = data.width as i32 * 4;
204            let pitch = self.width as i32 * 4;
205            let mut data_index = 0i32;
206            let mut index = (y as i32 * pitch) + (x as i32 * 4);
207            // WIDTH AND DATA WIDTH ARE DIFFERENT
208            for _ in 0..height {
209                for _ in 0..width {
210                    // if we're out of bounds on either surface, skip
211                    if index < 0 || index >= (self.width * self.height * 4) as i32 || data_index < 0 || data_index >= (data.width * data.height * 4) as i32 {
212                        data_index += 4;
213                        index += 4;
214                        continue;
215                    }
216                    self.data[index as usize] = data.data[data_index as usize];
217                    self.data[index as usize + 1] = data.data[data_index as usize + 1];
218                    self.data[index as usize + 2] = data.data[data_index as usize + 2];
219                    self.data[index as usize + 3] = data.data[data_index as usize + 3];
220                    data_index += 4;
221                    index += 4;
222                }
223                index += pitch - (width as i32 * 4);
224                data_index += data_pitch - (width as i32 * 4);
225            }
226        }
227        // data is rgba
228        fn from_raw_mask(width: usize, height: usize, data: &[u8], colour: TextColour) -> Self {
229            println!("from_raw_mask");
230            println!("width: {}", width);
231            println!("height: {}", height);
232            println!("data: {:?}", data);
233            TestSurface {
234                width,
235                height,
236                data: data.to_vec(),
237            }
238        }
239    }
240
241    #[test]
242    fn test_text_renderer() {
243        let mut renderer = TextRenderer::load("FreeMono.ttf").unwrap();
244        let mut surface = TestSurface {
245            width: 256,
246            height: 256,
247            data: vec![0; 256 * 256 * 4],
248        };
249        renderer.draw_string_monospaced("hElLo w0r1d!", 0.0, 0.0, 24.0, TextColour::new_rgb(255, 255, 255), &mut surface);
250        renderer.draw_string("hElLo w0r1d!", 0.0, 24.0, 24.0, TextColour::new_rgb(255, 255, 255), &mut surface);
251        // convert from rgba to rgb
252        let mut rgb_data = Vec::new();
253        for i in 0..(surface.width * surface.height) {
254            // if transparent, put black; otherwise, put the pixel
255            if surface.data[i * 4 + 3] == 0 {
256                rgb_data.push(0);
257                rgb_data.push(0);
258                rgb_data.push(0);
259            } else {
260                rgb_data.push(surface.data[i * 4]);
261                rgb_data.push(surface.data[i * 4 + 1]);
262                rgb_data.push(surface.data[i * 4 + 2]);
263            }
264        }
265        // output to bitmap
266        let mut file = std::fs::File::create("test.ppm").unwrap();
267        let _ = file.write(format!("P6\n{} {}\n255\n", surface.width, surface.height).as_bytes()).unwrap();
268        let _ = file.write(&rgb_data).unwrap();
269    }
270}