1use embedded_graphics::{
2 draw_target::DrawTarget,
3 mono_font::{MonoTextStyle, ascii::FONT_6X9},
4 pixelcolor::BinaryColor,
5 prelude::*,
6 primitives::{PrimitiveStyle, Rectangle},
7 text::{
8 Text,
9 renderer::{CharacterStyle, TextRenderer},
10 },
11};
12
13pub struct Textbox<'a, A> {
14 buffer: A,
15 mono_text_style: MonoTextStyle<'a, BinaryColor>,
16 character_bounding_box: Rectangle,
17 row_index: usize,
18 highlighted_range: Option<core::ops::Range<usize>>,
19}
20
21impl<'a, A> Textbox<'a, A> {
22 pub fn new(buffer: A) -> Self {
23 let mono_text_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::Off);
24
25 Self {
26 buffer,
27 character_bounding_box: mono_text_style
28 .measure_string("A", Point::zero(), embedded_graphics::text::Baseline::Top)
29 .bounding_box,
30 mono_text_style,
31 row_index: 0,
32 highlighted_range: None,
33 }
34 }
35
36 fn erase_index<D>(&mut self, device: &mut D, index: usize)
37 where
38 D: DrawTarget<Color = BinaryColor>,
39 {
40 let _ = Rectangle::new(
41 self.position_from_index(device, index),
42 self.character_bounding_box.size,
43 )
44 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
45 .draw(device);
46 }
47
48 fn characters_per_row<D>(&self, device: &mut D) -> u32
49 where
50 D: DrawTarget<Color = BinaryColor>,
51 {
52 device.bounding_box().size.width / self.character_bounding_box.size.width
53 }
54
55 fn row<D>(&self, device: &mut D, index: usize) -> usize
56 where
57 D: DrawTarget<Color = BinaryColor>,
58 {
59 index / self.characters_per_row(device) as usize
60 }
61
62 fn position_from_index<D>(&self, device: &mut D, index: usize) -> Point
63 where
64 D: DrawTarget<Color = BinaryColor>,
65 {
66 let y = self.row(device, index) as u32 * self.character_bounding_box.size.height;
67 let col = index as u32 % self.characters_per_row(device);
68 let x = col * self.character_bounding_box.size.width;
69
70 Point::new(
71 x.try_into().unwrap(),
72 y as i32 - self.character_bounding_box.size.height as i32 * self.row_index as i32,
73 )
74 }
75
76 #[allow(dead_code)]
77 fn scroll_up(&mut self) {
78 self.row_index = (self.row_index - 1).max(0)
79 }
80
81 fn scroll_to<D>(&mut self, device: &mut D, index: usize) -> bool
82 where
83 D: DrawTarget<Color = BinaryColor>,
84 {
85 let row_index = index / self.characters_per_row(device) as usize;
86 if row_index == self.row_index {
87 false
88 } else {
89 self.row_index = row_index;
90 true
91 }
92 }
93}
94
95impl<'a, A> Textbox<'a, A>
96where
97 A: core::ops::Deref<Target = str>,
98{
99 pub fn highlight<D>(&mut self, device: &mut D, range: core::ops::Range<usize>)
100 where
101 D: DrawTarget<Color = BinaryColor>,
102 {
103 if let Some(highlighted_range) = &self.highlighted_range {
104 self.draw(device, Some(highlighted_range.clone()), false);
105 }
106 self.highlighted_range = Some(range.clone());
107 if self.scroll_to(device, range.end) {
108 let _ = device.clear(BinaryColor::On);
109 self.draw(device, None, false);
110 }
111 self.draw(device, self.highlighted_range.clone(), true);
112 }
113
114 fn draw_character_at_index<D>(&mut self, device: &mut D, index: usize, invert: bool)
115 where
116 D: DrawTarget<Color = BinaryColor>,
117 {
118 let _ = Rectangle::new(
119 self.position_from_index(device, index),
120 self.character_bounding_box.size,
121 )
122 .into_styled(if invert {
123 PrimitiveStyle::with_fill(BinaryColor::Off)
124 } else {
125 PrimitiveStyle::with_fill(BinaryColor::On)
126 })
127 .draw(device);
128
129 let mut mono_text_style = self.mono_text_style;
130 if invert {
131 mono_text_style.set_background_color(Some(BinaryColor::Off));
132 mono_text_style.set_text_color(Some(BinaryColor::On));
133 }
134
135 let _ = Text::with_baseline(
136 self.buffer.get(index..(index + 1)).unwrap(),
137 self.position_from_index(device, index),
138 mono_text_style,
139 embedded_graphics::text::Baseline::Top,
140 )
141 .draw(device);
142 }
143
144 #[allow(dead_code)]
145 fn scroll_down<D>(&mut self, device: &mut D)
146 where
147 D: DrawTarget<Color = BinaryColor>,
148 {
149 self.row_index = (self.row_index + 1).min(self.max_row_index(device));
150 self.clear_last_row(device)
151 }
152
153 #[allow(dead_code)]
154 fn clear_last_row<D>(&mut self, device: &mut D)
155 where
156 D: DrawTarget<Color = BinaryColor>,
157 {
158 let n_rows_on_screen =
159 device.bounding_box().size.width / self.character_bounding_box.size.height - 1;
160 let rectangle = Rectangle::new(
161 Point::new(
162 0,
163 (self.character_bounding_box.size.height * n_rows_on_screen)
164 .try_into()
165 .unwrap(),
166 ),
167 Size::new(
168 device.bounding_box().size.width,
169 device.bounding_box().size.height
170 - (self.character_bounding_box.size.height * n_rows_on_screen),
171 ),
172 );
173 let _ = rectangle
174 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
175 .draw(device);
176 }
177
178 #[allow(dead_code)]
179 fn max_row_index<D>(&mut self, device: &mut D) -> usize
180 where
181 D: DrawTarget<Color = BinaryColor>,
182 {
183 let n_rows_on_screen =
184 device.bounding_box().size.width / self.character_bounding_box.size.height;
185 let n_rows_in_buffer = self
186 .buffer
187 .len()
188 .div_ceil(self.characters_per_row(device).try_into().unwrap());
189 n_rows_in_buffer.saturating_sub(n_rows_on_screen as usize)
190 }
191
192 pub fn draw<D>(&mut self, device: &mut D, range: Option<core::ops::Range<usize>>, invert: bool)
193 where
194 D: DrawTarget<Color = BinaryColor>,
195 {
196 for i in range.unwrap_or(0..self.buffer.len()) {
197 self.draw_character_at_index(device, i, invert);
198 }
199 }
200}
201
202impl<'a, const N: usize> Textbox<'a, heapless::String<N>> {
203 pub fn backspace<D>(&mut self, device: &mut D)
204 where
205 D: DrawTarget<Color = BinaryColor>,
206 {
207 if !self.buffer.is_empty() {
208 self.erase_index(device, self.buffer.len() - 1);
209 let _ = self.buffer.pop();
210 }
211 }
212
213 pub fn push<D>(&mut self, device: &mut D, character: char, invert: bool)
214 where
215 D: DrawTarget<Color = BinaryColor>,
216 {
217 if self.buffer.len() < self.buffer.capacity() {
218 let _ = self.buffer.push(character);
219 self.draw_character_at_index(device, self.buffer.len() - 1, invert);
220 }
221 }
222
223 pub fn release(self) -> heapless::String<N> {
224 self.buffer
225 }
226}
227
228#[cfg(test)]
229mod test {
230 extern crate std;
231 use core::str::FromStr;
232
233 use embedded_graphics::{Pixel, mock_display::MockDisplay};
234 use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::BinaryColor};
235
236 pub struct Device {
237 pub display: MockDisplay<BinaryColor>,
238 }
239
240 impl Device {
241 pub fn new() -> Self {
242 let mut display = MockDisplay::new();
243 display.set_allow_out_of_bounds_drawing(true);
244 display.set_allow_overdraw(true);
245
246 Self { display }
247 }
248 }
249
250 impl crate::Keypad for Device {
251 async fn event(&mut self) -> crate::KeyEvent {
252 crate::KeyEvent::Down(crate::Key::Down)
253 }
254
255 fn last_pressed(&mut self) -> Option<embassy_time::Duration> {
256 None
257 }
258 }
259
260 impl DrawTarget for Device {
261 type Color = BinaryColor;
262 type Error = core::convert::Infallible;
263 fn draw_iter<
264 I: IntoIterator<Item = Pixel<<Self as embedded_graphics::draw_target::DrawTarget>::Color>>,
265 >(
266 &mut self,
267 pixels: I,
268 ) -> Result<(), <Self as embedded_graphics::draw_target::DrawTarget>::Error> {
269 self.display.draw_iter(pixels)
270 }
271 }
272
273 impl embedded_graphics::prelude::Dimensions for Device {
274 fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle {
275 self.display.bounding_box()
276 }
277 }
278
279 #[test]
280 fn test_textbox() {
281 let characters = heapless::String::<15>::from_str("abc").unwrap();
282 let mut device = Device::new();
283 let _ = device.clear(BinaryColor::On);
284 let mut textbox = super::Textbox::new(characters);
285 textbox.draw(&mut device, None, false);
286 textbox.erase_index(&mut device, 1);
287 textbox.push(&mut device, 'G', false);
288 textbox.backspace(&mut device);
289 textbox.push(&mut device, 'H', false);
290 textbox.push(&mut device, 'I', false);
291 textbox.push(&mut device, 'J', false);
292 textbox.push(&mut device, 'K', false);
293 textbox.push(&mut device, 'L', false);
294 textbox.push(&mut device, 'M', false);
295 textbox.push(&mut device, 'N', false);
296 textbox.push(&mut device, 'O', false);
297
298 device.display.assert_pattern(&[
299 "################################################################",
300 "###################.##.##...####...##.##.##.####.###.##.##.#####",
301 "###################.##.###.######.###.#.###.####..#..##..#.#####",
302 "##...#########...##....###.######.###..####.####.#.#.##.#..#####",
303 "#.##.########.#####.##.###.######.###.#.###.####.#.#.##.##.#####",
304 "#.##.########.#####.##.###.###.##.###.##.##.####.###.##.##.#####",
305 "##...#########...##.##.##...###..####.##.##....#.###.##.##.#####",
306 "################################################################",
307 "################################################################",
308 "################################################################",
309 "#...############################################################",
310 ".###.###########################################################",
311 ".###.###########################################################",
312 ".###.###########################################################",
313 ".###.###########################################################",
314 "#...############################################################",
315 "################################################################",
316 "################################################################",
317 "################################################################",
318 "################################################################",
319 "################################################################",
320 "################################################################",
321 "################################################################",
322 "################################################################",
323 "################################################################",
324 "################################################################",
325 "################################################################",
326 "################################################################",
327 "################################################################",
328 "################################################################",
329 "################################################################",
330 "################################################################",
331 "################################################################",
332 "################################################################",
333 "################################################################",
334 "################################################################",
335 "################################################################",
336 "################################################################",
337 "################################################################",
338 "################################################################",
339 "################################################################",
340 "################################################################",
341 "################################################################",
342 "################################################################",
343 "################################################################",
344 "################################################################",
345 "################################################################",
346 "################################################################",
347 "################################################################",
348 "################################################################",
349 "################################################################",
350 "################################################################",
351 "################################################################",
352 "################################################################",
353 "################################################################",
354 "################################################################",
355 "################################################################",
356 "################################################################",
357 "################################################################",
358 "################################################################",
359 "################################################################",
360 "################################################################",
361 "################################################################",
362 "################################################################",
363 ]);
364 assert_eq!(textbox.release(), "abcHIJKLMNO");
365 }
366
367 #[test]
368 fn test_highlight() {
369 let characters = heapless::String::<15>::from_str("abcdefghijklmn").unwrap();
370 let mut device = Device::new();
371 let _ = device.clear(BinaryColor::On);
372 let mut textbox = super::Textbox::new(characters);
373 textbox.draw(&mut device, None, false);
374 textbox.draw(&mut device, Some(4..7), true);
375
376 device.display.assert_pattern(&[
377 "########################..................######################",
378 "#######.##############.#.........#........#.######.######.######",
379 "#######.##############.#........#.#.......#.####################",
380 "##...##...####...###...#..##....#.....##..#...###..#####..######",
381 "#.##.##.##.##.#####.##.#.#.##..###...#..#.#.##.###.######.######",
382 "#.##.##.##.##.#####.##.#.##.....#....#..#.#.##.###.######.######",
383 "##...##...####...###...#..###...#.....###.#.##.##...#####.######",
384 "########################................#.#############.#.######",
385 "########################..............##..##############.#######",
386 "################################################################",
387 "#.#####..#######################################################",
388 "#.######.#######################################################",
389 "#.#.####.###..#.###...##########################################",
390 "#..#####.###.#.#.##.##.#########################################",
391 "#.#.####.###.#.#.##.##.#########################################",
392 "#.##.##...##.###.##.##.#########################################",
393 "################################################################",
394 "################################################################",
395 "################################################################",
396 "################################################################",
397 "################################################################",
398 "################################################################",
399 "################################################################",
400 "################################################################",
401 "################################################################",
402 "################################################################",
403 "################################################################",
404 "################################################################",
405 "################################################################",
406 "################################################################",
407 "################################################################",
408 "################################################################",
409 "################################################################",
410 "################################################################",
411 "################################################################",
412 "################################################################",
413 "################################################################",
414 "################################################################",
415 "################################################################",
416 "################################################################",
417 "################################################################",
418 "################################################################",
419 "################################################################",
420 "################################################################",
421 "################################################################",
422 "################################################################",
423 "################################################################",
424 "################################################################",
425 "################################################################",
426 "################################################################",
427 "################################################################",
428 "################################################################",
429 "################################################################",
430 "################################################################",
431 "################################################################",
432 "################################################################",
433 "################################################################",
434 "################################################################",
435 "################################################################",
436 "################################################################",
437 "################################################################",
438 "################################################################",
439 "################################################################",
440 "################################################################",
441 ]);
442 assert_eq!(
443 textbox.release(),
444 heapless::String::<15>::from_str("abcdefghijklmn").unwrap()
445 );
446 }
447
448 #[test]
449 fn test_scroll() {
450 let mut device = Device::new();
451 let _ = device.clear(BinaryColor::On);
452 let mut textbox = super::Textbox::new(
453 "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGGHH",
454 );
455 textbox.scroll_down(&mut device);
456 textbox.draw(&mut device, None, false);
457
458 device.display.assert_pattern(&[
459 "################################################################",
460 "....##....##....##....##....##....##....##....##....##....######",
461 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
462 "....##....##....##....##....##....##....##....##....##....######",
463 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
464 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
465 "....##....##....##....##....##....##....##....##....##....######",
466 "################################################################",
467 "################################################################",
468 "################################################################",
469 "##..####..####..####..####..####..####..####..####..####..######",
470 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
471 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
472 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
473 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
474 "##..####..####..####..####..####..####..####..####..####..######",
475 "################################################################",
476 "################################################################",
477 "################################################################",
478 "#...###...###...###...###...###...###...###...###...###...######",
479 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
480 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
481 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
482 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
483 "#...###...###...###...###...###...###...###...###...###...######",
484 "################################################################",
485 "################################################################",
486 "################################################################",
487 "#....##....##....##....##....##....##....##....##....##....#####",
488 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
489 "#...###...###...###...###...###...###...###...###...###...######",
490 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
491 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
492 "#....##....##....##....##....##....##....##....##....##....#####",
493 "################################################################",
494 "################################################################",
495 "################################################################",
496 "#....##....##....##....##....##....##....##....##....##....#####",
497 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
498 "#...###...###...###...###...###...###...###...###...###...######",
499 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
500 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
501 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
502 "################################################################",
503 "################################################################",
504 "################################################################",
505 "##..####..####..####..####..####..####..####..####..####..######",
506 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
507 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
508 "#.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..#####",
509 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
510 "##..####..####..####..####..####..####..####..####..####..######",
511 "################################################################",
512 "################################################################",
513 "################################################################",
514 "#.##.##.##.#####################################################",
515 "#.##.##.##.#####################################################",
516 "#....##....#####################################################",
517 "#.##.##.##.#####################################################",
518 "#.##.##.##.#####################################################",
519 "#.##.##.##.#####################################################",
520 "################################################################",
521 "################################################################",
522 "################################################################",
523 ]);
524 }
525
526 #[test]
527 fn test_tail_on_push() {
528 let mut device = Device::new();
529 let _ = device.clear(BinaryColor::On);
530 let mut textbox = super::Textbox::new(
531 heapless::String::<72>::from_str(
532 "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGG",
533 )
534 .unwrap(),
535 );
536 textbox.draw(&mut device, None, false);
537
538 textbox.push(&mut device, 'H', false);
540 textbox.push(&mut device, 'H', false);
541 textbox.scroll_down(&mut device);
542 textbox.draw(&mut device, None, false);
543
544 assert_eq!(
545 textbox.release(),
546 heapless::String::<72>::from_str(
547 "AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGGHH"
548 )
549 .unwrap(),
550 );
551
552 device.display.assert_pattern(&[
553 "################################################################",
554 "....##....##....##....##....##....##....##....##....##....######",
555 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
556 "....##....##....##....##....##....##....##....##....##....######",
557 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
558 ".###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#.###.#####",
559 "....##....##....##....##....##....##....##....##....##....######",
560 "################################################################",
561 "################################################################",
562 "################################################################",
563 "##..####..####..####..####..####..####..####..####..####..######",
564 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
565 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
566 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
567 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
568 "##..####..####..####..####..####..####..####..####..####..######",
569 "################################################################",
570 "################################################################",
571 "################################################################",
572 "#...###...###...###...###...###...###...###...###...###...######",
573 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
574 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
575 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
576 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
577 "#...###...###...###...###...###...###...###...###...###...######",
578 "################################################################",
579 "################################################################",
580 "################################################################",
581 "#....##....##....##....##....##....##....##....##....##....#####",
582 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
583 "#...###...###...###...###...###...###...###...###...###...######",
584 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
585 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
586 "#....##....##....##....##....##....##....##....##....##....#####",
587 "################################################################",
588 "################################################################",
589 "################################################################",
590 "#....##....##....##....##....##....##....##....##....##....#####",
591 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
592 "#...###...###...###...###...###...###...###...###...###...######",
593 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
594 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
595 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
596 "################################################################",
597 "################################################################",
598 "################################################################",
599 "##..####..####..####..####..####..####..####..####..####..######",
600 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
601 "#.#####.#####.#####.#####.#####.#####.#####.#####.#####.########",
602 "#.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..##.#..#####",
603 "#.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.##.#####",
604 "##..####..####..####..####..####..####..####..####..####..######",
605 "################################################################",
606 "################################################################",
607 "################################################################",
608 "#.##.##.##.#####################################################",
609 "#.##.##.##.#####################################################",
610 "#....##....#####################################################",
611 "#.##.##.##.#####################################################",
612 "#.##.##.##.#####################################################",
613 "#.##.##.##.#####################################################",
614 "################################################################",
615 "################################################################",
616 "################################################################",
617 ]);
618 }
619}