1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7use smol_str::SmolStr;
8use ui_layout::Style;
9
10use crate::engine::bridge::text::{self, TextMeasureRequest};
11use crate::engine::layouter::types::{Color, TextFlowStyle, TextStyle};
12use crate::engine::renderer_model::{Brush, DrawCommand, FillRule, Paint, rect_path};
13use crate::engine::ui::components::input_text_types::{
14 InputTextEvent, InputTextKey, InputTextState,
15};
16use crate::engine::ui::custom_node::{ContentSize, CustomNode};
17
18const CARET_BLINK_INTERVAL_MS: u64 = 500;
20
21pub type OnValueChange = dyn Fn(&str) + Send + Sync;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26struct EditSnapshot {
27 value: String,
28 caret: usize,
29 preedit: String,
30}
31
32pub struct InputTextComponent {
34 state: Mutex<InputTextState>,
35 placeholder: SmolStr,
36 measurer: Arc<dyn text::TextMeasurer>,
37 undo_stack: Mutex<Vec<EditSnapshot>>,
38 redo_stack: Mutex<Vec<EditSnapshot>>,
39 dirty: AtomicBool,
40 blink_visible: AtomicBool,
41 blink_last_toggle: Mutex<Option<Instant>>,
42 on_value_change: Option<Arc<OnValueChange>>,
43 on_enter: Option<Arc<OnValueChange>>,
44 on_copy: Option<Arc<OnValueChange>>,
45 on_cut: Option<Arc<OnValueChange>>,
46}
47
48impl std::fmt::Debug for InputTextComponent {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("InputTextComponent")
51 .field("state", &self.state.lock().unwrap())
52 .field("placeholder", &self.placeholder)
53 .finish_non_exhaustive()
54 }
55}
56
57impl InputTextComponent {
58 pub fn new(
60 value: impl Into<String>,
61 placeholder: impl Into<SmolStr>,
62 measurer: Arc<dyn text::TextMeasurer>,
63 ) -> Self {
64 let value = value.into();
65 let caret = value.len();
66 Self {
67 state: Mutex::new(InputTextState {
68 value,
69 preedit: String::new(),
70 caret,
71 focused: false,
72 selection_start: None,
73 selection_end: None,
74 }),
75 placeholder: placeholder.into(),
76 measurer,
77 undo_stack: Mutex::new(Vec::new()),
78 redo_stack: Mutex::new(Vec::new()),
79 dirty: AtomicBool::new(true),
80 blink_visible: AtomicBool::new(true),
81 blink_last_toggle: Mutex::new(None),
82 on_value_change: None,
83 on_enter: None,
84 on_copy: None,
85 on_cut: None,
86 }
87 }
88
89 pub fn with_on_change(
91 value: impl Into<String>,
92 placeholder: impl Into<SmolStr>,
93 measurer: Arc<dyn text::TextMeasurer>,
94 on_value_change: Arc<OnValueChange>,
95 ) -> Self {
96 let mut input = Self::new(value, placeholder, measurer);
97 input.on_value_change = Some(on_value_change);
98 input
99 }
100
101 pub fn with_on_enter(
103 value: impl Into<String>,
104 placeholder: impl Into<SmolStr>,
105 measurer: Arc<dyn text::TextMeasurer>,
106 on_enter: Arc<OnValueChange>,
107 ) -> Self {
108 let mut input = Self::new(value, placeholder, measurer);
109 input.on_enter = Some(on_enter);
110 input
111 }
112
113 pub fn set_value(&self, value: impl Into<String>) {
117 let value = value.into();
118 let mut state = self.state.lock().unwrap();
119 state.value = value;
120 state.caret = state.value.len();
121 state.preedit.clear();
122 state.selection_start = None;
123 state.selection_end = None;
124 self.dirty.store(true, Ordering::Relaxed);
125 }
126
127 pub fn state(&self) -> InputTextState {
129 self.state.lock().unwrap().clone()
130 }
131
132 fn previous_boundary(value: &str, caret: usize) -> usize {
133 value[..caret]
134 .char_indices()
135 .next_back()
136 .map_or(0, |(index, _)| index)
137 }
138
139 fn next_boundary(value: &str, caret: usize) -> usize {
140 value[caret..]
141 .char_indices()
142 .nth(1)
143 .map_or(value.len(), |(offset, _)| caret + offset)
144 }
145
146 fn clamp_caret(value: &str, caret: usize) -> usize {
152 let len = value.len();
153 if caret >= len {
154 return len;
155 }
156 let mut last_boundary = 0;
157 for (i, _) in value.char_indices() {
158 if i > caret {
159 break;
160 }
161 last_boundary = i;
162 }
163 last_boundary
164 }
165
166 fn handle_key(state: &mut InputTextState, key: InputTextKey) {
167 if let (Some(start), Some(end)) = (state.selection_start, state.selection_end) {
170 match key {
171 InputTextKey::Backspace | InputTextKey::Delete => {
172 let start = Self::clamp_caret(&state.value, start);
173 let end = Self::clamp_caret(&state.value, end);
174 if end > start {
175 state.value.replace_range(start..end, "");
176 }
177 state.selection_start = None;
178 state.selection_end = None;
179 state.caret = Self::clamp_caret(&state.value, start);
180 return;
181 }
182 _ => {}
183 }
184 }
185
186 match key {
187 InputTextKey::Backspace if state.caret > 0 => {
188 let previous = Self::previous_boundary(&state.value, state.caret);
189 state.value.replace_range(previous..state.caret, "");
190 state.caret = Self::clamp_caret(&state.value, previous);
191 }
192 InputTextKey::Delete if state.caret < state.value.len() => {
193 let next = Self::next_boundary(&state.value, state.caret);
194 state.value.replace_range(state.caret..next, "");
195 state.caret = Self::clamp_caret(&state.value, state.caret);
196 }
197 InputTextKey::Left => {
198 state.caret = Self::clamp_caret(
199 &state.value,
200 Self::previous_boundary(&state.value, state.caret),
201 );
202 }
203 InputTextKey::Right => {
204 state.caret =
205 Self::clamp_caret(&state.value, Self::next_boundary(&state.value, state.caret));
206 }
207 InputTextKey::Home => state.caret = 0,
208 InputTextKey::End => state.caret = state.value.len(),
209 InputTextKey::Backspace | InputTextKey::Delete => {}
210 }
211 state.caret = Self::clamp_caret(&state.value, state.caret);
212 }
213
214 fn snapshot(&self) -> EditSnapshot {
215 let state = self.state.lock().unwrap();
216 EditSnapshot {
217 value: state.value.clone(),
218 caret: state.caret,
219 preedit: state.preedit.clone(),
220 }
221 }
222
223 fn push_undo(&self, snapshot: EditSnapshot) {
224 self.undo_stack.lock().unwrap().push(snapshot);
225 self.redo_stack.lock().unwrap().clear();
226 }
227
228 fn undo(&self) {
229 let Some(snapshot) = self.undo_stack.lock().unwrap().pop() else {
230 return;
231 };
232 self.redo_stack.lock().unwrap().push(self.snapshot());
233 let mut state = self.state.lock().unwrap();
234 state.value = snapshot.value;
235 state.caret = Self::clamp_caret(&state.value, snapshot.caret);
236 state.preedit = snapshot.preedit;
237 self.dirty.store(true, Ordering::Relaxed);
238 }
239
240 fn redo(&self) {
241 let Some(snapshot) = self.redo_stack.lock().unwrap().pop() else {
242 return;
243 };
244 self.undo_stack.lock().unwrap().push(self.snapshot());
245 let mut state = self.state.lock().unwrap();
246 state.value = snapshot.value;
247 state.caret = Self::clamp_caret(&state.value, snapshot.caret);
248 state.preedit = snapshot.preedit;
249 self.dirty.store(true, Ordering::Relaxed);
250 }
251}
252
253impl CustomNode for InputTextComponent {
254 fn draw_sized(
255 &self,
256 cmd_buf: &mut Vec<DrawCommand>,
257 text_style: &TextStyle,
258 text_flow_style: &TextFlowStyle,
259 _style: &Style,
260 size: ContentSize,
261 ) {
262 let state = self.state.lock().unwrap();
263 let mut style = text_style.clone();
264 let (display_text, placeholder) = if state.value.is_empty() && state.preedit.is_empty() {
265 (self.placeholder.as_str().to_owned(), true)
266 } else {
267 (
268 format!(
269 "{}{}{}",
270 &state.value[..state.caret],
271 state.preedit,
272 &state.value[state.caret..]
273 ),
274 false,
275 )
276 };
277 if placeholder {
278 style.color = Color(128, 128, 128, 255);
279 }
280
281 let preedit =
282 (!state.preedit.is_empty()).then_some((state.caret, state.caret + state.preedit.len()));
283 let caret = if state.focused {
286 let now = Instant::now();
287 let mut last_toggle = self.blink_last_toggle.lock().unwrap();
288 let should_toggle = match *last_toggle {
289 Some(last)
290 if now.duration_since(last)
291 >= Duration::from_millis(CARET_BLINK_INTERVAL_MS) =>
292 {
293 *last_toggle = Some(now);
294 true
295 }
296 None => {
297 *last_toggle = Some(now);
298 true
299 }
300 _ => false,
301 };
302 if should_toggle {
303 self.blink_visible.store(
304 !self.blink_visible.load(Ordering::Relaxed),
305 Ordering::Relaxed,
306 );
307 }
308 if self.blink_visible.load(Ordering::Relaxed) {
309 Some(state.caret + state.preedit.len())
310 } else {
311 None
312 }
313 } else {
314 None
315 };
316
317 draw_text_input(
318 &*self.measurer,
319 cmd_buf,
320 display_text,
321 0.0,
322 ((size.height - text_flow_style.font_size) * 0.5).max(0.0),
323 &style,
324 text_flow_style,
325 caret,
326 preedit,
327 text_style.color,
328 0.0,
329 size.height.max(0.0),
330 (size.height - 1.0).max(0.0),
331 );
332 }
333
334 fn intrinsic_size(&self) -> ContentSize {
335 ContentSize {
336 width: 200.0,
337 height: 28.0,
338 }
339 }
340
341 fn accepts_text_input(&self) -> bool {
342 true
343 }
344
345 fn set_focused(&self, focused: bool) {
346 let mut state = self.state.lock().unwrap();
347 state.focused = focused;
348 self.dirty.store(true, Ordering::Relaxed);
349 if !focused {
350 state.preedit.clear();
351 }
352 }
353
354 fn is_focused(&self) -> bool {
355 self.state.lock().unwrap().focused
356 }
357
358 fn handle_text_input(&self, event: InputTextEvent) -> bool {
359 match event {
360 InputTextEvent::Insert(text)
361 | InputTextEvent::Commit(text)
362 | InputTextEvent::Paste(text) => {
363 let text: String = text
364 .chars()
365 .filter(|character| !character.is_control())
366 .collect();
367 if text.is_empty() {
368 return true;
369 }
370 self.push_undo(self.snapshot());
371 let mut state = self.state.lock().unwrap();
372 if let (Some(start), Some(end)) = (state.selection_start, state.selection_end) {
374 let start = Self::clamp_caret(&state.value, start);
375 let end = Self::clamp_caret(&state.value, end);
376 if end > start {
377 state.value.replace_range(start..end, "");
378 state.selection_start = None;
379 state.selection_end = None;
380 state.caret = Self::clamp_caret(&state.value, start);
381 }
382 }
383 let caret = Self::clamp_caret(&state.value, state.caret);
384 state.value.insert_str(caret, &text);
385 state.caret = Self::clamp_caret(&state.value, caret + text.len());
386 state.preedit.clear();
387 let value = state.value.clone();
388 drop(state);
389 self.dirty.store(true, Ordering::Relaxed);
390 if let Some(ref cb) = self.on_value_change {
391 cb(&value);
392 }
393 }
394 InputTextEvent::Preedit(text) => {
395 let mut state = self.state.lock().unwrap();
396 if state.preedit != text {
397 state.preedit = text;
398 self.dirty.store(true, Ordering::Relaxed);
399 }
400 }
401 InputTextEvent::Key(key) => {
402 let changed = matches!(key, InputTextKey::Backspace | InputTextKey::Delete);
403 if changed {
404 self.push_undo(self.snapshot());
405 }
406 let mut state = self.state.lock().unwrap();
407 state.preedit.clear();
408 Self::handle_key(&mut state, key);
409 let value = state.value.clone();
410 drop(state);
411 self.dirty.store(true, Ordering::Relaxed);
412 if changed && let Some(ref cb) = self.on_value_change {
413 cb(&value);
414 }
415 }
416 InputTextEvent::Enter => {
417 let mut state = self.state.lock().unwrap();
418 if !state.preedit.is_empty() {
419 state.preedit.clear();
420 self.dirty.store(true, Ordering::Relaxed);
421 }
422 let value = state.value.clone();
423 drop(state);
424 if let Some(ref cb) = self.on_enter {
425 cb(&value);
426 }
427 }
428 InputTextEvent::Undo => {
429 self.undo();
430 }
431 InputTextEvent::Redo => {
432 self.redo();
433 }
434 InputTextEvent::CancelComposition => {
435 let mut state = self.state.lock().unwrap();
436 if !state.preedit.is_empty() {
437 state.preedit.clear();
438 self.dirty.store(true, Ordering::Relaxed);
439 }
440 }
441 InputTextEvent::Select { start, end } => {
442 let mut state = self.state.lock().unwrap();
443 let len = state.value.len();
444 let start = start.map(|s| Self::clamp_caret(&state.value, s));
445 let end = end.map(|e| Self::clamp_caret(&state.value, e));
446 match (start, end) {
447 (Some(s), Some(e)) if s <= e => {
448 state.selection_start = Some(s);
449 state.selection_end = Some(e);
450 state.caret = Self::clamp_caret(&state.value, e);
451 }
452 (Some(s), None) => {
453 state.selection_start = Some(s);
454 state.selection_end = Some(len);
455 state.caret = Self::clamp_caret(&state.value, len);
456 }
457 (None, Some(e)) => {
458 state.selection_start = Some(0);
459 state.selection_end = Some(e);
460 state.caret = Self::clamp_caret(&state.value, e);
461 }
462 _ => {
463 state.selection_start = None;
465 state.selection_end = None;
466 }
467 }
468 self.dirty.store(true, Ordering::Relaxed);
469 }
470 InputTextEvent::Copy => {
471 let state = self.state.lock().unwrap();
472 if let (Some(start), Some(end)) = (state.selection_start, state.selection_end) {
473 let start = Self::clamp_caret(&state.value, start);
474 let end = Self::clamp_caret(&state.value, end);
475 if end > start
476 && let Some(ref cb) = self.on_copy
477 {
478 cb(&state.value[start..end]);
479 }
480 }
481 }
482 InputTextEvent::Cut => {
483 self.push_undo(self.snapshot());
484 let mut state = self.state.lock().unwrap();
485 if let (Some(start), Some(end)) = (state.selection_start, state.selection_end) {
486 let start = Self::clamp_caret(&state.value, start);
487 let end = Self::clamp_caret(&state.value, end);
488 if end > start {
489 let selected = state.value[start..end].to_string();
490 state.value.replace_range(start..end, "");
491 state.selection_start = None;
492 state.selection_end = None;
493 state.caret = Self::clamp_caret(&state.value, start);
494 self.dirty.store(true, Ordering::Relaxed);
495 if let Some(ref cb) = self.on_copy {
496 cb(&selected);
497 }
498 if let Some(ref cb) = self.on_cut {
499 cb(&selected);
500 }
501 return true;
502 }
503 }
504 state.selection_start = None;
505 state.selection_end = None;
506 self.dirty.store(true, Ordering::Relaxed);
507 }
508 InputTextEvent::SelectAll => {
509 let mut state = self.state.lock().unwrap();
510 let len = state.value.len();
511 if len > 0 {
512 state.selection_start = Some(0);
513 state.selection_end = Some(len);
514 state.caret = len;
515 } else {
516 state.selection_start = None;
517 state.selection_end = None;
518 state.caret = 0;
519 }
520 self.dirty.store(true, Ordering::Relaxed);
521 }
522 }
523 true
524 }
525
526 fn is_composing(&self) -> bool {
527 !self.state.lock().unwrap().preedit.is_empty()
528 }
529
530 fn needs_repaint(&self) -> bool {
531 if self.is_focused() {
534 return true;
535 }
536 self.dirty.swap(false, Ordering::Relaxed)
537 }
538
539 fn composition_rect(&self) -> Option<(f32, f32, f32, f32)> {
540 let state = self.state.lock().unwrap();
541 if state.preedit.is_empty() {
542 return None;
543 }
544 let display_text = format!(
545 "{}{}{}",
546 &state.value[..state.caret],
547 state.preedit,
548 &state.value[state.caret..]
549 );
550 let style = TextStyle::default();
551 let Ok(fragments) = self.measurer.measure(&TextMeasureRequest {
552 text: display_text,
553 attribute: text::TextAttribute {
554 style,
555 flow_style: TextFlowStyle::default(),
556 },
557 }) else {
558 return None;
559 };
560 let mut byte_offset = 0;
561 let mut width = 0.0;
562 let mut start_x = None;
563 let mut preedit_width = 0.0;
564 let preedit_start = state.caret;
565 let preedit_end = state.caret + state.preedit.len();
566 for fragment in &fragments {
567 let frag_start = byte_offset;
568 let frag_end = byte_offset + fragment.text.len();
569 if start_x.is_none() && frag_start >= preedit_start {
570 start_x = Some(width);
571 }
572 if frag_end <= preedit_end && frag_start < preedit_end {
573 preedit_width += fragment.width;
574 }
575 byte_offset = frag_end;
576 width += fragment.width;
577 }
578 let start_x = start_x.unwrap_or(width);
579 Some((start_x, 0.0, preedit_width.max(0.0), 28.0_f32.max(0.0)))
580 }
581
582 fn role(&self) -> Option<&'static str> {
583 Some("textbox")
584 }
585
586 fn label(&self) -> Option<String> {
587 (!self.placeholder.is_empty()).then(|| self.placeholder.to_string())
588 }
589
590 fn value(&self) -> Option<String> {
591 Some(self.state.lock().unwrap().value.clone())
592 }
593}
594
595#[allow(clippy::too_many_arguments)]
597fn draw_text_input(
598 measurer: &dyn text::TextMeasurer,
599 cmd_buf: &mut Vec<DrawCommand>,
600 text: String,
601 x: f32,
602 y: f32,
603 style: &TextStyle,
604 flow_style: &TextFlowStyle,
605 caret: Option<usize>,
606 preedit: Option<(usize, usize)>,
607 decoration_color: Color,
608 caret_top: f32,
609 caret_height: f32,
610 underline_y: f32,
611) {
612 let Ok(fragments) = measurer.measure(&TextMeasureRequest {
613 text: text.clone(),
614 attribute: text::TextAttribute {
615 style: style.clone(),
616 flow_style: *flow_style,
617 },
618 }) else {
619 return;
620 };
621
622 let mut caret_x = None;
623 let mut preedit_start_x = None;
624 let mut preedit_end_x = None;
625
626 let mut byte_offset = 0;
627 let mut width = 0.0;
628
629 for fragment in &fragments {
630 if let Some(caret_pos) = caret
631 && caret_x.is_none()
632 && caret_pos <= byte_offset
633 {
634 caret_x = Some(width);
635 }
636
637 if let Some((start, end)) = preedit {
638 if preedit_start_x.is_none() && start <= byte_offset {
639 preedit_start_x = Some(width);
640 }
641
642 if preedit_end_x.is_none() && end <= byte_offset {
643 preedit_end_x = Some(width);
644 }
645 }
646
647 byte_offset += fragment.text.len();
648 width += fragment.width;
649 }
650
651 if let Some(caret_pos) = caret
652 && caret_x.is_none()
653 && caret_pos <= byte_offset
654 {
655 caret_x = Some(width);
656 }
657
658 if let Some((start, end)) = preedit {
659 if preedit_start_x.is_none() && start <= byte_offset {
660 preedit_start_x = Some(width);
661 }
662
663 if preedit_end_x.is_none() && end <= byte_offset {
664 preedit_end_x = Some(width);
665 }
666 }
667
668 let paint = Paint {
669 brush: Brush::Solid(decoration_color),
670 opacity: 1.0,
671 };
672
673 if let (Some(start_x), Some(end_x)) = (preedit_start_x, preedit_end_x) {
674 cmd_buf.push(DrawCommand::Fill {
675 path: rect_path(x + start_x, underline_y, (end_x - start_x).max(0.0), 1.0),
676 rule: FillRule::NonZero,
677 paint: paint.clone(),
678 });
679 }
680
681 if let Some(caret_x) = caret_x {
682 cmd_buf.push(DrawCommand::Fill {
683 path: rect_path(x + caret_x, caret_top, 1.0, caret_height),
684 rule: FillRule::NonZero,
685 paint,
686 });
687 }
688
689 cmd_buf.push(DrawCommand::DrawText {
690 x,
691 y,
692 text: text.into(),
693 style: style.clone(),
694 flow_style: *flow_style,
695 });
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 use crate::engine::bridge::text::FallbackTextMeasurer;
703
704 fn make_component(value: &str, placeholder: &str) -> InputTextComponent {
705 InputTextComponent::new(value, placeholder, Arc::new(FallbackTextMeasurer))
706 }
707
708 #[test]
709 fn preedit_is_replaced_by_ime_commit() {
710 let input = make_component("abc", "");
711 input.handle_text_input(InputTextEvent::Preedit("にほ".into()));
712 assert_eq!(input.state().preedit, "にほ");
713
714 input.handle_text_input(InputTextEvent::Commit("日本".into()));
715 let state = input.state();
716 assert_eq!(state.value, "abc日本");
717 assert!(state.preedit.is_empty());
718 }
719
720 #[test]
721 fn editing_uses_utf8_character_boundaries() {
722 let input = make_component("a日b", "");
723 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
724 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
725 assert_eq!(input.state().value, "ab");
726 }
727
728 #[test]
729 fn undo_redo_restores_value_and_caret() {
730 let input = make_component("", "");
731 input.handle_text_input(InputTextEvent::Insert("abc".into()));
732 assert_eq!(input.state().value, "abc");
733
734 input.handle_text_input(InputTextEvent::Undo);
735 assert_eq!(input.state().value, "");
736
737 input.handle_text_input(InputTextEvent::Redo);
738 assert_eq!(input.state().value, "abc");
739 }
740
741 #[test]
742 fn paste_inserts_at_caret() {
743 let input = make_component("ab", "");
744 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
745 input.handle_text_input(InputTextEvent::Paste("XY".into()));
746 assert_eq!(input.state().value, "aXYb");
747 }
748
749 #[test]
750 fn enter_keeps_value_and_clears_preedit() {
751 let input = make_component("abc", "");
752 input.handle_text_input(InputTextEvent::Preedit("にほ".into()));
753 input.handle_text_input(InputTextEvent::Enter);
754 let state = input.state();
755 assert_eq!(state.value, "abc");
756 assert!(state.preedit.is_empty());
757 }
758
759 #[test]
760 fn role_and_value_for_accessibility() {
761 let input = make_component("hello", "Name");
762 assert_eq!(input.role(), Some("textbox"));
763 assert_eq!(input.label(), Some("Name".to_string()));
764 assert_eq!(input.value(), Some("hello".to_string()));
765 }
766
767 #[test]
768 fn on_value_change_callback_fires() {
769 use std::sync::{Arc, Mutex};
770 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
771 let received_clone = Arc::clone(&received);
772 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
773 received_clone.lock().unwrap().push(v.to_string());
774 });
775 let input = InputTextComponent::with_on_change("", "", Arc::new(FallbackTextMeasurer), cb);
776
777 input.handle_text_input(InputTextEvent::Insert("hello".into()));
778 assert_eq!(*received.lock().unwrap(), vec!["hello"]);
779
780 input.handle_text_input(InputTextEvent::Insert(" world".into()));
781 assert_eq!(*received.lock().unwrap(), vec!["hello", "hello world"]);
782 }
783
784 #[test]
785 fn on_enter_callback_fires_with_current_value() {
786 use std::sync::{Arc, Mutex};
787 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
788 let received_clone = Arc::clone(&received);
789 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
790 received_clone.lock().unwrap().push(v.to_string());
791 });
792 let input = InputTextComponent::with_on_enter("", "", Arc::new(FallbackTextMeasurer), cb);
793
794 input.handle_text_input(InputTextEvent::Insert("https://example.com".into()));
795 input.handle_text_input(InputTextEvent::Enter);
796 assert_eq!(*received.lock().unwrap(), vec!["https://example.com"]);
797 }
798
799 #[test]
800 fn set_value_updates_value_and_caret() {
801 let input = make_component("old", "");
802 input.set_value("new value");
803 let state = input.state();
804 assert_eq!(state.value, "new value");
805 assert_eq!(state.caret, "new value".len());
806 assert!(input.needs_repaint());
807 }
808
809 #[test]
810 fn caret_clamped_after_backspace_at_start() {
811 let input = make_component("abc", "");
812 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
813 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
814 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
815 let state = input.state();
816 assert_eq!(state.value, "");
817 assert_eq!(state.caret, 0);
818 }
819
820 #[test]
821 fn caret_clamped_after_delete_at_end() {
822 let input = make_component("abc", "");
823 input.handle_text_input(InputTextEvent::Key(InputTextKey::Delete));
825 let state = input.state();
826 assert_eq!(state.value, "abc");
827 assert_eq!(state.caret, 3);
828
829 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
831 input.handle_text_input(InputTextEvent::Key(InputTextKey::Delete));
832 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
833 input.handle_text_input(InputTextEvent::Key(InputTextKey::Delete));
834 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
835 input.handle_text_input(InputTextEvent::Key(InputTextKey::Delete));
836 let state = input.state();
837 assert_eq!(state.value, "");
838 assert_eq!(state.caret, 0);
839 }
840
841 #[test]
842 fn caret_clamped_after_set_value_shortens_text() {
843 let input = make_component("hello world", "");
844 input.set_value("hi");
845 let state = input.state();
846 assert_eq!(state.value, "hi");
847 assert_eq!(state.caret, 2);
848 }
849
850 #[test]
851 fn caret_clamped_after_undo_from_stale_snapshot() {
852 let input = make_component("abc", "");
853 input.handle_text_input(InputTextEvent::Insert("日".into()));
854 assert_eq!(input.state().value, "abc日");
855 assert_eq!(input.state().caret, 6);
856
857 input.handle_text_input(InputTextEvent::Undo);
859 let state = input.state();
860 assert_eq!(state.value, "abc");
861 assert_eq!(state.caret, 3);
862 }
863
864 #[test]
865 fn caret_clamped_after_redo_from_stale_snapshot() {
866 let input = make_component("abc", "");
867 input.handle_text_input(InputTextEvent::Insert("日".into()));
868 input.handle_text_input(InputTextEvent::Undo);
869 let state = input.state();
870 assert_eq!(state.value, "abc");
871 assert_eq!(state.caret, 3);
872
873 input.handle_text_input(InputTextEvent::Redo);
875 let state = input.state();
876 assert_eq!(state.value, "abc日");
877 assert_eq!(state.caret, 6);
878 }
879
880 #[test]
881 fn caret_stays_at_utf8_boundary_with_multibyte_chars() {
882 let input = make_component("a日b", "");
883 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
885 assert_eq!(input.state().caret, 4);
886 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
888 assert_eq!(input.state().caret, 1);
889 input.handle_text_input(InputTextEvent::Key(InputTextKey::Left));
891 assert_eq!(input.state().caret, 0);
892 input.handle_text_input(InputTextEvent::Key(InputTextKey::Right));
894 assert_eq!(input.state().caret, 1);
895 input.handle_text_input(InputTextEvent::Key(InputTextKey::Right));
897 assert_eq!(input.state().caret, 4);
898 input.handle_text_input(InputTextEvent::Key(InputTextKey::Right));
900 assert_eq!(input.state().caret, 5);
901 }
902
903 #[test]
904 fn select_all_selects_entire_text() {
905 let input = make_component("abc", "");
906 input.handle_text_input(InputTextEvent::SelectAll);
907 let state = input.state();
908 assert_eq!(state.selection_start, Some(0));
909 assert_eq!(state.selection_end, Some(3));
910 assert_eq!(state.caret, 3);
911 }
912
913 #[test]
914 fn select_all_on_empty_clears_selection() {
915 let input = make_component("", "");
916 input.handle_text_input(InputTextEvent::SelectAll);
917 let state = input.state();
918 assert_eq!(state.selection_start, None);
919 assert_eq!(state.selection_end, None);
920 assert_eq!(state.caret, 0);
921 }
922
923 #[test]
924 fn select_none_clears_selection() {
925 let input = make_component("abc", "");
926 input.handle_text_input(InputTextEvent::SelectAll);
927 assert!(input.state().selection_start.is_some());
928 input.handle_text_input(InputTextEvent::Select {
929 start: None,
930 end: None,
931 });
932 let state = input.state();
933 assert_eq!(state.selection_start, None);
934 assert_eq!(state.selection_end, None);
935 }
936
937 #[test]
938 fn typing_with_selection_replaces_selection() {
939 let input = make_component("abc", "");
940 input.handle_text_input(InputTextEvent::SelectAll);
941 input.handle_text_input(InputTextEvent::Insert("X".into()));
942 let state = input.state();
943 assert_eq!(state.value, "X");
944 assert_eq!(state.caret, 1);
945 assert_eq!(state.selection_start, None);
946 assert_eq!(state.selection_end, None);
947 }
948
949 #[test]
950 fn backspace_with_selection_deletes_selection() {
951 let input = make_component("abc", "");
952 input.handle_text_input(InputTextEvent::SelectAll);
953 input.handle_text_input(InputTextEvent::Key(InputTextKey::Backspace));
954 let state = input.state();
955 assert_eq!(state.value, "");
956 assert_eq!(state.caret, 0);
957 assert_eq!(state.selection_start, None);
958 assert_eq!(state.selection_end, None);
959 }
960
961 #[test]
962 fn delete_with_selection_deletes_selection() {
963 let input = make_component("abc", "");
964 input.handle_text_input(InputTextEvent::SelectAll);
965 input.handle_text_input(InputTextEvent::Key(InputTextKey::Delete));
966 let state = input.state();
967 assert_eq!(state.value, "");
968 assert_eq!(state.caret, 0);
969 assert_eq!(state.selection_start, None);
970 assert_eq!(state.selection_end, None);
971 }
972
973 #[test]
974 fn copy_fires_callback_with_selected_text() {
975 use std::sync::{Arc, Mutex};
976 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
977 let received_clone = Arc::clone(&received);
978 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
979 received_clone.lock().unwrap().push(v.to_string());
980 });
981 let mut input = make_component("abc", "");
982 input.on_copy = Some(cb);
983 input.handle_text_input(InputTextEvent::SelectAll);
984 input.handle_text_input(InputTextEvent::Copy);
985 assert_eq!(*received.lock().unwrap(), vec!["abc"]);
986 }
987
988 #[test]
989 fn cut_fires_callbacks_and_removes_selection() {
990 use std::sync::{Arc, Mutex};
991 let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
992 let received_clone = Arc::clone(&received);
993 let cb: Arc<OnValueChange> = Arc::new(move |v: &str| {
994 received_clone.lock().unwrap().push(v.to_string());
995 });
996 let mut input = make_component("abc", "");
997 input.on_copy = Some(Arc::clone(&cb));
998 input.on_cut = Some(cb);
999 input.handle_text_input(InputTextEvent::SelectAll);
1000 input.handle_text_input(InputTextEvent::Cut);
1001 assert_eq!(*received.lock().unwrap(), vec!["abc", "abc"]);
1002 let state = input.state();
1003 assert_eq!(state.value, "");
1004 assert_eq!(state.caret, 0);
1005 assert_eq!(state.selection_start, None);
1006 assert_eq!(state.selection_end, None);
1007 }
1008
1009 #[test]
1010 fn set_value_clears_selection() {
1011 let input = make_component("abc", "");
1012 input.handle_text_input(InputTextEvent::SelectAll);
1013 assert!(input.state().selection_start.is_some());
1014 input.set_value("xyz");
1015 let state = input.state();
1016 assert_eq!(state.value, "xyz");
1017 assert_eq!(state.caret, 3);
1018 assert_eq!(state.selection_start, None);
1019 assert_eq!(state.selection_end, None);
1020 }
1021
1022 #[test]
1023 fn partial_selection_insert_replaces_range() {
1024 let input = make_component("hello", "");
1025 input.handle_text_input(InputTextEvent::Select {
1026 start: Some(1),
1027 end: Some(4),
1028 });
1029 input.handle_text_input(InputTextEvent::Insert("X".into()));
1030 let state = input.state();
1031 assert_eq!(state.value, "hXo");
1032 assert_eq!(state.caret, 2);
1033 assert_eq!(state.selection_start, None);
1034 assert_eq!(state.selection_end, None);
1035 }
1036}