Skip to main content

text_input_demo/
text_input_demo.rs

1//! Text Input Demo
2//!
3//! Demonstrates the `TextInput` widget with two fields:
4//! - A bordered `TextInput`
5//! - A borderless `TextInput` drawn inside a bordered `Container`
6//!
7//! This demo routes focus + mouse selection using `UiScene` (wraps `InteractionCache`) registration,
8//! instead of relying on cached widget geometry stored in `TextInputState`.
9//!
10//! Controls:
11//! - Click to focus
12//! - Type to edit; Backspace/Delete to remove
13//! - Tab switches focus between fields
14//! - Enter copies the focused field into the output area
15//! - Drag with the mouse to select text (click → drag → release)
16//! - Press 'q' to quit
17//!
18//! Note: cursor placement uses MinUI's deferred cursor request system and is applied at
19//! `window.end_frame()?`.
20
21use minui::prelude::*;
22use minui::ui::UiScene;
23use minui::widgets::{TextInput, TextInputState};
24
25const ID_PLAIN: InteractionId = 1;
26const ID_WRAPPED: InteractionId = 2;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum Focus {
30    Plain,
31    Wrapped,
32}
33
34struct State {
35    focus: Focus,
36    plain: TextInputState,
37    wrapped: TextInputState,
38    last_submit: String,
39
40    // Immediate-mode scene/router (wraps InteractionCache + focus/capture policies)
41    ui: UiScene,
42
43    // Mouse drag tracking (Model B): only extend selection while mouse is down.
44    mouse_down: bool,
45    dragging: bool,
46}
47
48fn main() -> minui::Result<()> {
49    let mut plain = TextInputState::new();
50    plain.set_focused(true);
51
52    let mut wrapped = TextInputState::new();
53    wrapped.set_focused(false);
54
55    let initial = State {
56        focus: Focus::Plain,
57        plain,
58        wrapped,
59        last_submit: String::from("(press Enter to submit focused field)"),
60
61        ui: UiScene::new(),
62
63        mouse_down: false,
64        dragging: false,
65    };
66
67    let mut app = App::new(initial)?;
68
69    // Ignore mouse-move spam; click/drag events still work.
70    app.window_mut().mouse_mut().set_movement_tracking(false);
71
72    app.run(
73        // ============================
74        // Update
75        // ============================
76        |state, event| {
77            // Apply common scene policies:
78            // - observe mouse position
79            // - tab traversal (if enabled; this demo uses manual toggle on Tab)
80            // - click-to-focus + mouse capture
81            let _effects = state.ui.apply_policies(&event);
82
83            // Ignore noisy events we don't use in this demo.
84            if matches!(event, Event::MouseMove { .. } | Event::Unknown) {
85                return true;
86            }
87
88            // Prefer modifier-aware key events first (the keyboard handler may emit these for most keys),
89            // with legacy fallback for older event paths.
90            if let Event::KeyWithModifiers(k) = event {
91                match k.key {
92                    KeyKind::Char('q') => return false,
93                    KeyKind::Tab => {
94                        // Many backends emit modifier-aware Tab (`KeyWithModifiers`) but may not emit
95                        // the legacy `Event::Tab`. Since this demo has exactly two fields, we keep a
96                        // deterministic, backend-agnostic behavior: toggle focus between them.
97                        //
98                        // Note: UiScene's built-in tab traversal depends on per-frame registration order
99                        // (available after draw), so we intentionally do not use it here.
100                        toggle_focus(state);
101                        return true;
102                    }
103                    KeyKind::Enter => {
104                        state.last_submit = match state.focus {
105                            Focus::Plain => format!("plain: {}", state.plain.text()),
106                            Focus::Wrapped => format!("wrapped: {}", state.wrapped.text()),
107                        };
108                        return true;
109                    }
110                    _ => {}
111                }
112            }
113
114            // Quit (legacy fallback)
115            if matches!(event, Event::Character('q')) {
116                return false;
117            }
118
119            // Focus switching (legacy fallback)
120            if matches!(event, Event::Tab) {
121                // `UiScene` tab traversal relies on the current frame's registered focusables
122                // (which only exists after draw). In this demo, we keep a simple, deterministic
123                // fallback: toggle between the two fields.
124                toggle_focus(state);
125                return true;
126            }
127
128            // Mouse focus + selection, routed via InteractionCache.
129            match event {
130                Event::MouseClick { x, y, button: _ } => {
131                    state.mouse_down = true;
132                    state.dragging = false;
133
134                    // Hit-test against the most recently drawn frame's registry.
135                    let hit = state.ui.hit_test_id(x, y);
136
137                    match hit {
138                        Some(ID_PLAIN) => {
139                            set_focus(state, Focus::Plain);
140                            state.plain.click_set_cursor(x);
141                            return true;
142                        }
143                        Some(ID_WRAPPED) => {
144                            set_focus(state, Focus::Wrapped);
145                            state.wrapped.click_set_cursor(x);
146                            return true;
147                        }
148                        _ => {
149                            // Click outside: cancel drag/selection tracking.
150                            state.mouse_down = false;
151                            state.dragging = false;
152                            return true;
153                        }
154                    }
155                }
156                Event::MouseDrag { x, y: _, button: _ } => {
157                    if !state.mouse_down {
158                        return true;
159                    }
160                    state.dragging = true;
161
162                    // Selection clamps to the field bounds internally.
163                    match state.focus {
164                        Focus::Plain => {
165                            state.plain.drag_select_to(x);
166                            return true;
167                        }
168                        Focus::Wrapped => {
169                            state.wrapped.drag_select_to(x);
170                            return true;
171                        }
172                    }
173                }
174                Event::MouseRelease { x, y: _, button: _ } => {
175                    // Finalize drag (one last update on release), then stop tracking.
176                    if state.mouse_down && state.dragging {
177                        match state.focus {
178                            Focus::Plain => state.plain.drag_select_to(x),
179                            Focus::Wrapped => state.wrapped.drag_select_to(x),
180                        }
181                    }
182
183                    state.mouse_down = false;
184                    state.dragging = false;
185                    return true;
186                }
187                _ => {}
188            }
189
190            // Submit on Enter: copy current focused value to output area (legacy fallback)
191            if matches!(event, Event::Enter) {
192                state.last_submit = match state.focus {
193                    Focus::Plain => format!("plain: {}", state.plain.text()),
194                    Focus::Wrapped => format!("wrapped: {}", state.wrapped.text()),
195                };
196                return true;
197            }
198
199            // Route remaining events to the focused field.
200            let consumed = match state.focus {
201                Focus::Plain => state.plain.handle_event(event),
202                Focus::Wrapped => state.wrapped.handle_event(event),
203            };
204
205            // If the input didn't consume it, ignore
206            consumed
207        },
208        // ============================
209        // Draw
210        // ============================
211        |state, window| {
212            let (w, h) = window.get_size();
213
214            // Begin a fresh immediate-mode scene frame (clears registrations, focusables, owners).
215            state.ui.begin_frame();
216
217            // Clear any stale cursor request; focused inputs will request one during draw.
218            window.clear_cursor_request();
219
220            // Layout constants
221            let margin: u16 = 2;
222            let row_gap: u16 = 2;
223
224            // Header
225            window.write_str(
226                0,
227                0,
228                "TextInput Demo — click fields, type, Enter to submit, Tab to switch, q to quit",
229            )?;
230
231            // Compute field geometry
232            let field_w = w.saturating_sub(margin * 2);
233
234            // Plain field at y = 2.
235            let plain_y = 2;
236            let plain = TextInput::new()
237                .with_position(margin, plain_y)
238                .with_width(field_w)
239                .with_border(true)
240                .with_placeholder("Plain input (click to focus)…");
241
242            // Wrapped-in-container field below.
243            let wrapped_container_y = plain_y + row_gap + 2;
244
245            // Fixed-height container; input sits on the content line.
246            let container_h: u16 = 3;
247
248            let container = Container::new()
249                .with_position_and_size(margin, wrapped_container_y, field_w, container_h)
250                .with_border()
251                .with_border_chars(BorderChars::single_line())
252                .with_border_color(ColorPair::new(Color::LightCyan, Color::Transparent))
253                .with_title("Wrapped in Container")
254                .with_title_alignment(TitleAlignment::Left)
255                .with_padding(minui::widgets::ContainerPadding::uniform(0));
256
257            // Output area near bottom
258            let output_y = h.saturating_sub(3);
259            let output_w = w.saturating_sub(margin * 2);
260            let output_prefix = "Last submit: ";
261            let output_text = format!("{}{}", output_prefix, state.last_submit);
262            let output_line = fit_to_cells(&output_text, output_w, TabPolicy::SingleCell, true);
263
264            // Draw + register plain field
265            // NOTE: `TextInput::draw_with_id(...)` registers into an InteractionCache, so we pass
266            // the underlying cache from UiScene.
267            plain.draw_with_id(window, &mut state.plain, state.ui.cache_mut(), ID_PLAIN)?;
268
269            // Draw container + wrapped field
270            container.draw(window)?;
271
272            let inner_x = margin + 1;
273            let inner_y = wrapped_container_y + 1;
274            let inner_w = field_w.saturating_sub(2);
275
276            let wrapped = TextInput::new()
277                .with_position(inner_x, inner_y)
278                .with_width(inner_w)
279                .with_border(false)
280                .with_placeholder("Wrapped input (click to focus)…");
281
282            // Draw + register wrapped field
283            wrapped.draw_with_id(window, &mut state.wrapped, state.ui.cache_mut(), ID_WRAPPED)?;
284
285            // Output area box
286            window.write_str_colored(
287                output_y.saturating_sub(1),
288                margin,
289                "─ Output ─",
290                ColorPair::new(Color::Yellow, Color::Transparent),
291            )?;
292            window.write_str_colored(
293                output_y,
294                margin,
295                &output_line,
296                ColorPair::new(Color::LightGray, Color::Transparent),
297            )?;
298            window.write_str_colored(
299                output_y + 1,
300                margin,
301                "Tip: Tab switches focus • Enter copies focused text here",
302                ColorPair::new(Color::DarkGray, Color::Transparent),
303            )?;
304
305            window.end_frame()?;
306            Ok(())
307        },
308    )?;
309
310    Ok(())
311}
312
313fn toggle_focus(state: &mut State) {
314    let next = match state.focus {
315        Focus::Plain => Focus::Wrapped,
316        Focus::Wrapped => Focus::Plain,
317    };
318    set_focus(state, next);
319}
320
321fn set_focus(state: &mut State, focus: Focus) {
322    state.focus = focus;
323    state.plain.set_focused(focus == Focus::Plain);
324    state.wrapped.set_focused(focus == Focus::Wrapped);
325}