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}