1#![no_std]
24#![warn(missing_docs)]
25#![warn(missing_debug_implementations)]
26#![warn(missing_copy_implementations)]
27#![warn(trivial_casts)]
28#![warn(trivial_numeric_casts)]
29#![deny(unsafe_code)]
30#![deny(unstable_features)]
31#![warn(unused_import_braces)]
32#![warn(unused_qualifications)]
33#![deny(rustdoc::broken_intra_doc_links)]
34#![deny(rustdoc::private_intra_doc_links)]
35
36use embedded_graphics::{
37 pixelcolor::{Gray8, Rgb888},
38 prelude::*,
39 primitives::Rectangle,
40 text::{
41 renderer::{CharacterStyle, TextMetrics, TextRenderer},
42 Alignment, Baseline,
43 },
44};
45use fontdue::layout::{Layout, TextStyle, WrapStyle};
46
47#[derive(Debug, Clone, Copy, Default)]
49pub enum VerticalAlign {
50 #[default]
51 Top,
53 Bottom,
55 Middle,
57}
58
59fn alpha_composite(background: Rgb888, foreground: Rgb888, alpha: u8) -> Rgb888 {
60 let (r1, g1, b1) = (
61 foreground.r() as u16,
62 foreground.g() as u16,
63 foreground.b() as u16,
64 );
65 let (r2, g2, b2) = (
66 background.r() as u16,
67 background.g() as u16,
68 background.b() as u16,
69 );
70
71 let alpha = alpha as u16;
72 let p = 255 - alpha;
73
74 Rgb888::new(
75 ((r1 * alpha + r2 * p) / 255) as u8,
76 ((g1 * alpha + g2 * p) / 255) as u8,
77 ((b1 * alpha + b2 * p) / 255) as u8,
78 )
79}
80
81fn inverse(col: Rgb888) -> Rgb888 {
82 let (r, g, b) = (col.r(), col.g(), col.b());
83 Rgb888::new(Rgb888::MAX_R - r, Rgb888::MAX_G - g, Rgb888::MAX_B - b)
84}
85
86#[derive(Debug, Clone, Copy)]
88pub struct FontdueTextStyle<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> {
89 pub font: &'a fontdue::Font,
91 pub color: C,
93 pub antialias_color: C,
95 pub size: u16,
97 pub max_width: Option<f32>,
99 pub max_height: Option<f32>,
101 pub horiz_align: Alignment,
103 pub vert_align_not_center: VerticalAlign,
105 pub line_height: f32,
107 pub word_wrap: bool,
109 pub wrap_hard_breaks: bool,
111}
112
113impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> FontdueTextStyle<'a, C> {
114 fn ascent(&self) -> u16 {
115 self.font
116 .horizontal_line_metrics(self.size as f32)
117 .unwrap()
118 .ascent as u16
119 }
120
121 fn descent(&self) -> u16 {
122 self.font
123 .horizontal_line_metrics(self.size as f32)
124 .unwrap()
125 .descent as u16
126 }
127
128 fn baseline_offset(&self, baseline: Baseline) -> i32 {
129 match baseline {
130 Baseline::Top => self.ascent().saturating_sub(1) as i32,
131 Baseline::Bottom => -(self.descent() as i32),
132 Baseline::Middle => (self.ascent() as i32 - self.descent() as i32) / 2,
133 Baseline::Alphabetic => 0,
134 }
135 }
136}
137
138impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> FontdueTextStyle<'a, C>
139where
140 Rgb888: From<C>,
141{
142 pub fn new(font: &'a fontdue::Font, color: C, size: u16) -> Self {
144 Self {
145 font,
146 color,
147 antialias_color: inverse(Rgb888::from(color)).into(),
148 size,
149 max_width: None,
150 max_height: None,
151 horiz_align: Alignment::Left,
152 vert_align_not_center: VerticalAlign::Top,
153 line_height: 1.0 * size as f32,
154 word_wrap: true,
155 wrap_hard_breaks: true,
156 }
157 }
158
159 pub fn with_aa_color(font: &'a fontdue::Font, color: C, aa_color: C, size: u16) -> Self {
161 Self {
162 font,
163 color,
164 antialias_color: aa_color,
165 size,
166 max_width: None,
167 max_height: None,
168 horiz_align: Alignment::Left,
169 vert_align_not_center: VerticalAlign::Top,
170 line_height: 1.0 * size as f32,
171 word_wrap: true,
172 wrap_hard_breaks: true,
173 }
174 }
175
176 pub fn render_glyph_at<D: DrawTarget<Color = C>>(
178 &self,
179 idx: u16,
180 x: f32,
181 y: f32,
182 target: &mut D,
183 ) -> Result<Point, D::Error> {
184 let (m, d) = self.font.rasterize_indexed(idx, self.size as f32);
185
186 let bbx = Rectangle::new(
187 Point {
188 x: x as i32,
189 y: y as i32,
190 },
191 Size {
192 width: m.width as u32,
193 height: m.height as u32,
194 },
195 );
196
197 let mut data_iter = d.iter();
198
199 let c8: Rgb888 = self.color.into();
200 let bc8: Rgb888 = self.antialias_color.into();
201
202 bbx.points()
203 .filter_map(|p| {
204 let l = *(data_iter.next()?);
205 if l != 0 {
206 Some(Pixel(p, alpha_composite(bc8, c8, l).into()))
207 } else {
208 None
209 }
210 })
211 .draw(target)?;
212
213 Ok(Point::new(m.advance_width as i32, m.advance_height as i32))
214 }
215
216 pub fn generate_layout(&self, text: &str, position: Point) -> Layout {
218 let mut layout = Layout::new(fontdue::layout::CoordinateSystem::PositiveYDown);
219 let settings = fontdue::layout::LayoutSettings {
220 x: position.x as f32,
221 y: position.y as f32,
222 line_height: self.line_height,
223 max_height: self.max_height,
224 max_width: self.max_width,
225 wrap_style: match self.word_wrap {
226 true => WrapStyle::Word,
227 false => WrapStyle::Letter,
228 },
229 wrap_hard_breaks: self.wrap_hard_breaks,
230 horizontal_align: match self.horiz_align {
231 Alignment::Center => fontdue::layout::HorizontalAlign::Center,
232 Alignment::Left => fontdue::layout::HorizontalAlign::Left,
233 Alignment::Right => fontdue::layout::HorizontalAlign::Right,
234 },
235 vertical_align: match self.vert_align_not_center {
236 VerticalAlign::Middle => fontdue::layout::VerticalAlign::Middle,
237 VerticalAlign::Top => fontdue::layout::VerticalAlign::Top,
238 VerticalAlign::Bottom => fontdue::layout::VerticalAlign::Bottom,
239 },
240 };
241
242 layout.reset(&settings);
243
244 layout.append(&[self.font], &TextStyle::new(text, self.size as f32, 0));
245
246 layout
247 }
248}
249
250impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> CharacterStyle
251 for FontdueTextStyle<'a, C>
252{
253 type Color = C;
254
255 fn set_text_color(&mut self, text_color: Option<C>) {
256 if let Some(color) = text_color {
258 self.color = color;
259 }
260 }
261
262 }
264
265impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> TextRenderer
266 for FontdueTextStyle<'a, C>
267where
268 Rgb888: From<C>,
269{
270 type Color = C;
271
272 fn draw_string<D>(
273 &self,
274 text: &str,
275 position: Point,
276 baseline: Baseline,
277 target: &mut D,
278 ) -> Result<Point, D::Error>
279 where
280 Rgb888: From<C>,
281 D: DrawTarget<Color = Self::Color>,
282 {
283 let mut position = position + Point::new(0, self.baseline_offset(baseline));
284 let layout = self.generate_layout(text, position);
285
286 for glyph in layout.glyphs() {
287 position += self.render_glyph_at(
288 glyph.key.glyph_index,
289 glyph.x,
290 glyph.y - (self.baseline_offset(Baseline::Middle) as f32 * 2.0),
291 target,
292 )?;
293 }
294
295 Ok(position)
296 }
297
298 fn draw_whitespace<D>(
299 &self,
300 width: u32,
301 position: Point,
302 baseline: Baseline,
303 _: &mut D,
304 ) -> Result<Point, D::Error>
305 where
306 Rgb888: From<C>,
307 D: DrawTarget<Color = Self::Color>,
308 {
309 let position = position + Point::new(0, self.baseline_offset(baseline));
310
311 Ok(position + Size::new(width, 0))
312 }
313
314 fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics {
315 let position = position + Point::new(0, self.baseline_offset(baseline));
316 let layout = self.generate_layout(text, position);
317
318 let mut dx = 0.0;
319 let mut dy = 0.0;
320 for met in layout.glyphs().iter().map(|g| {
321 self.font
322 .metrics_indexed(g.key.glyph_index, self.size as f32)
323 }) {
324 dy += met.advance_height;
325 dx += met.advance_width;
326 }
327
328 let bounding_box = Rectangle::new(
329 position - Size::new(0, self.ascent().saturating_sub(1) as u32 + (dy as u32)),
330 Size::new(dx as u32, self.line_height()),
331 );
332
333 TextMetrics {
334 bounding_box,
335 next_position: position + Size::new(dx as u32, 0),
336 }
337 }
338
339 fn line_height(&self) -> u32 {
340 self.line_height as u32
341 }
342}