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}