fontdue_sdl2/
lib.rs

1//! # fontdue-sdl2
2//!
3//! This crate is glue code for rendering text with [sdl2][sdl2],
4//! rasterized and laid out by [fontdue][fontdue].
5//!
6//! # Usage
7//!
8//! First, set up fontdue and layout some glyphs with the [`Color`]
9//! included as user data:
10//!
11//! ```
12//! # use fontdue::{Font, layout::{Layout, TextStyle, CoordinateSystem}};
13//! # use fontdue_sdl2::FontTexture;
14//! # use sdl2::pixels::Color;
15//! let font = include_bytes!("../examples/roboto/Roboto-Regular.ttf") as &[u8];
16//! let roboto_regular = Font::from_bytes(font, Default::default()).unwrap();
17//! let fonts = &[roboto_regular];
18//! let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
19//! layout.append(fonts, &TextStyle::with_user_data(
20//!     "Hello, World!",           // The text to lay out
21//!     32.0,                      // The font size
22//!     0,                         // The font index (Roboto Regular)
23//!     Color::RGB(0xFF, 0xFF, 0)  // The color of the text
24//! ));
25//! ```
26//!
27//! Then draw them using a [`FontTexture`]. It needs a
28//! [`TextureCreator`], just as any SDL [`Texture`].
29//!
30//! ```
31//! # use fontdue::{Font, layout::{Layout, TextStyle, CoordinateSystem}};
32//! # use fontdue_sdl2::FontTexture;
33//! # use sdl2::pixels::Color;
34//! # let sdl_context = sdl2::init().unwrap();
35//! # let video_subsystem = sdl_context.video().unwrap();
36//! # let window = video_subsystem.window("fontdue-sdl2 example", 800, 600).position_centered().build().unwrap();
37//! # let mut canvas = window.into_canvas().build().unwrap();
38//! # let texture_creator = canvas.texture_creator();
39//! # let font = include_bytes!("../examples/roboto/Roboto-Regular.ttf") as &[u8];
40//! # let roboto_regular = Font::from_bytes(font, Default::default()).unwrap();
41//! # let fonts = &[roboto_regular];
42//! # let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
43//! # layout.append(fonts, &TextStyle::with_user_data(
44//! #     "Hello, World!",           // The text to lay out
45//! #     32.0,                      // The font size
46//! #     0,                         // The font index (Roboto Regular)
47//! #     Color::RGB(0xFF, 0xFF, 0)  // The color of the text
48//! # ));
49//! # canvas.clear();
50//! let mut font_texture = FontTexture::new(&texture_creator).unwrap();
51//! let _ = font_texture.draw_text(&mut canvas, fonts, layout.glyphs());
52//! # canvas.present();
53//! ```
54//!
55//! Note that drawing text can fail if there are issues with the
56//! rendering setup. It's often valid to simply ignore, or crash.
57//!
58//! The [`FontTexture`] is intended to be created once, at the
59//! beginning of your program, and then used throughout. Generally,
60//! you should treat [`FontTexture`] similarly to the [`Font`]-slice
61//! passed to fontdue, and associate each [`FontTexture`] with a
62//! specific `&[Font]`. See the [`FontTexture`] documentation for more
63//! information.
64//!
65//! See `examples/basic.rs` for a complete example program.
66//!
67//! [fontdue]: https://docs.rs/fontdue
68//! [sdl2]: https://docs.rs/sdl2
69
70use fontdue::layout::GlyphPosition;
71use fontdue::Font;
72use sdl2::pixels::{Color, PixelFormatEnum};
73use sdl2::rect::Rect;
74use sdl2::render::{BlendMode, Canvas, RenderTarget, Texture, TextureCreator};
75
76#[cfg(not(feature = "unsafe_textures"))]
77mod public_api;
78
79#[cfg(feature = "unsafe_textures")]
80mod public_api_no_lifetimes;
81#[allow(clippy::unsafe_removed_from_name)]
82#[cfg(feature = "unsafe_textures")]
83use public_api_no_lifetimes as public_api;
84
85mod rect_allocator;
86use rect_allocator::{CacheReservation, RectAllocator};
87
88pub use public_api::FontTexture;
89pub use fontdue;
90pub use sdl2;
91
92/// Called by [FontTexture::new].
93pub(crate) fn create_font_texture<T>(
94    texture_creator: &TextureCreator<T>,
95) -> Result<Texture, String> {
96    use sdl2::render::TextureValueError::*;
97    let mut texture = match texture_creator.create_texture_streaming(
98        Some(PixelFormatEnum::RGBA32), // = the pixels are always [r, g, b, a] when read as u8's.
99        1024,
100        1024,
101    ) {
102        Ok(t) => t,
103        Err(WidthOverflows(_))
104        | Err(HeightOverflows(_))
105        | Err(WidthMustBeMultipleOfTwoForFormat(_, _)) => {
106            unreachable!()
107        }
108        Err(SdlError(s)) => return Err(s),
109    };
110    texture.set_blend_mode(BlendMode::Blend);
111    Ok(texture)
112}
113
114/// Called by [FontTexture::draw_text].
115fn draw_text<RT: RenderTarget>(
116    font_texture: &mut Texture,
117    rect_allocator: &mut RectAllocator,
118    canvas: &mut Canvas<RT>,
119    fonts: &[Font],
120    glyphs: &[GlyphPosition<Color>],
121) -> Result<(), String> {
122    struct RenderableGlyph {
123        texture_rect: Rect,
124        canvas_rect: Rect,
125    }
126    struct MissingGlyph {
127        color: Color,
128        canvas_rect: Rect,
129    }
130
131    let mut result_glyphs = Vec::with_capacity(glyphs.len());
132    let mut missing_glyphs = Vec::new();
133
134    for glyph in glyphs.iter().filter(|glyph| glyph.width * glyph.height > 0) {
135        let canvas_rect = Rect::new(
136            glyph.x as i32,
137            glyph.y as i32,
138            glyph.width as u32,
139            glyph.height as u32,
140        );
141        let color = glyph.user_data;
142
143        match rect_allocator.get_rect_in_texture(*glyph) {
144            CacheReservation::AlreadyRasterized(texture_rect) => {
145                result_glyphs.push(RenderableGlyph {
146                    texture_rect,
147                    canvas_rect,
148                });
149            }
150            CacheReservation::EmptySpace(texture_rect) => {
151                let (metrics, pixels) = fonts[glyph.font_index].rasterize_config(glyph.key);
152
153                let mut full_color_pixels = Vec::with_capacity(pixels.len());
154                for coverage in pixels {
155                    full_color_pixels.push(color.r);
156                    full_color_pixels.push(color.g);
157                    full_color_pixels.push(color.b);
158                    full_color_pixels.push(coverage);
159                }
160                font_texture
161                    .update(texture_rect, &full_color_pixels, metrics.width * 4)
162                    .map_err(|err| format!("{}", err))?;
163
164                result_glyphs.push(RenderableGlyph {
165                    texture_rect,
166                    canvas_rect,
167                });
168            }
169            CacheReservation::OutOfSpace => {
170                log::error!(
171                    "Glyph cache cannot fit '{}' (size {}, font index {})",
172                    glyph.parent,
173                    glyph.key.px,
174                    glyph.font_index,
175                );
176                missing_glyphs.push(MissingGlyph { color, canvas_rect });
177            }
178        }
179    }
180
181    for glyph in result_glyphs {
182        canvas.copy(font_texture, glyph.texture_rect, glyph.canvas_rect)?;
183    }
184
185    let previous_color = canvas.draw_color();
186    for glyph in missing_glyphs {
187        canvas.set_draw_color(glyph.color);
188        let _ = canvas.draw_rect(glyph.canvas_rect);
189    }
190    canvas.set_draw_color(previous_color);
191
192    Ok(())
193}