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