1pub(crate) mod cursor;
4pub(crate) mod line;
5pub(crate) mod line_iter;
6pub(crate) mod space_config;
7
8use crate::{
9 parser::Parser,
10 plugin::{PluginMarker as Plugin, ProcessingState},
11 rendering::{
12 cursor::Cursor,
13 line::{LineRenderState, StyledLineRenderer},
14 },
15 style::TextBoxStyle,
16 TextBox,
17};
18use az::SaturatingAs;
19use embedded_graphics::{
20 draw_target::{DrawTarget, DrawTargetExt},
21 prelude::{Dimensions, Point, Size},
22 primitives::Rectangle,
23 text::renderer::{CharacterStyle, TextRenderer},
24 Drawable,
25};
26use line_iter::LineEndType;
27
28#[derive(Clone)]
32pub struct TextBoxProperties<'a, S> {
33 pub box_style: &'a TextBoxStyle,
35
36 pub char_style: &'a S,
38
39 pub text_height: i32,
41
42 pub bounding_box: Rectangle,
44}
45
46impl<'a, F, M> Drawable for TextBox<'a, F, M>
47where
48 F: TextRenderer<Color = <F as CharacterStyle>::Color> + CharacterStyle,
49 M: Plugin<'a, <F as TextRenderer>::Color> + Plugin<'a, <F as CharacterStyle>::Color>,
50{
51 type Color = <F as CharacterStyle>::Color;
52 type Output = &'a str;
53
54 #[inline]
55 fn draw<D: DrawTarget<Color = Self::Color>>(
56 &self,
57 display: &mut D,
58 ) -> Result<&'a str, D::Error> {
59 let mut cursor = Cursor::new(
60 self.bounds,
61 self.character_style.line_height(),
62 self.style.line_height,
63 self.style.tab_size.into_pixels(&self.character_style),
64 );
65
66 let text_height = self
67 .style
68 .measure_text_height_impl(
69 self.plugin.clone(),
70 &self.character_style,
71 self.text,
72 cursor.line_width(),
73 )
74 .saturating_as::<i32>();
75
76 let box_height = self.bounding_box().size.height.saturating_as::<i32>();
77
78 self.style.vertical_alignment.apply_vertical_alignment(
79 &mut cursor,
80 text_height,
81 box_height,
82 );
83
84 cursor.y += self.vertical_offset;
85
86 let props = TextBoxProperties {
87 box_style: &self.style,
88 char_style: &self.character_style,
89 text_height,
90 bounding_box: self.bounding_box(),
91 };
92
93 self.plugin.on_start_render(&mut cursor, props);
94
95 let mut state = LineRenderState {
96 text_renderer: self.character_style.clone(),
97 parser: Parser::parse(self.text),
98 end_type: LineEndType::EndOfText,
99 plugin: &self.plugin,
100 };
101
102 state.plugin.set_state(ProcessingState::Render);
103
104 let mut anything_drawn = false;
105 loop {
106 state.plugin.new_line();
107
108 let display_range = self
109 .style
110 .height_mode
111 .calculate_displayed_row_range(&cursor);
112 let display_range_start = display_range.start.saturating_as::<i32>();
113 let display_range_count = display_range.count() as u32;
114 let display_size = Size::new(cursor.line_width(), display_range_count);
115
116 let line_start = cursor.line_start();
117
118 let mut display = display.clipped(&Rectangle::new(
121 line_start + Point::new(0, display_range_start),
122 display_size,
123 ));
124 if display_range_count == 0 {
125 if anything_drawn {
127 let remaining_bytes = state.parser.as_str().len();
129 let consumed_bytes = self.text.len() - remaining_bytes;
130
131 state.plugin.post_render(
132 &mut display,
133 &self.character_style,
134 None,
135 Rectangle::new(line_start, Size::new(0, cursor.line_height())),
136 )?;
137 state.plugin.on_rendering_finished();
138 return Ok(self.text.get(consumed_bytes..).unwrap());
139 }
140 } else {
141 anything_drawn = true;
142 }
143
144 StyledLineRenderer {
145 cursor: cursor.line(),
146 state: &mut state,
147 style: &self.style,
148 }
149 .draw(&mut display)?;
150
151 match state.end_type {
152 LineEndType::EndOfText => {
153 state.plugin.on_rendering_finished();
154 break;
155 }
156 LineEndType::CarriageReturn => {}
157 _ => {
158 cursor.new_line();
159
160 if state.end_type == LineEndType::NewLine {
161 cursor.y += self.style.paragraph_spacing.saturating_as::<i32>();
162 }
163 }
164 }
165 }
166
167 Ok("")
168 }
169}
170
171#[cfg(test)]
172pub mod test {
173 use embedded_graphics::{
174 mock_display::MockDisplay,
175 mono_font::{
176 ascii::{FONT_6X10, FONT_6X9},
177 MonoTextStyleBuilder,
178 },
179 pixelcolor::BinaryColor,
180 prelude::*,
181 primitives::Rectangle,
182 };
183
184 use crate::{
185 alignment::HorizontalAlignment,
186 style::{HeightMode, TextBoxStyle, TextBoxStyleBuilder, VerticalOverdraw},
187 utils::test::{size_for, TestFont},
188 TextBox,
189 };
190
191 #[track_caller]
192 pub fn assert_rendered(
193 alignment: HorizontalAlignment,
194 text: &str,
195 size: Size,
196 pattern: &[&str],
197 ) {
198 assert_styled_rendered(
199 TextBoxStyleBuilder::new().alignment(alignment).build(),
200 text,
201 size,
202 pattern,
203 );
204 }
205
206 #[track_caller]
207 pub fn assert_styled_rendered(style: TextBoxStyle, text: &str, size: Size, pattern: &[&str]) {
208 let mut display = MockDisplay::new();
209
210 let character_style = MonoTextStyleBuilder::new()
211 .font(&FONT_6X9)
212 .text_color(BinaryColor::On)
213 .background_color(BinaryColor::Off)
214 .build();
215
216 TextBox::with_textbox_style(
217 text,
218 Rectangle::new(Point::zero(), size),
219 character_style,
220 style,
221 )
222 .draw(&mut display)
223 .unwrap();
224
225 display.assert_pattern(pattern);
226 }
227
228 #[test]
229 fn nbsp_doesnt_break() {
230 assert_rendered(
231 HorizontalAlignment::Left,
232 "a b c\u{a0}d e f",
233 size_for(&FONT_6X9, 5, 3),
234 &[
235 ".................. ",
236 ".............#.... ",
237 ".............#.... ",
238 "..###........###.. ",
239 ".#..#........#..#. ",
240 ".#..#........#..#. ",
241 "..###........###.. ",
242 ".................. ",
243 ".................. ",
244 "..............................",
245 "................#.............",
246 "................#.............",
247 "..###.........###.........##..",
248 ".#...........#..#........#.##.",
249 ".#...........#..#........##...",
250 "..###.........###.........###.",
251 "..............................",
252 "..............................",
253 "...... ",
254 "...#.. ",
255 "..#.#. ",
256 "..#... ",
257 ".###.. ",
258 "..#... ",
259 "..#... ",
260 "...... ",
261 "...... ",
262 ],
263 );
264 }
265
266 #[test]
267 fn vertical_offset() {
268 let mut display = MockDisplay::new();
269
270 let character_style = MonoTextStyleBuilder::new()
271 .font(&FONT_6X9)
272 .text_color(BinaryColor::On)
273 .background_color(BinaryColor::Off)
274 .build();
275
276 TextBox::new(
277 "hello",
278 Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
279 character_style,
280 )
281 .set_vertical_offset(6)
282 .draw(&mut display)
283 .unwrap();
284
285 display.assert_pattern(&[
286 " ",
287 " ",
288 " ",
289 " ",
290 " ",
291 " ",
292 "..............................",
293 ".#...........##....##.........",
294 ".#............#.....#.........",
295 ".###....##....#.....#.....##..",
296 ".#..#..#.##...#.....#....#..#.",
297 ".#..#..##.....#.....#....#..#.",
298 ".#..#...###..###...###....##..",
299 "..............................",
300 "..............................",
301 ]);
302 }
303
304 #[test]
305 fn vertical_offset_negative() {
306 let mut display = MockDisplay::new();
307
308 let character_style = MonoTextStyleBuilder::new()
309 .font(&FONT_6X9)
310 .text_color(BinaryColor::On)
311 .background_color(BinaryColor::Off)
312 .build();
313
314 TextBox::with_textbox_style(
315 "hello",
316 Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
317 character_style,
318 TextBoxStyleBuilder::new()
319 .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
320 .build(),
321 )
322 .set_vertical_offset(-4)
323 .draw(&mut display)
324 .unwrap();
325
326 display.assert_pattern(&[
327 ".#..#..#.##...#.....#....#..#.",
328 ".#..#..##.....#.....#....#..#.",
329 ".#..#...###..###...###....##..",
330 "..............................",
331 "..............................",
332 ]);
333 }
334
335 #[test]
336 fn rendering_not_stopped_prematurely() {
337 let mut display = MockDisplay::new();
338
339 let character_style = MonoTextStyleBuilder::new()
340 .font(&FONT_6X10)
341 .text_color(BinaryColor::On)
342 .background_color(BinaryColor::Off)
343 .build();
344
345 TextBox::with_textbox_style(
346 "hello\nbuggy\nworld",
347 Rectangle::new(Point::zero(), size_for(&FONT_6X10, 5, 3)),
348 character_style,
349 TextBoxStyleBuilder::new()
350 .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
351 .build(),
352 )
353 .set_vertical_offset(-20)
354 .draw(&mut display)
355 .unwrap();
356
357 display.assert_pattern(&[
358 "..............................",
359 "...................##.......#.",
360 "....................#.......#.",
361 "#...#..###..#.##....#....##.#.",
362 "#...#.#...#.##..#...#...#..##.",
363 "#.#.#.#...#.#.......#...#...#.",
364 "#.#.#.#...#.#.......#...#..##.",
365 ".#.#...###..#......###...##.#.",
366 "..............................",
367 "..............................",
368 ]);
369 }
370
371 #[test]
372 fn space_wrapping_issue() {
373 let mut display = MockDisplay::new();
374
375 let character_style = MonoTextStyleBuilder::new()
376 .font(&FONT_6X10)
377 .text_color(BinaryColor::On)
378 .background_color(BinaryColor::Off)
379 .build();
380
381 TextBox::with_textbox_style(
382 "Hello, s",
383 Rectangle::new(Point::zero(), size_for(&FONT_6X10, 10, 2)),
384 character_style,
385 TextBoxStyleBuilder::new()
386 .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
387 .trailing_spaces(true)
388 .build(),
389 )
390 .draw(&mut display)
391 .unwrap();
392
393 display.assert_pattern(&[
394 "............................................................",
395 "#...#........##....##.......................................",
396 "#...#.........#.....#.......................................",
397 "#...#..###....#.....#....###................................",
398 "#####.#...#...#.....#...#...#...............................",
399 "#...#.#####...#.....#...#...#...............................",
400 "#...#.#.......#.....#...#...#...##..........................",
401 "#...#..###...###...###...###....#...........................",
402 "...............................#............................",
403 "............................................................",
404 "............ ",
405 "............ ",
406 "............ ",
407 ".......###.. ",
408 "......#..... ",
409 ".......###.. ",
410 "..........#. ",
411 "......####.. ",
412 "............ ",
413 "............ ",
414 ]);
415 }
416
417 #[test]
418 fn rendering_justified_text_with_negative_left_side_bearing() {
419 let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
420 display.set_allow_overdraw(true);
421
422 let text = "j000 0 j00 00j00 0";
423 let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
424 let size = Size::new(50, 0);
425
426 TextBox::with_textbox_style(
427 text,
428 Rectangle::new(Point::zero(), size),
429 character_style,
430 TextBoxStyleBuilder::new()
431 .alignment(HorizontalAlignment::Justified)
432 .height_mode(HeightMode::FitToText)
433 .build(),
434 )
435 .draw(&mut display)
436 .unwrap();
437
438 display.assert_pattern(&[
439 "..#.####.####.####.........####........#.####.####",
440 "....#..#.#..#.#..#.........#..#..........#..#.#..#",
441 "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
442 "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
443 "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
444 "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
445 "..#.####.####.####.........####........#.####.####",
446 "..#....................................#..........",
447 "..#....................................#..........",
448 "##...................................##...........",
449 "####.####.#.####.####....#### ",
450 "#..#.#..#...#..#.#..#....#..# ",
451 "#..#.#..#.#.#..#.#..#....#..# ",
452 "#..#.#..#.#.#..#.#..#....#..# ",
453 "#..#.#..#.#.#..#.#..#....#..# ",
454 "#..#.#..#.#.#..#.#..#....#..# ",
455 "####.####.#.####.####....#### ",
456 "..........#.................. ",
457 "..........#.................. ",
458 "........##................... ",
459 ]);
460 }
461
462 #[test]
463 fn correctly_breaks_long_words_for_monospace_fonts() {
464 let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
465 display.set_allow_overdraw(true);
466
467 let text = "000000000000000000";
468 let character_style = MonoTextStyleBuilder::new()
469 .font(&FONT_6X10)
470 .text_color(BinaryColor::On)
471 .background_color(BinaryColor::Off)
472 .build();
473
474 TextBox::with_textbox_style(
475 text,
476 Rectangle::new(Point::zero(), size_for(&FONT_6X10, 10, 2)),
477 character_style,
478 TextBoxStyleBuilder::new()
479 .alignment(HorizontalAlignment::Left)
480 .height_mode(HeightMode::FitToText)
481 .build(),
482 )
483 .draw(&mut display)
484 .unwrap();
485
486 display.assert_pattern(&[
487 "............................................................",
488 "..#.....#.....#.....#.....#.....#.....#.....#.....#.....#...",
489 ".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#..",
490 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
491 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
492 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.",
493 ".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#..",
494 "..#.....#.....#.....#.....#.....#.....#.....#.....#.....#...",
495 "............................................................",
496 "............................................................",
497 "................................................ ",
498 "..#.....#.....#.....#.....#.....#.....#.....#... ",
499 ".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#.. ",
500 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
501 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
502 "#...#.#...#.#...#.#...#.#...#.#...#.#...#.#...#. ",
503 ".#.#...#.#...#.#...#.#...#.#...#.#...#.#...#.#.. ",
504 "..#.....#.....#.....#.....#.....#.....#.....#... ",
505 "................................................ ",
506 "................................................ ",
507 ]);
508 }
509
510 #[test]
511 fn correctly_breaks_long_words_for_fonts_with_letter_spacing() {
512 let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
513 display.set_allow_overdraw(true);
514
515 let text = "000000000000000000";
516 let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
517 let size = Size::new(49, 0);
518
519 TextBox::with_textbox_style(
520 text,
521 Rectangle::new(Point::zero(), size),
522 character_style,
523 TextBoxStyleBuilder::new()
524 .alignment(HorizontalAlignment::Left)
525 .height_mode(HeightMode::FitToText)
526 .build(),
527 )
528 .draw(&mut display)
529 .unwrap();
530
531 display.assert_pattern(&[
532 "####.####.####.####.####.####.####.####.####.####",
533 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
534 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
535 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
536 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
537 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
538 "####.####.####.####.####.####.####.####.####.####",
539 ".................................................",
540 ".................................................",
541 ".................................................",
542 "####.####.####.####.####.####.####.#### ",
543 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
544 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
545 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
546 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
547 "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..# ",
548 "####.####.####.####.####.####.####.#### ",
549 "....................................... ",
550 "....................................... ",
551 "....................................... ",
552 ]);
553 }
554
555 #[test]
556 fn correctly_breaks_long_words_with_wide_utf8_characters() {
557 let mut display = MockDisplay::new();
558 let character_style = MonoTextStyleBuilder::new()
559 .font(&FONT_6X10)
560 .text_color(BinaryColor::On)
561 .background_color(BinaryColor::Off)
562 .build();
563
564 TextBox::with_textbox_style(
565 "広広広", Rectangle::new(Point::zero(), size_for(&FONT_6X10, 2, 2)),
567 character_style,
568 TextBoxStyleBuilder::new().build(),
569 )
570 .draw(&mut display)
571 .unwrap();
572
573 display.assert_pattern(&[
574 "............",
575 ".###...###..",
576 "#...#.#...#.",
577 "...#.....#..",
578 "..#.....#...",
579 "..#.....#...",
580 "............",
581 "..#.....#...",
582 "............",
583 "............",
584 "...... ",
585 ".###.. ",
586 "#...#. ",
587 "...#.. ",
588 "..#... ",
589 "..#... ",
590 "...... ",
591 "..#... ",
592 "...... ",
593 "...... ",
594 ]);
595 }
596}