1use alloc::boxed::Box;
31use rlvgl_core::{
32 edit::EditCore as CoreEditCore,
33 event::{Event, Key},
34 font::{FontMetrics, WidgetFont, shape_text_ltr},
35 renderer::Renderer,
36 widget::{Rect, Widget},
37};
38use rlvgl_widgets::label::Label;
39
40pub use rlvgl_core::edit::EditCore;
48
49pub use rlvgl_core::edit::DEFAULT_CHAR_WIDTH;
51pub use rlvgl_core::edit::DEFAULT_LINE_HEIGHT;
53
54type SubmitCallback = Box<dyn FnMut(&str)>;
56
57const CARET_WIDTH: i32 = rlvgl_core::edit::CARET_WIDTH;
59
60struct UiEditCore {
69 core: CoreEditCore,
70 label: Label,
71}
72
73impl UiEditCore {
74 fn new(text: &str, bounds: Rect, multi_line: bool) -> Self {
75 Self {
76 core: CoreEditCore::new(text, bounds, multi_line),
77 label: Label::new(text, bounds),
78 }
79 }
80
81 fn set_font(&mut self, font: &'static dyn FontMetrics) {
82 self.label.set_font(font);
83 }
84
85 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
86 self.label.widget_font_mut()
87 }
88
89 fn set_text(&mut self, text: &str) {
90 self.core.buffer = alloc::string::String::from(text);
92 self.core.caret = self.core.caret.min(self.core.buffer.chars().count());
93 self.label.set_text(text);
94 if let Some(cb) = self.core.on_change.as_mut() {
95 cb(&self.core.buffer);
96 }
97 }
98
99 fn committed(&mut self) {
100 self.label.set_text(self.core.buffer.clone());
102 if let Some(cb) = self.core.on_change.as_mut() {
103 cb(&self.core.buffer);
104 }
105 }
106
107 fn try_insert(&mut self, c: char) -> bool {
108 let printable = ('\u{20}'..='\u{7e}').contains(&c);
111 if !(printable || (c == '\n' && self.core.multi_line)) {
112 return false;
113 }
114 if c != '\n'
115 && let Some(accept) = self.core.accept.as_ref()
116 && !accept(c)
117 {
118 return false;
119 }
120 if let Some(max) = self.core.max_len
121 && self.core.char_count() >= max
122 {
123 return false;
124 }
125 let at = self.core.byte_index(self.core.caret);
126 self.core.buffer.insert(at, c);
127 self.core.caret += 1;
128 self.committed();
129 true
130 }
131
132 fn try_backspace(&mut self) -> bool {
133 if self.core.caret == 0 {
134 return false;
135 }
136 let at = self.core.byte_index(self.core.caret - 1);
137 self.core.buffer.remove(at);
138 self.core.caret -= 1;
139 self.committed();
140 true
141 }
142
143 fn handle_key(&mut self, key: &Key) -> bool {
144 match key {
145 Key::Character(c) => {
146 self.try_insert(*c);
147 true
148 }
149 Key::Space => {
150 self.try_insert(' ');
151 true
152 }
153 Key::Backspace => {
154 self.try_backspace();
155 true
156 }
157 Key::ArrowLeft => {
158 self.core.caret = self.core.caret.saturating_sub(1);
159 true
160 }
161 Key::ArrowRight => {
162 self.core.caret = (self.core.caret + 1).min(self.core.char_count());
163 true
164 }
165 _ => false,
166 }
167 }
168
169 fn caret_row_col(&self) -> (i32, i32) {
170 self.core.caret_row_col()
171 }
172
173 fn draw_caret(&self, renderer: &mut dyn Renderer) {
174 if !self.core.active {
175 return;
176 }
177 let bounds = self.label.bounds();
178 let (row, col) = self.caret_row_col();
179 renderer.fill_rect(
180 Rect {
181 x: bounds.x + col * self.core.char_width,
182 y: bounds.y + row * self.core.line_height,
183 width: CARET_WIDTH,
184 height: self.core.line_height,
185 },
186 self.label.style.border_color,
187 );
188 }
189
190 fn draw_multi_line(&self, renderer: &mut dyn Renderer) {
191 rlvgl_core::draw::draw_widget_bg(renderer, self.label.bounds(), &self.label.style);
192 let bounds = self.label.bounds();
193 for (row, line) in self.core.buffer.split('\n').enumerate() {
194 if line.is_empty() {
195 continue;
196 }
197 let baseline = bounds.y + (row as i32 + 1) * self.core.line_height;
198 let shaped = shape_text_ltr(self.label.resolved_font(), line, (bounds.x, baseline), 0);
199 renderer.draw_text_shaped(
200 &shaped,
201 (0, 0),
202 self.label.text_color_with_alpha(self.label.style.alpha),
203 );
204 }
205 }
206}
207
208#[allow(clippy::type_complexity)]
215pub struct Input {
216 core: UiEditCore,
217 on_submit: Option<SubmitCallback>,
218}
219
220impl Input {
221 pub fn new(text: &str, bounds: Rect) -> Self {
224 Self {
225 core: UiEditCore::new(text, bounds, false),
226 on_submit: None,
227 }
228 }
229
230 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
233 self.core.set_font(font);
234 }
235
236 pub fn on_change<F: FnMut(&str) + 'static>(mut self, handler: F) -> Self {
239 self.core.core.on_change = Some(Box::new(handler));
240 self
241 }
242
243 pub fn on_submit<F: FnMut(&str) + 'static>(mut self, handler: F) -> Self {
246 self.on_submit = Some(Box::new(handler));
247 self
248 }
249
250 pub fn active(mut self, active: bool) -> Self {
252 self.core.core.active = active;
253 self
254 }
255
256 pub fn with_accept<F: Fn(char) -> bool + 'static>(mut self, accept: F) -> Self {
260 self.core.core.accept = Some(Box::new(accept));
261 self
262 }
263
264 pub fn with_max_len(mut self, max: usize) -> Self {
266 self.core.core.max_len = Some(max);
267 self
268 }
269
270 pub fn with_char_metrics(mut self, char_width: i32, line_height: i32) -> Self {
274 self.core.core.char_width = char_width.max(1);
275 self.core.core.line_height = line_height.max(1);
276 self
277 }
278
279 pub fn is_active(&self) -> bool {
281 self.core.core.active
282 }
283
284 pub fn set_active(&mut self, active: bool) {
287 self.core.core.active = active;
288 }
289
290 pub fn caret(&self) -> usize {
292 self.core.core.caret
293 }
294
295 pub fn style(&self) -> &rlvgl_core::style::Style {
297 &self.core.label.style
298 }
299
300 pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
302 &mut self.core.label.style
303 }
304
305 pub fn set_text(&mut self, text: &str) {
308 self.core.set_text(text);
309 }
310
311 pub fn text(&self) -> &str {
313 &self.core.core.buffer
314 }
315}
316
317impl Widget for Input {
318 fn bounds(&self) -> Rect {
319 self.core.label.bounds()
320 }
321
322 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
323 self.core.widget_font_mut()
324 }
325
326 fn draw(&self, renderer: &mut dyn Renderer) {
327 self.core.label.draw(renderer);
328 self.core.draw_caret(renderer);
329 }
330
331 fn handle_event(&mut self, event: &Event) -> bool {
332 if !self.core.core.active {
333 return false;
334 }
335 if let Event::KeyDown { key } = event {
336 if *key == Key::Enter {
337 if let Some(cb) = self.on_submit.as_mut() {
338 cb(&self.core.core.buffer);
339 }
340 return true;
341 }
342 return self.core.handle_key(key);
343 }
344 false
345 }
346}
347
348pub struct Textarea {
354 core: UiEditCore,
355}
356
357impl Textarea {
358 pub fn new(text: &str, bounds: Rect) -> Self {
360 Self {
361 core: UiEditCore::new(text, bounds, true),
362 }
363 }
364
365 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
368 self.core.set_font(font);
369 }
370
371 pub fn on_change<F: FnMut(&str) + 'static>(mut self, handler: F) -> Self {
374 self.core.core.on_change = Some(Box::new(handler));
375 self
376 }
377
378 pub fn active(mut self, active: bool) -> Self {
380 self.core.core.active = active;
381 self
382 }
383
384 pub fn with_accept<F: Fn(char) -> bool + 'static>(mut self, accept: F) -> Self {
387 self.core.core.accept = Some(Box::new(accept));
388 self
389 }
390
391 pub fn with_max_len(mut self, max: usize) -> Self {
393 self.core.core.max_len = Some(max);
394 self
395 }
396
397 pub fn with_char_metrics(mut self, char_width: i32, line_height: i32) -> Self {
400 self.core.core.char_width = char_width.max(1);
401 self.core.core.line_height = line_height.max(1);
402 self
403 }
404
405 pub fn is_active(&self) -> bool {
407 self.core.core.active
408 }
409
410 pub fn set_active(&mut self, active: bool) {
412 self.core.core.active = active;
413 }
414
415 pub fn caret(&self) -> usize {
418 self.core.core.caret
419 }
420
421 pub fn style(&self) -> &rlvgl_core::style::Style {
423 &self.core.label.style
424 }
425
426 pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
428 &mut self.core.label.style
429 }
430
431 pub fn set_text(&mut self, text: &str) {
433 self.core.set_text(text);
434 }
435
436 pub fn text(&self) -> &str {
438 &self.core.core.buffer
439 }
440}
441
442impl Widget for Textarea {
443 fn bounds(&self) -> Rect {
444 self.core.label.bounds()
445 }
446
447 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
448 self.core.widget_font_mut()
449 }
450
451 fn draw(&self, renderer: &mut dyn Renderer) {
452 self.core.draw_multi_line(renderer);
453 self.core.draw_caret(renderer);
454 }
455
456 fn handle_event(&mut self, event: &Event) -> bool {
457 if !self.core.core.active {
458 return false;
459 }
460 if let Event::KeyDown { key } = event {
461 if *key == Key::Enter {
462 self.core.try_insert('\n');
463 return true;
464 }
465 return self.core.handle_key(key);
466 }
467 false
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use alloc::rc::Rc;
475 use alloc::vec::Vec;
476 use core::cell::{Cell, RefCell};
477 use rlvgl_core::widget::Rect;
478
479 const BOUNDS: Rect = Rect {
480 x: 0,
481 y: 0,
482 width: 200,
483 height: 20,
484 };
485
486 fn key(input: &mut Input, key: Key) -> bool {
487 input.handle_event(&Event::KeyDown { key })
488 }
489
490 fn type_str(input: &mut Input, s: &str) {
491 for c in s.chars() {
492 key(input, Key::Character(c));
493 }
494 }
495
496 #[test]
497 fn input_sets_text_and_calls_handler() {
498 let called = Rc::new(Cell::new(false));
499 let flag = called.clone();
500 let mut input = Input::new(
501 "hi",
502 Rect {
503 x: 0,
504 y: 0,
505 width: 10,
506 height: 10,
507 },
508 )
509 .on_change(move |_| flag.set(true));
510 input.set_text("new");
511 assert_eq!(input.text(), "new");
512 assert!(called.get());
513 }
514
515 #[test]
516 fn textarea_wraps_input() {
517 let mut area = Textarea::new(
518 "a",
519 Rect {
520 x: 0,
521 y: 0,
522 width: 10,
523 height: 20,
524 },
525 );
526 area.set_text("b");
527 assert_eq!(area.text(), "b");
528 }
529
530 #[test]
531 fn synthetic_stream_types_hi_backspace_leaves_h() {
532 let mut input = Input::new("", BOUNDS);
533 input.set_active(true);
534 type_str(&mut input, "HI");
535 assert_eq!(input.text(), "HI");
536 assert_eq!(input.caret(), 2);
537 key(&mut input, Key::Backspace);
538 assert_eq!(input.text(), "H");
539 assert_eq!(input.caret(), 1);
540 }
541
542 #[test]
543 fn caret_movement_and_mid_buffer_edits() {
544 let mut input = Input::new("", BOUNDS);
545 input.set_active(true);
546 type_str(&mut input, "AC");
547 key(&mut input, Key::ArrowLeft);
548 assert_eq!(input.caret(), 1);
549 key(&mut input, Key::Character('B'));
550 assert_eq!(input.text(), "ABC");
551 assert_eq!(input.caret(), 2);
552 key(&mut input, Key::Backspace);
553 assert_eq!(input.text(), "AC");
554 assert_eq!(input.caret(), 1);
555 key(&mut input, Key::ArrowLeft);
557 key(&mut input, Key::ArrowLeft);
558 assert_eq!(input.caret(), 0, "left-clamped");
559 key(&mut input, Key::Backspace);
560 assert_eq!(input.text(), "AC", "backspace at 0 is a no-op");
561 for _ in 0..5 {
562 key(&mut input, Key::ArrowRight);
563 }
564 assert_eq!(input.caret(), 2, "right-clamped");
565 }
566
567 #[test]
568 fn inactive_inputs_ignore_keys_and_set_active_switches_recipient() {
569 let mut a = Input::new("", BOUNDS);
570 let mut b = Input::new("", BOUNDS);
571 a.set_active(true);
572
573 let press = Event::KeyDown {
574 key: Key::Character('x'),
575 };
576 assert!(a.handle_event(&press));
577 assert!(!b.handle_event(&press), "inactive consumes nothing");
578 assert_eq!(a.text(), "x");
579 assert_eq!(b.text(), "");
580
581 a.set_active(false);
583 b.set_active(true);
584 assert!(!a.handle_event(&press));
585 assert!(b.handle_event(&press));
586 assert_eq!(a.text(), "x", "deactivated field untouched");
587 assert_eq!(b.text(), "x");
588 }
589
590 #[test]
591 fn on_change_fires_per_edit_and_on_submit_on_enter() {
592 let changes = Rc::new(RefCell::new(Vec::new()));
593 let submits = Rc::new(RefCell::new(Vec::new()));
594 let c = changes.clone();
595 let s = submits.clone();
596 let mut input = Input::new("", BOUNDS)
597 .on_change(move |t| c.borrow_mut().push(alloc::string::String::from(t)))
598 .on_submit(move |t| s.borrow_mut().push(alloc::string::String::from(t)));
599 input.set_active(true);
600 type_str(&mut input, "OK");
601 key(&mut input, Key::Backspace);
602 assert_eq!(*changes.borrow(), alloc::vec!["O", "OK", "O"]);
603 key(&mut input, Key::Enter);
604 assert_eq!(*submits.borrow(), alloc::vec!["O"]);
605 assert_eq!(
606 input.text(),
607 "O",
608 "Enter does not mutate a single-line buffer"
609 );
610 key(&mut input, Key::Backspace);
612 key(&mut input, Key::Backspace); assert_eq!(changes.borrow().len(), 4);
614 }
615
616 #[test]
617 fn charset_hook_rejects_without_breaking_caret_state() {
618 let mut dialpad =
619 Input::new("", BOUNDS).with_accept(|c| c.is_ascii_digit() || c == '*' || c == '#');
620 dialpad.set_active(true);
621 type_str(&mut dialpad, "1a2*b#");
622 assert_eq!(dialpad.text(), "12*#", "out-of-set chars rejected");
623 assert_eq!(dialpad.caret(), 4, "caret unaffected by rejections");
624 key(&mut dialpad, Key::ArrowLeft);
626 key(&mut dialpad, Key::Character('z'));
627 assert_eq!(dialpad.caret(), 3);
628 key(&mut dialpad, Key::Character('5'));
629 assert_eq!(dialpad.text(), "12*5#");
630 }
631
632 #[test]
633 fn max_len_caps_the_buffer() {
634 let mut input = Input::new("", BOUNDS).with_max_len(3);
635 input.set_active(true);
636 type_str(&mut input, "12345");
637 assert_eq!(input.text(), "123");
638 assert_eq!(input.caret(), 3);
639 key(&mut input, Key::Backspace);
640 key(&mut input, Key::Character('9'));
641 assert_eq!(input.text(), "129", "frees up after a delete");
642 }
643
644 #[test]
645 fn non_printable_input_is_rejected() {
646 let mut input = Input::new("", BOUNDS);
647 input.set_active(true);
648 key(&mut input, Key::Character('\u{8}'));
649 key(&mut input, Key::Character('\n'));
650 key(&mut input, Key::Character('é'));
651 assert_eq!(input.text(), "", "ASCII v1 bound (WID-00 §5.2)");
652 key(&mut input, Key::Space);
653 assert_eq!(input.text(), " ", "Space key inserts a space");
654 }
655
656 #[test]
657 fn textarea_enter_inserts_newline_and_caret_tracks_rows() {
658 let mut area = Textarea::new("", BOUNDS);
659 area.set_active(true);
660 for c in "AB".chars() {
661 area.handle_event(&Event::KeyDown {
662 key: Key::Character(c),
663 });
664 }
665 area.handle_event(&Event::KeyDown { key: Key::Enter });
666 for c in "C".chars() {
667 area.handle_event(&Event::KeyDown {
668 key: Key::Character(c),
669 });
670 }
671 assert_eq!(area.text(), "AB\nC");
672 assert_eq!(area.caret(), 4);
673 assert_eq!(area.core.caret_row_col(), (1, 1));
674 area.handle_event(&Event::KeyDown {
676 key: Key::Backspace,
677 });
678 area.handle_event(&Event::KeyDown {
679 key: Key::Backspace,
680 });
681 assert_eq!(area.text(), "AB");
682 assert_eq!(area.core.caret_row_col(), (0, 2));
683 }
684
685 #[test]
689 fn render_after_backspace_matches_plain_h_render() {
690 struct Capture(Vec<(i32, i32, alloc::string::String)>, Vec<Rect>);
691 impl Renderer for Capture {
692 fn fill_rect(&mut self, rect: Rect, _color: rlvgl_core::widget::Color) {
693 self.1.push(rect);
694 }
695 fn draw_text(
696 &mut self,
697 position: (i32, i32),
698 text: &str,
699 _color: rlvgl_core::widget::Color,
700 ) {
701 self.0
702 .push((position.0, position.1, alloc::string::String::from(text)));
703 }
704 }
705 let render = |input: &Input| {
706 let mut capture = Capture(Vec::new(), Vec::new());
707 input.draw(&mut capture);
708 (capture.0, capture.1)
709 };
710
711 let mut edited = Input::new("", BOUNDS);
712 edited.set_active(true);
713 type_str(&mut edited, "HI");
714 key(&mut edited, Key::Backspace);
715
716 let mut reference = Input::new("", BOUNDS);
717 reference.set_active(true);
718 type_str(&mut reference, "H");
719
720 assert_eq!(render(&edited), render(&reference));
721 }
722}