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