Skip to main content

rlvgl_ui/
input.rs

1// SPDX-License-Identifier: MIT
2//! Input and textarea components for rlvgl-ui.
3//!
4//! Wrap the [`rlvgl_widgets::label::Label`] widget to provide simple text
5//! fields backed by `rlvgl-widgets`.
6
7use alloc::boxed::Box;
8use rlvgl_core::{
9    event::Event,
10    renderer::Renderer,
11    widget::{Rect, Widget},
12};
13use rlvgl_widgets::label::Label;
14
15/// Callback type invoked when an input's text changes.
16type ChangeCallback = Box<dyn FnMut(&str)>;
17
18/// Single-line text input component.
19#[allow(clippy::type_complexity)]
20pub struct Input {
21    inner: Label,
22    on_change: Option<ChangeCallback>,
23}
24
25impl Input {
26    /// Create a new input with the provided initial value and bounds.
27    pub fn new(text: &str, bounds: Rect) -> Self {
28        Self {
29            inner: Label::new(text, bounds),
30            on_change: None,
31        }
32    }
33
34    /// Register a change handler invoked when [`Self::set_text`] is called.
35    pub fn on_change<F: FnMut(&str) + 'static>(mut self, handler: F) -> Self {
36        self.on_change = Some(Box::new(handler));
37        self
38    }
39
40    /// Immutable access to the input style.
41    pub fn style(&self) -> &rlvgl_core::style::Style {
42        &self.inner.style
43    }
44
45    /// Mutable access to the input style.
46    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
47        &mut self.inner.style
48    }
49
50    /// Update the input text and trigger the change handler if present.
51    pub fn set_text(&mut self, text: &str) {
52        self.inner.set_text(text);
53        if let Some(cb) = self.on_change.as_mut() {
54            cb(self.inner.text());
55        }
56    }
57
58    /// Retrieve the current input text.
59    pub fn text(&self) -> &str {
60        self.inner.text()
61    }
62}
63
64impl Widget for Input {
65    fn bounds(&self) -> Rect {
66        self.inner.bounds()
67    }
68
69    fn draw(&self, renderer: &mut dyn Renderer) {
70        self.inner.draw(renderer);
71    }
72
73    fn handle_event(&mut self, event: &Event) -> bool {
74        self.inner.handle_event(event)
75    }
76}
77
78/// Multi-line textarea component.
79pub struct Textarea {
80    inner: Input,
81}
82
83impl Textarea {
84    /// Create a new textarea with the provided text and bounds.
85    pub fn new(text: &str, bounds: Rect) -> Self {
86        Self {
87            inner: Input::new(text, bounds),
88        }
89    }
90
91    /// Register a change handler invoked when the text updates.
92    pub fn on_change<F: FnMut(&str) + 'static>(mut self, handler: F) -> Self {
93        self.inner = self.inner.on_change(handler);
94        self
95    }
96
97    /// Immutable access to the textarea style.
98    pub fn style(&self) -> &rlvgl_core::style::Style {
99        self.inner.style()
100    }
101
102    /// Mutable access to the textarea style.
103    pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
104        self.inner.style_mut()
105    }
106
107    /// Update the textarea text.
108    pub fn set_text(&mut self, text: &str) {
109        self.inner.set_text(text);
110    }
111
112    /// Retrieve the textarea content.
113    pub fn text(&self) -> &str {
114        self.inner.text()
115    }
116}
117
118impl Widget for Textarea {
119    fn bounds(&self) -> Rect {
120        self.inner.bounds()
121    }
122
123    fn draw(&self, renderer: &mut dyn Renderer) {
124        self.inner.draw(renderer);
125    }
126
127    fn handle_event(&mut self, event: &Event) -> bool {
128        self.inner.handle_event(event)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use alloc::rc::Rc;
136    use core::cell::Cell;
137    use rlvgl_core::widget::Rect;
138
139    #[test]
140    fn input_sets_text_and_calls_handler() {
141        let called = Rc::new(Cell::new(false));
142        let flag = called.clone();
143        let mut input = Input::new(
144            "hi",
145            Rect {
146                x: 0,
147                y: 0,
148                width: 10,
149                height: 10,
150            },
151        )
152        .on_change(move |_| flag.set(true));
153        input.set_text("new");
154        assert_eq!(input.text(), "new");
155        assert!(called.get());
156    }
157
158    #[test]
159    fn textarea_wraps_input() {
160        let mut area = Textarea::new(
161            "a",
162            Rect {
163                x: 0,
164                y: 0,
165                width: 10,
166                height: 20,
167            },
168        );
169        area.set_text("b");
170        assert_eq!(area.text(), "b");
171    }
172}