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