Skip to main content

rlvgl_ui/
input.rs

1// SPDX-License-Identifier: MIT
2//! Editable input and textarea components for rlvgl-ui (WID initiative).
3//!
4//! [`Input`] (single-line) and [`Textarea`] (multi-line) own an edit
5//! buffer and, while **active** ([`Input::set_active`]), consume
6//! [`Event::KeyDown`]: printable-ASCII insertion, [`Key::Backspace`]
7//! deletion, Enter (submit / newline), and Left/Right caret movement —
8//! with a visible line caret, change/submit callbacks, and max-length +
9//! accepted-charset hooks (e.g. a digits-plus-`*`/`#` dialpad field).
10//!
11//! Key routing is deliberately minimal (WID-00 §7): applications toggle
12//! `set_active` from their own focus bookkeeping; no focus framework is
13//! imposed. Inactive fields consume nothing and never mutate.
14//!
15//! Rendering still delegates text to [`rlvgl_widgets::label::Label`].
16//! Caret geometry uses nominal char metrics (WID-00 §6.3) because the
17//! `Renderer` trait exposes no glyph extents — exact for monospace /
18//! bitmap fonts, approximate for proportional backend fonts. The caret
19//! does not blink (WID-00 §6.2): rendering stays a pure function of the
20//! edit state, which headless D-dump tests rely on.
21//!
22//! # LPAR-14 §5.C — EditCore promotion
23//!
24//! [`EditCore`] has been moved to [`rlvgl_core::edit::EditCore`] to break the
25//! crate cycle that would arise if `rlvgl-widgets::textarea` depended on
26//! `rlvgl-ui`.  This module re-exports it as `pub use` so that all existing
27//! `ui::input::EditCore` paths continue to resolve unchanged (WID-01 path
28//! preserved).
29
30use 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
40// ── Re-export so existing `ui::input::EditCore` paths keep working ────────────
41
42/// Re-export of [`rlvgl_core::edit::EditCore`] for backward compatibility.
43///
44/// Widgets and test code that previously used `rlvgl_ui::input::EditCore`
45/// continue to resolve here.  New code should import from
46/// [`rlvgl_core::edit`] directly.
47pub use rlvgl_core::edit::EditCore;
48
49/// Re-exported default char width constant (WID-00 §6.3).
50pub use rlvgl_core::edit::DEFAULT_CHAR_WIDTH;
51/// Re-exported default line height constant (WID-00 §6.3).
52pub use rlvgl_core::edit::DEFAULT_LINE_HEIGHT;
53
54/// Callback type invoked when a single-line input submits (Enter).
55type SubmitCallback = Box<dyn FnMut(&str)>;
56
57/// Caret thickness in pixels.
58const CARET_WIDTH: i32 = rlvgl_core::edit::CARET_WIDTH;
59
60// ── UiEditCore: ui-layer wrapper that also owns a Label ───────────────────────
61//
62// The promoted `CoreEditCore` (from `rlvgl_core::edit`) does not own a
63// `Label` — it holds only the buffer, caret, and mutation state.  The
64// ui-layer widgets still need a `Label` for rendering and style access, so
65// we keep a thin local wrapper struct `UiEditCore` that forwards all mutation
66// calls to the inner `CoreEditCore` while keeping the `Label` in sync.
67
68struct 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        // CoreEditCore::set_text fires on_change; we also sync the label.
91        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        // Sync the label then fire on_change (mirrors original EditCore::committed).
101        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        // Delegate the gate logic to CoreEditCore but intercept the callback
109        // so we can also sync the label.
110        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/// Single-line editable text input (WID-00 §5–§8).
209///
210/// While active (see [`set_active`](Self::set_active)), consumes
211/// `KeyDown` for character insertion, backspace, caret movement, and
212/// Enter (which fires [`on_submit`](Self::on_submit) without mutating
213/// the buffer).
214#[allow(clippy::type_complexity)]
215pub struct Input {
216    core: UiEditCore,
217    on_submit: Option<SubmitCallback>,
218}
219
220impl Input {
221    /// Create a new input with the provided initial value and bounds.
222    /// The caret starts at the end of the initial text.
223    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    /// Assign the font used to render this input (FONT-00 §5); resolves to
231    /// `FONT_6X10` when unset.
232    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
233        self.core.set_font(font);
234    }
235
236    /// Register a change handler invoked after every successful edit
237    /// and on [`Self::set_text`].
238    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    /// Register a submit handler fired when Enter is pressed while
244    /// active. The buffer is not mutated by Enter.
245    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    /// Set whether this input consumes key events and return the widget.
251    pub fn active(mut self, active: bool) -> Self {
252        self.core.core.active = active;
253        self
254    }
255
256    /// Restrict insertions to characters accepted by `accept`
257    /// (evaluated after the printable-ASCII bound). Rejected characters
258    /// leave buffer and caret untouched (WID-00 §8.2).
259    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    /// Cap the buffer at `max` characters (WID-00 §8.1).
265    pub fn with_max_len(mut self, max: usize) -> Self {
266        self.core.core.max_len = Some(max);
267        self
268    }
269
270    /// Override the nominal char metrics used for caret geometry
271    /// (WID-00 §6.3). Defaults: [`DEFAULT_CHAR_WIDTH`] /
272    /// [`DEFAULT_LINE_HEIGHT`].
273    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    /// Whether this field currently consumes keys (WID-00 §7).
280    pub fn is_active(&self) -> bool {
281        self.core.core.active
282    }
283
284    /// Toggle key consumption. Applications keep at most one field
285    /// active; the framework does not track focus (WID-00 §7.1).
286    pub fn set_active(&mut self, active: bool) {
287        self.core.core.active = active;
288    }
289
290    /// Current caret position (char index, `0..=len`).
291    pub fn caret(&self) -> usize {
292        self.core.core.caret
293    }
294
295    /// Immutable access to the input style.
296    pub fn style(&self) -> &rlvgl_core::style::Style {
297        &self.core.label.style
298    }
299
300    /// Mutable access to the input style.
301    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
302        &mut self.core.label.style
303    }
304
305    /// Update the input text programmatically and trigger the change
306    /// handler if present. The caret clamps to the new length.
307    pub fn set_text(&mut self, text: &str) {
308        self.core.set_text(text);
309    }
310
311    /// Retrieve the current input text.
312    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
348/// Multi-line editable textarea (WID-00 §5–§8).
349///
350/// Identical edit model to [`Input`] except Enter inserts a newline
351/// (an ordinary edit) instead of submitting, and rendering draws each
352/// `'\n'`-split line separately.
353pub struct Textarea {
354    core: UiEditCore,
355}
356
357impl Textarea {
358    /// Create a new textarea with the provided text and bounds.
359    pub fn new(text: &str, bounds: Rect) -> Self {
360        Self {
361            core: UiEditCore::new(text, bounds, true),
362        }
363    }
364
365    /// Assign the font used to render this textarea (FONT-00 §5); resolves to
366    /// `FONT_6X10` when unset.
367    pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
368        self.core.set_font(font);
369    }
370
371    /// Register a change handler invoked after every successful edit
372    /// and on [`Self::set_text`].
373    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    /// Set whether this textarea consumes key events and return the widget.
379    pub fn active(mut self, active: bool) -> Self {
380        self.core.core.active = active;
381        self
382    }
383
384    /// Restrict insertions to characters accepted by `accept`
385    /// (newlines from Enter are exempt; WID-00 §8.2).
386    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    /// Cap the buffer at `max` characters (newlines count).
392    pub fn with_max_len(mut self, max: usize) -> Self {
393        self.core.core.max_len = Some(max);
394        self
395    }
396
397    /// Override the nominal char metrics used for caret geometry and
398    /// line pitch (WID-00 §6.3).
399    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    /// Whether this field currently consumes keys (WID-00 §7).
406    pub fn is_active(&self) -> bool {
407        self.core.core.active
408    }
409
410    /// Toggle key consumption (WID-00 §7.1).
411    pub fn set_active(&mut self, active: bool) {
412        self.core.core.active = active;
413    }
414
415    /// Current caret position (char index across the whole buffer;
416    /// newlines count one).
417    pub fn caret(&self) -> usize {
418        self.core.core.caret
419    }
420
421    /// Immutable access to the textarea style.
422    pub fn style(&self) -> &rlvgl_core::style::Style {
423        &self.core.label.style
424    }
425
426    /// Mutable access to the textarea style.
427    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
428        &mut self.core.label.style
429    }
430
431    /// Update the textarea text programmatically.
432    pub fn set_text(&mut self, text: &str) {
433        self.core.set_text(text);
434    }
435
436    /// Retrieve the textarea content.
437    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        // Clamps at both ends.
556        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        // Application-level focus switch (WID-00 §7.1).
582        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        // Rejected edits fire neither callback.
611        key(&mut input, Key::Backspace);
612        key(&mut input, Key::Backspace); // second one hits empty buffer
613        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        // A rejected char mid-buffer leaves the caret where it was.
625        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        // Backspace across the newline merges lines.
675        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    /// WID-00 §6.2: rendering is a pure function of the edit state —
686    /// typing "HI" then backspace renders byte-identically to typing
687    /// just "H" (the sim D-dump test asserts the same over the wire).
688    #[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}