1use std::time::Instant;
24
25use crate::event::{UiEvent, keys as key};
26use crate::layout::LayoutNode;
27use crate::text;
28use crate::widget::{TEXT_BOX_PAD_X, Widget, WidgetKind};
29
30const BLINK_MS: u128 = 530;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum TextBoxEvent {
36 Ignored,
39 Handled,
41 Changed,
43 Submitted,
45}
46
47impl TextBoxEvent {
48 pub fn consumed(self) -> bool {
51 self != TextBoxEvent::Ignored
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct TextBoxState {
58 text: String,
59 caret: usize,
61 anchor: usize,
63 first: usize,
65 focused: bool,
66 dragging: bool,
68 last_input: Option<Instant>,
70 max_length: usize,
72 pad_l: f32,
73 pad_r: f32,
74 font_scale: f32,
75 font: String,
76 on_change: Option<String>,
77 on_submit: Option<String>,
78}
79
80impl Default for TextBoxState {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86fn byte_at(s: &str, i: usize) -> usize {
88 s.char_indices().nth(i).map_or(s.len(), |(b, _)| b)
89}
90
91fn allowed(c: char) -> bool {
93 c != '§' && c >= ' ' && c != '\u{7f}'
94}
95
96impl TextBoxState {
97 pub const fn new() -> Self {
98 Self {
99 text: String::new(),
100 caret: 0,
101 anchor: 0,
102 first: 0,
103 focused: false,
104 dragging: false,
105 last_input: None,
106 max_length: 0,
107 pad_l: TEXT_BOX_PAD_X,
108 pad_r: TEXT_BOX_PAD_X,
109 font_scale: 1.0,
110 font: String::new(),
111 on_change: None,
112 on_submit: None,
113 }
114 }
115
116 pub fn text(&self) -> &str {
117 &self.text
118 }
119
120 pub fn set_text(&mut self, text: impl Into<String>) {
122 self.text = text.into();
123 if self.max_length > 0 {
124 let cut = byte_at(&self.text, self.max_length);
125 self.text.truncate(cut);
126 }
127 self.caret = self.len();
128 self.anchor = self.caret;
129 self.first = 0;
130 }
131
132 pub fn clear(&mut self) {
134 self.set_text("");
135 }
136
137 pub fn is_focused(&self) -> bool {
138 self.focused
139 }
140
141 pub fn focus(&mut self) {
143 self.focused = true;
144 self.caret = self.len();
145 self.anchor = self.caret;
146 self.touch();
147 }
148
149 pub fn blur(&mut self) {
151 self.focused = false;
152 self.dragging = false;
153 self.anchor = self.caret;
154 }
155
156 pub fn caret(&self) -> usize {
158 self.caret
159 }
160
161 pub fn selection(&self) -> (usize, usize) {
163 (self.caret.min(self.anchor), self.caret.max(self.anchor))
164 }
165
166 pub fn select_all(&mut self) {
168 self.anchor = 0;
169 self.caret = self.len();
170 }
171
172 pub fn event_name(&self, ev: TextBoxEvent) -> Option<&str> {
176 match ev {
177 TextBoxEvent::Changed => self.on_change.as_deref(),
178 TextBoxEvent::Submitted => self.on_submit.as_deref(),
179 _ => None,
180 }
181 }
182
183 fn len(&self) -> usize {
184 self.text.chars().count()
185 }
186
187 fn touch(&mut self) {
188 self.last_input = Some(Instant::now());
189 }
190
191 pub fn apply(&mut self, w: &mut Widget) {
195 let WidgetKind::TextBox(tb) = &mut w.kind else {
196 return;
197 };
198 self.max_length = tb.max_length;
199 self.on_change = tb.on_change.clone();
200 self.on_submit = tb.on_submit.clone();
201 self.pad_l = w.style.pad[3];
202 self.pad_r = w.style.pad[1];
203 self.font_scale = w.style.font_scale;
204 self.font = w.style.font.clone();
205 if self.max_length > 0 && self.len() > self.max_length {
206 let cut = byte_at(&self.text, self.max_length);
207 self.text.truncate(cut);
208 }
209 let n = self.len();
210 self.caret = self.caret.min(n);
211 self.anchor = self.anchor.min(n);
212 self.first = self.first.min(self.caret);
213 tb.text = self.text.clone();
214 tb.caret = self.focused.then_some(self.caret);
215 tb.caret_visible = self.focused
216 && self
217 .last_input
218 .is_none_or(|t| (t.elapsed().as_millis() / BLINK_MS).is_multiple_of(2));
219 tb.selection = if self.focused {
220 self.selection()
221 } else {
222 (0, 0)
223 };
224 tb.first = self.first;
225 w.focused = self.focused;
226 }
227
228 fn inner_w(&self, node: &LayoutNode) -> f32 {
230 (node.rect.w - 2.0 - self.pad_l - self.pad_r).max(0.0)
231 }
232
233 fn char_w(&self, c: char) -> f32 {
234 text::char_width_in(c, &self.font) * self.font_scale
235 }
236
237 fn scroll_to_caret(&mut self, node: &LayoutNode) {
239 self.first = text_box_first(
240 &self.text,
241 self.first,
242 self.caret,
243 self.inner_w(node),
244 |c| self.char_w(c),
245 );
246 }
247
248 fn index_at(&self, node: &LayoutNode, mx: f32) -> usize {
250 let mut x = node.rect.x + 1.0 + self.pad_l;
251 let mut i = self.first;
252 for c in self.text.chars().skip(self.first) {
253 let w = self.char_w(c);
254 if mx < x + w / 2.0 {
255 return i;
256 }
257 x += w;
258 i += 1;
259 }
260 i
261 }
262
263 fn delete_selection(&mut self) -> bool {
265 let (a, b) = self.selection();
266 if a == b {
267 return false;
268 }
269 let (ba, bb) = (byte_at(&self.text, a), byte_at(&self.text, b));
270 self.text.replace_range(ba..bb, "");
271 self.caret = a;
272 self.anchor = a;
273 true
274 }
275
276 fn insert(&mut self, c: char) -> bool {
277 let had_selection = self.delete_selection();
278 if self.max_length > 0 && self.len() >= self.max_length {
279 return had_selection;
280 }
281 let at = byte_at(&self.text, self.caret);
282 self.text.insert(at, c);
283 self.caret += 1;
284 self.anchor = self.caret;
285 true
286 }
287
288 fn move_to(&mut self, i: usize, extend: bool) {
289 self.caret = i.min(self.len());
290 if !extend {
291 self.anchor = self.caret;
292 }
293 }
294
295 pub fn handle(&mut self, event: &str, node: &LayoutNode) -> TextBoxEvent {
298 self.handle_event(&UiEvent::parse(event), node)
299 }
300
301 pub fn handle_event(&mut self, event: &UiEvent<'_>, node: &LayoutNode) -> TextBoxEvent {
303 let result = self.handle_inner(event, node);
304 if result.consumed() {
305 self.touch();
306 self.scroll_to_caret(node);
307 }
308 result
309 }
310
311 fn handle_inner(&mut self, event: &UiEvent<'_>, node: &LayoutNode) -> TextBoxEvent {
312 let (k, mods) = match *event {
313 UiEvent::Click { x: mx, y: my } => {
314 if node.visible && node.enabled && node.rect.contains(mx, my) {
315 self.focused = true;
316 self.dragging = true;
317 let i = self.index_at(node, mx);
318 self.move_to(i, false);
319 return TextBoxEvent::Handled;
320 }
321 if self.focused {
322 self.blur();
323 }
324 return TextBoxEvent::Ignored;
325 }
326 UiEvent::Drag { x: mx, .. } => {
327 if !self.dragging {
328 return TextBoxEvent::Ignored;
329 }
330 let i = self.index_at(node, mx);
331 self.move_to(i, true);
332 return TextBoxEvent::Handled;
333 }
334 UiEvent::Release { .. } => {
335 return if std::mem::take(&mut self.dragging) {
336 TextBoxEvent::Handled
337 } else {
338 TextBoxEvent::Ignored
339 };
340 }
341 _ if !self.focused => return TextBoxEvent::Ignored,
342 UiEvent::Char(c) => {
343 if !allowed(c) {
344 return TextBoxEvent::Handled;
345 }
346 return if self.insert(c) {
347 TextBoxEvent::Changed
348 } else {
349 TextBoxEvent::Handled
350 };
351 }
352 UiEvent::Key { key, mods } => (key, mods),
353 _ => return TextBoxEvent::Ignored,
354 };
355 let shift = mods & key::MOD_SHIFT != 0;
356 let ctrl = mods & (key::MOD_CONTROL | key::MOD_SUPER) != 0;
357 match k {
358 key::ESCAPE => self.blur(),
359 key::ENTER | key::KP_ENTER => return TextBoxEvent::Submitted,
360 key::BACKSPACE => {
361 if !self.delete_selection() {
362 if self.caret == 0 {
363 return TextBoxEvent::Handled;
364 }
365 self.anchor = self.caret - 1;
366 self.delete_selection();
367 }
368 return TextBoxEvent::Changed;
369 }
370 key::DELETE => {
371 if !self.delete_selection() {
372 if self.caret >= self.len() {
373 return TextBoxEvent::Handled;
374 }
375 self.anchor = self.caret + 1;
376 self.delete_selection();
377 }
378 return TextBoxEvent::Changed;
379 }
380 key::LEFT => {
381 let (a, b) = self.selection();
382 let to = if !shift && a != b {
383 a
384 } else {
385 self.caret.saturating_sub(1)
386 };
387 self.move_to(to, shift);
388 }
389 key::RIGHT => {
390 let (a, b) = self.selection();
391 let to = if !shift && a != b { b } else { self.caret + 1 };
392 self.move_to(to, shift);
393 }
394 key::HOME => self.move_to(0, shift),
395 key::END => self.move_to(self.len(), shift),
396 key::A if ctrl => self.select_all(),
397 _ => {}
400 }
401 TextBoxEvent::Handled
402 }
403}
404
405pub(crate) fn text_box_first(
409 text: &str,
410 first: usize,
411 caret: usize,
412 inner_w: f32,
413 char_w: impl Fn(char) -> f32,
414) -> usize {
415 let mut first = first.min(caret);
416 let widths: Vec<f32> = text.chars().map(char_w).collect();
417 let caret = caret.min(widths.len());
418 while first < caret && widths[first..caret].iter().sum::<f32>() > inner_w {
419 first += 1;
420 }
421 first
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::layout::{self, FlexDir};
428 use crate::widget;
429
430 fn node() -> LayoutNode {
431 let root = widget::panel(FlexDir::Column)
432 .child(widget::text_box("").id("box").w(50.0).at(10.0, 10.0));
433 let n = layout::compute(&root, 300.0, 300.0);
434 layout::find_node(&n, "box").unwrap().clone()
435 }
436
437 fn typed(s: &mut TextBoxState, n: &LayoutNode, text: &str) {
438 for c in text.chars() {
439 assert_eq!(
440 s.handle(&format!("char:{}", c as u32), n),
441 TextBoxEvent::Changed
442 );
443 }
444 }
445
446 #[test]
447 fn focus_type_and_edit() {
448 let n = node();
449 assert_eq!((n.rect.x, n.rect.w, n.rect.h), (10.0, 50.0, 16.0));
450 let mut s = TextBoxState::new();
451 assert_eq!(s.handle("char:65", &n), TextBoxEvent::Ignored, "unfocused");
452 assert_eq!(s.handle("click:20:15", &n), TextBoxEvent::Handled);
453 assert!(s.is_focused());
454 typed(&mut s, &n, "héllo");
455 assert_eq!(s.text(), "héllo");
456 assert_eq!(s.handle("key:259:0", &n), TextBoxEvent::Changed);
457 assert_eq!(s.text(), "héll");
458 assert_eq!(s.handle("key:268:0", &n), TextBoxEvent::Handled);
459 assert_eq!(s.handle("key:261:0", &n), TextBoxEvent::Changed);
460 assert_eq!(s.text(), "éll");
461 assert_eq!(s.handle("key:262:1", &n), TextBoxEvent::Handled);
462 assert_eq!(s.selection(), (0, 1));
463 typed(&mut s, &n, "E");
464 assert_eq!(s.text(), "Ell");
465 assert_eq!(s.handle("key:65:2", &n), TextBoxEvent::Handled);
466 assert_eq!(s.selection(), (0, 3));
467 assert_eq!(s.handle("key:259:0", &n), TextBoxEvent::Changed);
468 assert_eq!(s.text(), "");
469 assert_eq!(s.handle("key:257:0", &n), TextBoxEvent::Submitted);
470 assert_eq!(s.handle("key:256:0", &n), TextBoxEvent::Handled);
472 assert_eq!(s.handle("key:256:0", &n), TextBoxEvent::Ignored);
473 }
474
475 #[test]
476 fn click_outside_unfocuses_and_passes_on() {
477 let n = node();
478 let mut s = TextBoxState::new();
479 s.handle("click:20:15", &n);
480 assert_eq!(s.handle("click:200:200", &n), TextBoxEvent::Ignored);
481 assert!(!s.is_focused());
482 }
483
484 #[test]
485 fn max_length_and_scrolling() {
486 let n = node();
487 let mut s = TextBoxState::new();
488 let mut w = widget::text_box("").max_length(12);
489 s.apply(&mut w);
490 s.handle("click:20:15", &n);
491 for _ in 0..20 {
492 s.handle("char:120", &n); }
494 assert_eq!(s.text().len(), 12);
495 assert_eq!(s.first, 6);
497 s.handle("key:268:0", &n);
498 assert_eq!(s.first, 0);
499 s.handle("click:26:15", &n); assert_eq!(s.caret(), 2);
502 s.apply(&mut w);
503 match &w.kind {
504 WidgetKind::TextBox(tb) => {
505 assert_eq!(tb.caret, Some(2));
506 assert_eq!(tb.text.len(), 12);
507 }
508 _ => unreachable!(),
509 }
510 }
511
512 #[test]
513 fn event_names_come_from_the_widget() {
514 let mut s = TextBoxState::new();
515 let mut w = widget::text_box("").on_change("search").on_submit("go");
516 s.apply(&mut w);
517 assert_eq!(s.event_name(TextBoxEvent::Changed), Some("search"));
518 assert_eq!(s.event_name(TextBoxEvent::Submitted), Some("go"));
519 assert_eq!(s.event_name(TextBoxEvent::Handled), None);
520 }
521}