Skip to main content

lv_tui/widgets/
input.rs

1use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::Style;
7use crate::text::Text;
8
9/// A single-line text input widget.
10///
11/// Supports cursor movement (Home/End/Left/Right), Backspace/Delete,
12/// placeholder text, focus styling, and an optional submit callback.
13/// Use [`Input::text`] to retrieve the entered value.
14pub struct Input {
15    text: String,
16    /// Byte-offset cursor position.
17    cursor_byte: usize,
18    focused: bool,
19    placeholder: Text,
20    style: Style,
21    focus_style: Style,
22    /// Callback invoked on Enter (takes the current text, returns nothing).
23    on_submit: Option<Box<dyn FnMut(&str)>>,
24}
25
26impl Input {
27    /// Creates an empty input field.
28    pub fn new() -> Self {
29        Self {
30            text: String::new(),
31            cursor_byte: 0,
32            focused: false,
33            placeholder: Text::from(""),
34            style: Style::default(),
35            focus_style: Style::default()
36                .bg(crate::style::Color::White)
37                .fg(crate::style::Color::Black),
38            on_submit: None,
39        }
40    }
41
42    pub fn placeholder(mut self, text: impl Into<Text>) -> Self {
43        self.placeholder = text.into();
44        self
45    }
46
47    pub fn style(mut self, style: Style) -> Self {
48        self.style = style;
49        self
50    }
51
52    pub fn focus_style(mut self, style: Style) -> Self {
53        self.focus_style = style;
54        self
55    }
56
57    /// Sets a callback invoked when Enter is pressed.
58    /// Receives the current text as a `&str`.
59    pub fn on_submit(mut self, f: impl FnMut(&str) + 'static) -> Self {
60        self.on_submit = Some(Box::new(f));
61        self
62    }
63
64    pub fn text(&self) -> &str {
65        &self.text
66    }
67
68    fn clamp_cursor(&mut self) {
69        if self.cursor_byte > self.text.len() {
70            self.cursor_byte = self.text.len();
71        }
72    }
73
74    fn cursor_left(&mut self) {
75        if self.cursor_byte == 0 {
76            return;
77        }
78        // 找到前一个 char 的边界
79        if let Some((i, _)) = self.text.char_indices().rev().find(|&(i, _)| i < self.cursor_byte) {
80            self.cursor_byte = i;
81        } else {
82            self.cursor_byte = 0;
83        }
84    }
85
86    fn cursor_right(&mut self) {
87        // 找到下一个 char 边界
88        if let Some((i, _)) = self
89            .text
90            .char_indices()
91            .find(|&(i, _)| i > self.cursor_byte)
92        {
93            self.cursor_byte = i;
94        } else {
95            self.cursor_byte = self.text.len();
96        }
97    }
98
99    /// cursor_byte 之前有多少个 char(用于渲染时高亮第几个字符)
100    fn cursor_char_index(&self) -> usize {
101        self.text[..self.cursor_byte].chars().count()
102    }
103}
104
105impl Component for Input {
106    fn render(&self, cx: &mut RenderCx) {
107        let placeholder_mode = self.text.is_empty() && !self.focused;
108        let display_str: String = if placeholder_mode {
109            self.placeholder.first_text().to_string()
110        } else {
111            self.text.clone()
112        };
113
114        let base_style = if placeholder_mode {
115            Style::default().fg(crate::style::Color::Gray)
116        } else {
117            Style::default()
118        };
119
120        let cursor_style = Style::default()
121            .bg(crate::style::Color::White)
122            .fg(crate::style::Color::Black);
123
124        let cursor_char = if placeholder_mode {
125            0
126        } else {
127            self.cursor_char_index()
128        };
129        for (i, ch) in display_str.chars().enumerate() {
130            if i == cursor_char && self.focused {
131                cx.set_style(cursor_style.clone());
132            } else {
133                cx.set_style(base_style.clone());
134            }
135            cx.text(ch.to_string());
136        }
137
138        if self.cursor_byte >= self.text.len() && self.focused {
139            cx.set_style(cursor_style);
140            cx.text(" ");
141        }
142    }
143
144    fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
145        let display_str: String = if self.text.is_empty() {
146            self.placeholder.first_text().to_string()
147        } else {
148            self.text.clone()
149        };
150        let width: u16 = display_str
151            .chars()
152            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16)
153            .sum();
154        Size {
155            width: (width + 1),
156            height: 1,
157        }
158    }
159
160    fn event(&mut self, event: &Event, cx: &mut EventCx) {
161        match event {
162            Event::Focus => {
163                self.focused = true;
164                cx.invalidate_paint();
165                return;
166            }
167            Event::Blur => {
168                self.focused = false;
169                cx.invalidate_paint();
170                return;
171            }
172            _ => {}
173        }
174
175        if cx.phase() != crate::event::EventPhase::Target {
176            return;
177        }
178
179        if let Event::Key(key_event) = event {
180            // Ctrl+C: quit application
181            if key_event.key == crate::event::Key::Char('c') && key_event.modifiers.ctrl {
182                cx.quit();
183                return;
184            }
185            // Ctrl+D: delete forward, or do nothing on empty
186            if key_event.key == crate::event::Key::Char('d') && key_event.modifiers.ctrl {
187                if self.cursor_byte < self.text.len() {
188                    let end = self.text[self.cursor_byte..]
189                        .chars()
190                        .next()
191                        .map(|c| self.cursor_byte + c.len_utf8())
192                        .unwrap_or(self.cursor_byte);
193                    self.text.drain(self.cursor_byte..end);
194                    cx.invalidate_paint();
195                }
196                return;
197            }
198            if key_event.modifiers.ctrl || key_event.modifiers.alt {
199                return;
200            }
201
202            self.clamp_cursor();
203
204            match &key_event.key {
205                crate::event::Key::Enter => {
206                    if let Some(ref mut f) = self.on_submit {
207                        f(&self.text);
208                    }
209                }
210                crate::event::Key::Char(ch) => {
211                    self.text.insert(self.cursor_byte, *ch);
212                    self.cursor_byte += ch.len_utf8();
213                    cx.invalidate_paint();
214                }
215                crate::event::Key::Backspace => {
216                    if self.cursor_byte > 0 {
217                        let old = self.cursor_byte;
218                        self.cursor_left();
219                        self.text.drain(self.cursor_byte..old);
220                        cx.invalidate_paint();
221                    }
222                }
223                crate::event::Key::Delete => {
224                    if self.cursor_byte < self.text.len() {
225                        let end = self.text[self.cursor_byte..]
226                            .chars()
227                            .next()
228                            .map(|c| self.cursor_byte + c.len_utf8())
229                            .unwrap_or(self.cursor_byte);
230                        self.text.drain(self.cursor_byte..end);
231                        cx.invalidate_paint();
232                    }
233                }
234                crate::event::Key::Left => {
235                    let old = self.cursor_byte;
236                    self.cursor_left();
237                    if self.cursor_byte != old {
238                        cx.invalidate_paint();
239                    }
240                }
241                crate::event::Key::Right => {
242                    let old = self.cursor_byte;
243                    self.cursor_right();
244                    if self.cursor_byte != old {
245                        cx.invalidate_paint();
246                    }
247                }
248                crate::event::Key::Home => {
249                    if self.cursor_byte != 0 {
250                        self.cursor_byte = 0;
251                        cx.invalidate_paint();
252                    }
253                }
254                crate::event::Key::End => {
255                    if self.cursor_byte != self.text.len() {
256                        self.cursor_byte = self.text.len();
257                        cx.invalidate_paint();
258                    }
259                }
260                _ => {}
261            }
262        }
263    }
264
265    fn layout(&mut self, _rect: Rect, _cx: &mut LayoutCx) {}
266
267    fn style(&self) -> Style {
268        self.style.clone()
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    // Input tests go here
275}
276