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