Skip to main content

tui_lipan/widgets/input/
mod.rs

1mod layout;
2mod node;
3mod reconcile;
4
5use std::sync::Arc;
6
7pub(crate) use layout::measure_input;
8pub use node::InputNode;
9pub(crate) use reconcile::reconcile_input;
10
11use crate::callback::{Callback, KeyHandler};
12use crate::core::element::{Element, ElementKind};
13use crate::core::event::MouseEvent;
14use crate::style::{
15    BorderStyle, CaretShape, Color, LayoutConstraints, Length, Padding, Style, StyleSlot,
16};
17use crate::text::edit::TextEditEvent;
18use crate::text::input::TextInput;
19
20/// A single-line text input.
21///
22/// Recommended state binding:
23/// `Input::bound(&state).on_change(ctx.link().callback(Msg::Changed))`
24/// and then `ev.apply_to(&mut state)` in the handler.
25/// Storing only the raw text value breaks cursor navigation and selection across rerenders.
26#[derive(Clone)]
27pub struct Input {
28    pub(crate) value: Arc<str>,
29    pub(crate) cursor: usize,
30    pub(crate) anchor: Option<usize>,
31    pub(crate) placeholder: Option<Arc<str>>,
32    pub(crate) prefix: Option<Arc<str>>,
33    pub(crate) suffix: Option<Arc<str>>,
34    pub(crate) truncate_head: bool,
35    pub(crate) style: Style,
36    pub(crate) hover_style: StyleSlot,
37    pub(crate) focus_style: StyleSlot,
38    pub(crate) focus_content_style: Style,
39    pub(crate) hover_border_style: Option<BorderStyle>,
40    pub(crate) placeholder_style: Style,
41    pub(crate) focus_placeholder_style: Style,
42    pub(crate) prefix_style: Style,
43    pub(crate) focus_prefix_style: Style,
44    pub(crate) suffix_style: Style,
45    pub(crate) focus_suffix_style: Style,
46    pub(crate) caret_shape: Option<CaretShape>,
47    pub(crate) caret_color: Option<Color>,
48    pub(crate) selection_style: StyleSlot,
49    pub(crate) border: bool,
50    /// Border style.
51    /// Default: `BorderStyle::Plain`.
52    pub(crate) border_style: BorderStyle,
53    /// Padding.
54    /// Default: `Padding { left: 1, right: 1, top: 0, bottom: 0 }`.
55    pub(crate) padding: Padding,
56    pub(crate) mask: Option<char>,
57    pub(crate) disabled: bool,
58    pub(crate) disabled_style: Style,
59    pub(crate) read_only: bool,
60    pub(crate) error: Option<Arc<str>>,
61    pub(crate) error_style: Style,
62    pub(crate) reserve_error_row: bool,
63    /// Requested width.
64    /// Default: `Length::Flex(1)`.
65    pub(crate) width: Length,
66    /// Requested height.
67    /// Default: `Length::Auto`.
68    pub(crate) height: Length,
69    pub(crate) on_change: Option<Callback<InputEvent>>,
70    pub(crate) on_edit: Option<Callback<TextEditEvent>>,
71    pub(crate) on_click: Option<Callback<MouseEvent>>,
72    pub(crate) on_key: Option<KeyHandler>,
73    pub(crate) key_interceptor: Option<KeyHandler>,
74    pub(crate) focusable: bool,
75    pub(crate) tab_stop: bool,
76    pub(crate) on_focus: Option<Callback<()>>,
77    pub(crate) on_blur: Option<Callback<()>>,
78}
79
80impl Input {
81    /// Create a new input.
82    pub fn new(value: impl Into<Arc<str>>) -> Self {
83        let value = value.into();
84        let cursor = value.len();
85        Self {
86            value,
87            cursor,
88            anchor: None,
89            placeholder: None,
90            prefix: None,
91            suffix: None,
92            truncate_head: false,
93            style: Style::default(),
94            hover_style: StyleSlot::Inherit,
95            focus_style: StyleSlot::Inherit,
96            focus_content_style: Style::default(),
97            hover_border_style: None,
98            placeholder_style: Style::default(),
99            focus_placeholder_style: Style::default(),
100            prefix_style: Style::default(),
101            focus_prefix_style: Style::default(),
102            suffix_style: Style::default(),
103            focus_suffix_style: Style::default(),
104            caret_shape: None,
105            caret_color: None,
106            selection_style: StyleSlot::Inherit,
107            border: true,
108            border_style: BorderStyle::Plain,
109            padding: Padding {
110                left: 1,
111                right: 1,
112                top: 0,
113                bottom: 0,
114            },
115            mask: None,
116            disabled: false,
117            disabled_style: Style::default(),
118            read_only: false,
119            error: None,
120            error_style: Style::default(),
121            reserve_error_row: false,
122            width: Length::Flex(1),
123            height: Length::Auto,
124            on_change: None,
125            on_edit: None,
126            on_click: None,
127            on_key: None,
128            key_interceptor: None,
129            focusable: true,
130            tab_stop: true,
131            on_focus: None,
132            on_blur: None,
133        }
134    }
135
136    /// Create a new input bound to a [`TextInput`] state bundle.
137    pub fn bound(state: &TextInput) -> Self {
138        Self::new("").bind(state)
139    }
140
141    /// Set the cursor position (byte index).
142    pub fn cursor(mut self, cursor: usize) -> Self {
143        self.cursor = cursor;
144        self
145    }
146
147    /// Set the selection anchor position (byte index).
148    /// When set, text between anchor and cursor is selected.
149    pub fn anchor(mut self, anchor: Option<usize>) -> Self {
150        self.anchor = anchor;
151        self
152    }
153
154    /// Bind the input's value, cursor, and anchor from a [`TextInput`] state bundle.
155    pub fn bind(mut self, state: &TextInput) -> Self {
156        self.value = state.text().into();
157        self.cursor = state.cursor();
158        self.anchor = state.anchor();
159        self
160    }
161
162    /// Set the placeholder (shown when empty and not focused).
163    pub fn placeholder(mut self, placeholder: impl Into<Arc<str>>) -> Self {
164        self.placeholder = Some(placeholder.into());
165        self
166    }
167
168    /// Set the prefix displayed before the input content.
169    pub fn prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
170        self.prefix = Some(prefix.into());
171        self
172    }
173
174    /// Set the suffix displayed after the input content.
175    pub fn suffix(mut self, suffix: impl Into<Arc<str>>) -> Self {
176        self.suffix = Some(suffix.into());
177        self
178    }
179
180    /// Toggle leading truncation when content overflows.
181    pub fn truncate_head(mut self, truncate_head: bool) -> Self {
182        self.truncate_head = truncate_head;
183        self
184    }
185
186    /// Set base style.
187    pub fn style(mut self, style: Style) -> Self {
188        self.style = style;
189        self
190    }
191
192    /// Set style when hovered.
193    pub fn hover_style(mut self, style: Style) -> Self {
194        self.hover_style = StyleSlot::Replace(style);
195        self
196    }
197
198    /// Extend the active theme's hover style with additional fields.
199    pub fn extend_hover_style(mut self, style: Style) -> Self {
200        self.hover_style = StyleSlot::Extend(style);
201        self
202    }
203
204    /// Inherit hover style from the active theme.
205    pub fn inherit_hover_style(mut self) -> Self {
206        self.hover_style = StyleSlot::Inherit;
207        self
208    }
209
210    /// Set hover style slot directly for composite forwarding.
211    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
212        self.hover_style = slot;
213        self
214    }
215
216    /// Set chrome/surface style when focused.
217    pub fn focus_style(mut self, style: Style) -> Self {
218        self.focus_style = StyleSlot::Replace(style);
219        self
220    }
221
222    /// Extend the active theme's focus style with additional fields.
223    pub fn extend_focus_style(mut self, style: Style) -> Self {
224        self.focus_style = StyleSlot::Extend(style);
225        self
226    }
227
228    /// Inherit focus style from the active theme.
229    pub fn inherit_focus_style(mut self) -> Self {
230        self.focus_style = StyleSlot::Inherit;
231        self
232    }
233
234    /// Set focus style slot directly for composite forwarding.
235    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
236        self.focus_style = slot;
237        self
238    }
239
240    /// Set content text style when focused.
241    pub fn focus_content_style(mut self, style: Style) -> Self {
242        self.focus_content_style = style;
243        self
244    }
245
246    /// Set border style when hovered.
247    pub fn hover_border_style(mut self, border_style: BorderStyle) -> Self {
248        self.hover_border_style = Some(border_style);
249        self
250    }
251
252    /// Set placeholder style.
253    pub fn placeholder_style(mut self, style: Style) -> Self {
254        self.placeholder_style = style;
255        self
256    }
257
258    /// Set placeholder style when focused.
259    pub fn focus_placeholder_style(mut self, style: Style) -> Self {
260        self.focus_placeholder_style = style;
261        self
262    }
263
264    /// Set prefix style.
265    pub fn prefix_style(mut self, style: Style) -> Self {
266        self.prefix_style = style;
267        self
268    }
269
270    /// Set prefix style when focused.
271    pub fn focus_prefix_style(mut self, style: Style) -> Self {
272        self.focus_prefix_style = style;
273        self
274    }
275
276    /// Set suffix style.
277    pub fn suffix_style(mut self, style: Style) -> Self {
278        self.suffix_style = style;
279        self
280    }
281
282    /// Set suffix style when focused.
283    pub fn focus_suffix_style(mut self, style: Style) -> Self {
284        self.focus_suffix_style = style;
285        self
286    }
287
288    /// Override the active theme's caret shape (bar, block, or underline).
289    pub fn caret_shape(mut self, shape: CaretShape) -> Self {
290        self.caret_shape = Some(shape);
291        self
292    }
293
294    /// Override the active theme's hardware caret color (only used for block caret rendering).
295    pub fn caret_color(mut self, color: Color) -> Self {
296        self.caret_color = Some(color);
297        self
298    }
299
300    /// Set selection highlight style.
301    pub fn selection_style(mut self, style: Style) -> Self {
302        self.selection_style = StyleSlot::Replace(style);
303        self
304    }
305
306    /// Extend the active theme's selection style with additional fields.
307    pub fn extend_selection_style(mut self, style: Style) -> Self {
308        self.selection_style = StyleSlot::Extend(style);
309        self
310    }
311
312    /// Inherit selection style from the active theme.
313    pub fn inherit_selection_style(mut self) -> Self {
314        self.selection_style = StyleSlot::Inherit;
315        self
316    }
317
318    /// Set selection style slot directly for composite forwarding.
319    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
320        self.selection_style = slot;
321        self
322    }
323
324    /// Set border.
325    pub fn border(mut self, border: bool) -> Self {
326        self.border = border;
327        self
328    }
329
330    /// Set border style.
331    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
332        self.border_style = border_style;
333        self
334    }
335
336    /// Set padding.
337    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
338        self.padding = padding.into();
339        self
340    }
341
342    /// Set mask character (e.g. '*' for passwords).
343    pub fn mask(mut self, mask: Option<char>) -> Self {
344        self.mask = mask;
345        self
346    }
347
348    /// Override requested width.
349    pub fn width(mut self, width: Length) -> Self {
350        self.width = width;
351        self
352    }
353
354    /// Override requested height.
355    pub fn height(mut self, height: Length) -> Self {
356        self.height = height;
357        self
358    }
359
360    /// Callback fired when the input value or cursor changes.
361    pub fn on_change(mut self, cb: Callback<InputEvent>) -> Self {
362        self.on_change = Some(cb);
363        self
364    }
365
366    /// Callback fired with incremental edit information.
367    pub fn on_edit(mut self, cb: Callback<TextEditEvent>) -> Self {
368        self.on_edit = Some(cb);
369        self
370    }
371
372    /// Set on-click handler.
373    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
374        self.on_click = Some(cb);
375        self
376    }
377
378    /// Set on-key handler.
379    pub fn on_key(mut self, handler: KeyHandler) -> Self {
380        self.on_key = Some(handler);
381        self
382    }
383
384    /// Set a pre-insertion key interceptor.
385    ///
386    /// This handler runs **before** text insertion in the editable path (Phase 3).
387    /// If it returns `true`, the key is consumed and neither text insertion nor
388    /// `on_key` will fire. Use this to intercept character keys (e.g. spacebar)
389    /// that would otherwise be inserted into the input.
390    pub fn key_interceptor(mut self, handler: KeyHandler) -> Self {
391        self.key_interceptor = Some(handler);
392        self
393    }
394
395    /// Set disabled state.
396    pub fn disabled(mut self, disabled: bool) -> Self {
397        self.disabled = disabled;
398        self
399    }
400
401    /// Set disabled style.
402    pub fn disabled_style(mut self, style: Style) -> Self {
403        self.disabled_style = style;
404        self
405    }
406
407    /// Set read-only mode. Allows mouse selection but blocks keyboard input.
408    pub fn read_only(mut self, read_only: bool) -> Self {
409        self.read_only = read_only;
410        self
411    }
412
413    /// Set error message to display below the input.
414    pub fn error<S>(mut self, message: Option<S>) -> Self
415    where
416        S: Into<Arc<str>>,
417    {
418        self.error = message.map(Into::into);
419        self
420    }
421
422    /// Set style for the error message text.
423    pub fn error_style(mut self, style: Style) -> Self {
424        self.error_style = style;
425        self
426    }
427
428    /// Reserve a dedicated row for error text even when no error is present.
429    pub fn reserve_error_row(mut self, reserve_error_row: bool) -> Self {
430        self.reserve_error_row = reserve_error_row;
431        self
432    }
433
434    /// Control whether the node is focusable.
435    pub fn focusable(mut self, focusable: bool) -> Self {
436        self.focusable = focusable;
437        self
438    }
439
440    /// When `false`, the input stays focusable but is skipped by Tab / Shift+Tab traversal.
441    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
442        self.tab_stop = tab_stop;
443        self
444    }
445
446    /// Set the callback fired when the input gains focus.
447    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
448        self.on_focus = Some(cb);
449        self
450    }
451
452    /// Set the callback fired when the input loses focus.
453    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
454        self.on_blur = Some(cb);
455        self
456    }
457}
458
459impl From<Input> for Element {
460    fn from(value: Input) -> Self {
461        let mut layout = LayoutConstraints::default();
462        if value.focusable {
463            let (min_w, min_h) = measure_input(&value);
464            layout.focus_min_w = min_w;
465            // Also set strict minimum height constraints to prevent collapse
466            // in tight layouts (especially Flex containers).
467            layout.min_h = Length::Px(min_h);
468        } else {
469            let (_, min_h) = measure_input(&value);
470            layout.min_h = Length::Px(min_h);
471        }
472        Element::new(ElementKind::Input(Box::new(value))).with_layout(layout)
473    }
474}
475
476/// An input change event.
477#[derive(Clone, Debug, PartialEq, Eq, Hash)]
478pub struct InputEvent {
479    /// Updated value.
480    pub value: Arc<str>,
481    /// Updated cursor position (byte index).
482    pub cursor: usize,
483    /// Selection anchor position (byte index), if any.
484    pub anchor: Option<usize>,
485}
486
487impl InputEvent {
488    /// Apply this event to a [`TextInput`] state bundle.
489    pub fn apply_to(&self, state: &mut TextInput) {
490        state.core.text = self.value.to_string();
491        state.core.cursor = crate::utils::text::clamp_cursor(&state.core.text, self.cursor);
492        state.core.anchor = self
493            .anchor
494            .map(|anchor| crate::utils::text::clamp_cursor(&state.core.text, anchor));
495    }
496}
497
498impl crate::layout::hash::LayoutHash for Input {
499    fn layout_hash(
500        &self,
501        hasher: &mut impl std::hash::Hasher,
502        _recurse: &dyn Fn(&Element) -> Option<u64>,
503    ) -> Option<()> {
504        use std::hash::Hash;
505        self.width.hash(hasher);
506        self.height.hash(hasher);
507        self.border.hash(hasher);
508        self.padding.hash(hasher);
509        self.focusable.hash(hasher);
510        self.value.hash(hasher);
511        self.placeholder.hash(hasher);
512        self.prefix.hash(hasher);
513        self.suffix.hash(hasher);
514        self.error.hash(hasher);
515        self.reserve_error_row.hash(hasher);
516        Some(())
517    }
518}