1#![warn(missing_docs)]
46
47use std::borrow::Cow;
48use std::panic::Location;
49
50use crate::cursor::Cursor;
51use crate::event::{LogicalKey, NamedKey, UiEvent, UiEventKind};
52use crate::metrics::MetricsRole;
53use crate::selection::{Selection, SelectionPoint, SelectionRange};
54use crate::style::StyleProfile;
55use crate::text::metrics::TextGeometry;
56use crate::tokens;
57use crate::tree::*;
58use crate::widgets::text::text;
59
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub struct TextSelection {
70 pub anchor: usize,
72 pub head: usize,
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub enum MaskMode {
88 #[default]
90 None,
91 Password,
94}
95
96const MASK_CHAR: char = '•';
97
98#[derive(Clone, Copy, Debug, Default)]
110pub struct TextInputOpts<'a> {
111 pub placeholder: Option<&'a str>,
114 pub max_length: Option<usize>,
119 pub mask: MaskMode,
121}
122
123impl<'a> TextInputOpts<'a> {
124 pub fn placeholder(mut self, p: &'a str) -> Self {
127 self.placeholder = Some(p);
128 self
129 }
130
131 pub fn max_length(mut self, n: usize) -> Self {
134 self.max_length = Some(n);
135 self
136 }
137
138 pub fn password(mut self) -> Self {
141 self.mask = MaskMode::Password;
142 self
143 }
144
145 fn is_masked(&self) -> bool {
146 !matches!(self.mask, MaskMode::None)
147 }
148}
149
150impl TextSelection {
151 pub const fn caret(head: usize) -> Self {
153 Self { anchor: head, head }
154 }
155
156 pub const fn range(anchor: usize, head: usize) -> Self {
159 Self { anchor, head }
160 }
161
162 pub fn ordered(self) -> (usize, usize) {
164 (self.anchor.min(self.head), self.anchor.max(self.head))
165 }
166
167 pub fn is_collapsed(self) -> bool {
169 self.anchor == self.head
170 }
171}
172
173#[track_caller]
208pub fn text_input(key: &str, value: &str, selection: &Selection) -> El {
209 text_input_with(key, value, selection, TextInputOpts::default())
210}
211
212#[track_caller]
217pub fn text_input_with(
218 key: &str,
219 value: &str,
220 selection: &Selection,
221 opts: TextInputOpts<'_>,
222) -> El {
223 build_text_input(value, selection.within(key), opts).key(key)
224}
225
226#[track_caller]
236fn build_text_input(value: &str, view: Option<TextSelection>, opts: TextInputOpts<'_>) -> El {
237 let selection = view.unwrap_or_default();
238 let head = clamp_to_char_boundary(value, selection.head.min(value.len()));
239 let anchor = clamp_to_char_boundary(value, selection.anchor.min(value.len()));
240 let lo = anchor.min(head);
241 let hi = anchor.max(head);
242 let line_h = line_height_px();
243
244 let display = display_str(value, opts.mask);
249
250 let geometry = single_line_geometry(&display);
254 let to_display = |b: usize| original_to_display_byte(value, b, opts.mask);
255 let head_px = geometry.prefix_width(to_display(head));
256 let lo_px = geometry.prefix_width(to_display(lo));
257 let hi_px = geometry.prefix_width(to_display(hi));
258
259 let mut children: Vec<El> = Vec::with_capacity(4);
260
261 if lo < hi {
266 children.push(
267 El::new(Kind::Custom("text_input_selection"))
268 .style_profile(StyleProfile::Solid)
269 .fill(tokens::SELECTION_BG)
270 .dim_fill(tokens::SELECTION_BG_UNFOCUSED)
271 .radius(2.0)
272 .width(Size::Fixed(hi_px - lo_px))
273 .height(Size::Fixed(line_h))
274 .translate(lo_px, 0.0),
275 );
276 }
277
278 if value.is_empty()
282 && let Some(ph) = opts.placeholder
283 {
284 children.push(
285 text(ph)
286 .muted()
287 .width(Size::Hug)
288 .height(Size::Fixed(line_h)),
289 );
290 }
291
292 children.push(
295 text(display.into_owned())
296 .width(Size::Hug)
297 .height(Size::Fixed(line_h)),
298 );
299
300 if view.is_some() {
310 children.push(
311 caret_bar()
312 .translate(head_px, 0.0)
313 .alpha_follows_focused_ancestor()
314 .blink_when_focused(),
315 );
316 }
317
318 let inner = El::new(Kind::Group)
328 .clip()
329 .width(Size::Fill(1.0))
330 .height(Size::Fill(1.0))
331 .layout(move |ctx| {
332 let x_offset = (head_px - ctx.container.w).max(0.0);
340 ctx.children
341 .iter()
342 .map(|c| {
343 let (w, h) = (ctx.measure)(c);
344 let w = match c.width {
348 Size::Fixed(v) => v,
349 Size::Hug | Size::Ch(_) => w,
353 Size::Fill(_) => ctx.container.w,
354 Size::Aspect(r) => h * r,
355 };
356 let h = match c.height {
357 Size::Fixed(v) => v,
358 Size::Hug | Size::Ch(_) => h,
359 Size::Fill(_) => ctx.container.h,
360 Size::Aspect(r) => w * r,
361 };
362 let y = ctx.container.y + (ctx.container.h - h) * 0.5;
367 Rect::new(ctx.container.x - x_offset, y, w, h)
368 })
369 .collect()
370 })
371 .children(children);
372
373 El::new(Kind::Custom("text_input"))
374 .at_loc(Location::caller())
375 .style_profile(StyleProfile::Surface)
376 .metrics_role(MetricsRole::Input)
377 .surface_role(SurfaceRole::Input)
378 .focusable()
379 .always_show_focus_ring()
382 .capture_keys()
383 .paint_overflow(Sides::all(tokens::RING_WIDTH))
384 .hit_overflow(Sides::all(tokens::HIT_OVERFLOW))
385 .cursor(Cursor::Text)
386 .fill(tokens::MUTED)
387 .stroke(tokens::BORDER)
388 .default_radius(tokens::RADIUS_MD)
389 .axis(Axis::Overlay)
390 .align(Align::Start)
391 .justify(Justify::Center)
392 .default_width(Size::Fill(1.0))
393 .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
394 .default_padding(Sides::xy(tokens::SPACE_3, 0.0))
395 .child(inner)
396}
397
398fn caret_bar() -> El {
399 El::new(Kind::Custom("text_input_caret"))
400 .style_profile(StyleProfile::Solid)
401 .fill(tokens::FOREGROUND)
402 .width(Size::Fixed(2.0))
403 .height(Size::Fixed(line_height_px()))
404 .radius(1.0)
405}
406
407fn line_height_px() -> f32 {
408 tokens::TEXT_SM.line_height
409}
410
411fn single_line_geometry(value: &str) -> TextGeometry<'_> {
412 TextGeometry::new(
413 value,
414 tokens::TEXT_SM.size,
415 FontWeight::Regular,
416 false,
417 TextWrap::NoWrap,
418 None,
419 )
420}
421
422pub fn apply_event(
454 value: &mut String,
455 selection: &mut Selection,
456 event: &UiEvent,
457 key: &str,
458) -> bool {
459 apply_event_with(value, selection, event, key, &TextInputOpts::default())
460}
461
462pub fn apply_event_with(
466 value: &mut String,
467 selection: &mut Selection,
468 event: &UiEvent,
469 key: &str,
470 opts: &TextInputOpts<'_>,
471) -> bool {
472 if matches!(
482 event.kind,
483 UiEventKind::PointerDown
484 | UiEventKind::Drag
485 | UiEventKind::MiddleClick
486 | UiEventKind::LongPress
487 ) && !event.is_route(key)
488 {
489 return false;
490 }
491 let mut local = selection.within(key).unwrap_or_default();
492 let changed = fold_event_local(value, &mut local, event, opts);
493 if changed {
494 selection.range = Some(SelectionRange {
495 anchor: SelectionPoint::new(key, local.anchor),
496 head: SelectionPoint::new(key, local.head),
497 });
498 }
499 changed
500}
501
502fn fold_event_local(
506 value: &mut String,
507 selection: &mut TextSelection,
508 event: &UiEvent,
509 opts: &TextInputOpts<'_>,
510) -> bool {
511 selection.anchor = clamp_to_char_boundary(value, selection.anchor.min(value.len()));
512 selection.head = clamp_to_char_boundary(value, selection.head.min(value.len()));
513 match event.kind {
514 UiEventKind::TextInput => {
515 let Some(insert) = event.text.as_deref() else {
516 return false;
517 };
518 if (event.modifiers.ctrl && !event.modifiers.alt) || event.modifiers.logo {
534 return false;
535 }
536 let filtered: String = insert.chars().filter(|c| !c.is_control()).collect();
537 if filtered.is_empty() {
538 return false;
539 }
540 let to_insert = clip_to_max_length(value, *selection, &filtered, opts.max_length);
541 if to_insert.is_empty() {
542 return false;
543 }
544 replace_selection(value, selection, &to_insert);
545 true
546 }
547 UiEventKind::MiddleClick => {
548 let Some(byte) = caret_byte_at(value, event, opts) else {
549 return false;
550 };
551 *selection = TextSelection::caret(byte);
552 if let Some(insert) = event.text.as_deref() {
553 replace_selection_with(value, selection, insert, opts);
554 }
555 true
556 }
557 UiEventKind::KeyDown => {
558 let Some(kp) = event.key_press.as_ref() else {
559 return false;
560 };
561 let mods = kp.modifiers;
562 if mods.ctrl
566 && !mods.alt
567 && !mods.logo
568 && let LogicalKey::Character(c) = &kp.logical
569 && c.eq_ignore_ascii_case("a")
570 {
571 let len = value.len();
572 if selection.anchor == 0 && selection.head == len {
573 return false;
574 }
575 *selection = TextSelection {
576 anchor: 0,
577 head: len,
578 };
579 return true;
580 }
581 if mods.ctrl
586 && !mods.alt
587 && !mods.logo
588 && !mods.shift
589 && let LogicalKey::Character(c) = &kp.logical
590 && c.eq_ignore_ascii_case("w")
591 {
592 return delete_word_backward(value, selection);
593 }
594 match kp.logical.named() {
595 Some(NamedKey::Escape) => {
596 if selection.is_collapsed() {
597 return false;
598 }
599 selection.anchor = selection.head;
600 true
601 }
602 Some(NamedKey::Backspace) => {
603 if !selection.is_collapsed() {
604 replace_selection(value, selection, "");
605 return true;
606 }
607 if selection.head == 0 {
608 return false;
609 }
610 if mods.ctrl && !mods.alt && !mods.logo {
611 return delete_word_backward(value, selection);
612 }
613 let prev = prev_char_boundary(value, selection.head);
614 value.replace_range(prev..selection.head, "");
615 selection.head = prev;
616 selection.anchor = prev;
617 true
618 }
619 Some(NamedKey::Delete) => {
620 if !selection.is_collapsed() {
621 replace_selection(value, selection, "");
622 return true;
623 }
624 if selection.head >= value.len() {
625 return false;
626 }
627 if mods.ctrl && !mods.alt && !mods.logo {
628 return delete_word_forward(value, selection);
629 }
630 let next = next_char_boundary(value, selection.head);
631 value.replace_range(selection.head..next, "");
632 true
633 }
634 Some(NamedKey::ArrowLeft) => {
635 let target = if selection.is_collapsed() || mods.shift {
636 if selection.head == 0 {
637 return false;
638 }
639 if mods.ctrl && !mods.alt && !mods.logo {
640 crate::selection::prev_word_boundary(value, selection.head)
641 } else {
642 prev_char_boundary(value, selection.head)
643 }
644 } else if mods.ctrl && !mods.alt && !mods.logo {
645 crate::selection::prev_word_boundary(value, selection.head)
648 } else {
649 selection.ordered().0
651 };
652 selection.head = target;
653 if !mods.shift {
654 selection.anchor = target;
655 }
656 true
657 }
658 Some(NamedKey::ArrowRight) => {
659 let target = if selection.is_collapsed() || mods.shift {
660 if selection.head >= value.len() {
661 return false;
662 }
663 if mods.ctrl && !mods.alt && !mods.logo {
664 crate::selection::next_word_boundary(value, selection.head)
665 } else {
666 next_char_boundary(value, selection.head)
667 }
668 } else if mods.ctrl && !mods.alt && !mods.logo {
669 crate::selection::next_word_boundary(value, selection.head)
670 } else {
671 selection.ordered().1
673 };
674 selection.head = target;
675 if !mods.shift {
676 selection.anchor = target;
677 }
678 true
679 }
680 Some(NamedKey::Home) => {
681 if selection.head == 0 && (mods.shift || selection.anchor == 0) {
682 return false;
683 }
684 selection.head = 0;
685 if !mods.shift {
686 selection.anchor = 0;
687 }
688 true
689 }
690 Some(NamedKey::End) => {
691 let end = value.len();
692 if selection.head == end && (mods.shift || selection.anchor == end) {
693 return false;
694 }
695 selection.head = end;
696 if !mods.shift {
697 selection.anchor = end;
698 }
699 true
700 }
701 _ => false,
702 }
703 }
704 UiEventKind::PointerDown => {
705 let (Some((px, _py)), Some(target)) = (event.pointer, event.target.as_ref()) else {
706 return false;
707 };
708 let viewport_w = (target.rect.w - 2.0 * tokens::SPACE_3).max(0.0);
714 let x_offset = current_x_offset(value, selection.head, viewport_w, opts.mask);
715 let local_x = px - target.rect.x - tokens::SPACE_3 + x_offset;
716 let pos = caret_from_x(value, local_x, opts.mask);
717 if !event.modifiers.shift {
724 match event.click_count {
725 2 => {
726 let (lo, hi) = crate::selection::word_range_at(value, pos);
727 selection.anchor = lo;
728 selection.head = hi;
729 return true;
730 }
731 n if n >= 3 => {
732 selection.anchor = 0;
733 selection.head = value.len();
734 return true;
735 }
736 _ => {}
737 }
738 }
739 selection.head = pos;
740 if !event.modifiers.shift {
741 selection.anchor = pos;
742 }
743 true
744 }
745 UiEventKind::LongPress => {
746 let (Some((px, _py)), Some(target)) = (event.pointer, event.target.as_ref()) else {
747 return false;
748 };
749 let viewport_w = (target.rect.w - 2.0 * tokens::SPACE_3).max(0.0);
750 let x_offset = current_x_offset(value, selection.head, viewport_w, opts.mask);
751 let local_x = px - target.rect.x - tokens::SPACE_3 + x_offset;
752 let pos = caret_from_x(value, local_x, opts.mask);
753 let (lo, hi) = crate::selection::word_range_at(value, pos);
754 selection.anchor = lo;
755 selection.head = hi;
756 true
757 }
758 UiEventKind::Drag => {
759 let (Some((px, _py)), Some(target)) = (event.pointer, event.target.as_ref()) else {
760 return false;
761 };
762 let viewport_w = (target.rect.w - 2.0 * tokens::SPACE_3).max(0.0);
767 let x_offset = current_x_offset(value, selection.head, viewport_w, opts.mask);
768 let local_x = px - target.rect.x - tokens::SPACE_3 + x_offset;
769 let pos = caret_from_x(value, local_x, opts.mask);
770 if !event.modifiers.shift {
771 match event.click_count {
772 2 => {
773 extend_word_selection(value, selection, pos);
774 return true;
775 }
776 n if n >= 3 => {
777 selection.anchor = 0;
778 selection.head = value.len();
779 return true;
780 }
781 _ => {}
782 }
783 }
784 selection.head = pos;
785 true
786 }
787 UiEventKind::Click => false,
788 _ => false,
789 }
790}
791
792fn extend_word_selection(value: &str, selection: &mut TextSelection, pos: usize) {
793 let (selected_lo, selected_hi) = selection.ordered();
794 let (word_lo, word_hi) = crate::selection::word_range_at(value, pos);
795 if pos < selected_lo {
796 selection.anchor = selected_hi;
797 selection.head = word_lo;
798 } else {
799 selection.anchor = selected_lo;
800 selection.head = word_hi;
801 }
802}
803
804pub fn selected_text(value: &str, selection: TextSelection) -> &str {
807 let head = clamp_to_char_boundary(value, selection.head.min(value.len()));
808 let anchor = clamp_to_char_boundary(value, selection.anchor.min(value.len()));
809 &value[anchor.min(head)..anchor.max(head)]
810}
811
812pub(crate) fn delete_word_backward(value: &mut String, selection: &mut TextSelection) -> bool {
817 if !selection.is_collapsed() {
818 replace_selection(value, selection, "");
819 return true;
820 }
821 if selection.head == 0 {
822 return false;
823 }
824 let target = crate::selection::prev_word_boundary(value, selection.head);
825 if target == selection.head {
826 return false;
827 }
828 value.replace_range(target..selection.head, "");
829 selection.head = target;
830 selection.anchor = target;
831 true
832}
833
834pub(crate) fn delete_word_forward(value: &mut String, selection: &mut TextSelection) -> bool {
839 if !selection.is_collapsed() {
840 replace_selection(value, selection, "");
841 return true;
842 }
843 if selection.head >= value.len() {
844 return false;
845 }
846 let target = crate::selection::next_word_boundary(value, selection.head);
847 if target == selection.head {
848 return false;
849 }
850 value.replace_range(selection.head..target, "");
851 true
852}
853
854pub fn replace_selection(value: &mut String, selection: &mut TextSelection, replacement: &str) {
858 selection.anchor = clamp_to_char_boundary(value, selection.anchor.min(value.len()));
859 selection.head = clamp_to_char_boundary(value, selection.head.min(value.len()));
860 let (lo, hi) = selection.ordered();
861 value.replace_range(lo..hi, replacement);
862 let new_caret = lo + replacement.len();
863 selection.anchor = new_caret;
864 selection.head = new_caret;
865}
866
867pub fn replace_selection_with(
874 value: &mut String,
875 selection: &mut TextSelection,
876 replacement: &str,
877 opts: &TextInputOpts<'_>,
878) -> usize {
879 let clipped = clip_to_max_length(value, *selection, replacement, opts.max_length);
880 let len = clipped.len();
881 replace_selection(value, selection, &clipped);
882 len
883}
884
885pub fn select_all(value: &str) -> TextSelection {
887 TextSelection {
888 anchor: 0,
889 head: value.len(),
890 }
891}
892
893#[derive(Clone, Copy, Debug, PartialEq, Eq)]
904pub enum ClipboardKind {
905 Copy,
907 Cut,
909 Paste,
911}
912
913pub fn clipboard_request(event: &UiEvent) -> Option<ClipboardKind> {
959 clipboard_request_for(event, &TextInputOpts::default())
960}
961
962pub fn clipboard_request_for(event: &UiEvent, opts: &TextInputOpts<'_>) -> Option<ClipboardKind> {
966 if event.kind != UiEventKind::KeyDown {
967 return None;
968 }
969 let kp = event.key_press.as_ref()?;
970 let mods = kp.modifiers;
971 if mods.alt || mods.shift {
974 return None;
975 }
976 let kind = match &kp.logical {
977 LogicalKey::Character(c) if mods.ctrl || mods.logo => {
978 match c.to_ascii_lowercase().as_str() {
979 "c" => ClipboardKind::Copy,
980 "x" => ClipboardKind::Cut,
981 "v" => ClipboardKind::Paste,
982 _ => return None,
983 }
984 }
985 LogicalKey::Named(named) if !mods.ctrl && !mods.logo => match named {
988 NamedKey::Copy => ClipboardKind::Copy,
989 NamedKey::Cut => ClipboardKind::Cut,
990 NamedKey::Paste => ClipboardKind::Paste,
991 _ => return None,
992 },
993 _ => return None,
994 };
995 if opts.is_masked() && matches!(kind, ClipboardKind::Copy | ClipboardKind::Cut) {
996 return None;
997 }
998 Some(kind)
999}
1000
1001#[track_caller]
1011pub fn caret_byte_at(value: &str, event: &UiEvent, opts: &TextInputOpts<'_>) -> Option<usize> {
1012 let (px, _py) = event.pointer?;
1013 let target = event.target.as_ref()?;
1014 let local_x = px - target.rect.x - tokens::SPACE_3;
1015 Some(caret_from_x(value, local_x, opts.mask))
1016}
1017
1018fn current_x_offset(value: &str, head: usize, viewport_w: f32, mask: MaskMode) -> f32 {
1030 if viewport_w <= 0.0 {
1031 return 0.0;
1032 }
1033 let head = clamp_to_char_boundary(value, head.min(value.len()));
1034 let display = display_str(value, mask);
1035 let geometry = single_line_geometry(&display);
1036 let head_display = original_to_display_byte(value, head, mask);
1037 let head_px = geometry.prefix_width(head_display);
1038 (head_px - viewport_w).max(0.0)
1039}
1040
1041fn caret_from_x(value: &str, local_x: f32, mask: MaskMode) -> usize {
1042 if value.is_empty() || local_x <= 0.0 {
1043 return 0;
1044 }
1045 let probe = display_str(value, mask);
1046 let local_y = line_height_px() * 0.5;
1047 let geometry = single_line_geometry(&probe);
1048 let display_byte = match geometry.hit_byte(local_x, local_y) {
1049 Some(byte) => byte.min(probe.len()),
1050 None => probe.len(),
1051 };
1052 display_to_original_byte(value, display_byte, mask)
1053}
1054
1055fn display_str(value: &str, mask: MaskMode) -> Cow<'_, str> {
1060 match mask {
1061 MaskMode::None => Cow::Borrowed(value),
1062 MaskMode::Password => {
1063 let n = value.chars().count();
1064 let mut s = String::with_capacity(n * MASK_CHAR.len_utf8());
1065 for _ in 0..n {
1066 s.push(MASK_CHAR);
1067 }
1068 Cow::Owned(s)
1069 }
1070 }
1071}
1072
1073fn original_to_display_byte(value: &str, byte_index: usize, mask: MaskMode) -> usize {
1074 match mask {
1075 MaskMode::None => byte_index.min(value.len()),
1076 MaskMode::Password => {
1077 let clamped = clamp_to_char_boundary(value, byte_index.min(value.len()));
1078 value[..clamped].chars().count() * MASK_CHAR.len_utf8()
1079 }
1080 }
1081}
1082
1083fn display_to_original_byte(value: &str, display_byte: usize, mask: MaskMode) -> usize {
1085 match mask {
1086 MaskMode::None => clamp_to_char_boundary(value, display_byte.min(value.len())),
1087 MaskMode::Password => {
1088 let scalar_idx = display_byte / MASK_CHAR.len_utf8();
1089 value
1090 .char_indices()
1091 .nth(scalar_idx)
1092 .map(|(i, _)| i)
1093 .unwrap_or(value.len())
1094 }
1095 }
1096}
1097
1098fn clip_to_max_length<'a>(
1106 value: &str,
1107 selection: TextSelection,
1108 replacement: &'a str,
1109 max_length: Option<usize>,
1110) -> Cow<'a, str> {
1111 let Some(max) = max_length else {
1112 return Cow::Borrowed(replacement);
1113 };
1114 let lo = clamp_to_char_boundary(value, selection.anchor.min(selection.head).min(value.len()));
1115 let hi = clamp_to_char_boundary(value, selection.anchor.max(selection.head).min(value.len()));
1116 let post_other = value[..lo].chars().count() + value[hi..].chars().count();
1117 let allowed = max.saturating_sub(post_other);
1118 if replacement.chars().count() <= allowed {
1119 Cow::Borrowed(replacement)
1120 } else {
1121 Cow::Owned(replacement.chars().take(allowed).collect())
1122 }
1123}
1124
1125fn clamp_to_char_boundary(s: &str, idx: usize) -> usize {
1126 let mut idx = idx.min(s.len());
1127 while idx > 0 && !s.is_char_boundary(idx) {
1128 idx -= 1;
1129 }
1130 idx
1131}
1132
1133fn prev_char_boundary(s: &str, from: usize) -> usize {
1134 let mut i = from.saturating_sub(1);
1135 while i > 0 && !s.is_char_boundary(i) {
1136 i -= 1;
1137 }
1138 i
1139}
1140
1141fn next_char_boundary(s: &str, from: usize) -> usize {
1142 let mut i = (from + 1).min(s.len());
1143 while i < s.len() && !s.is_char_boundary(i) {
1144 i += 1;
1145 }
1146 i
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151 use super::*;
1152 use crate::event::{
1153 KeyModifiers, KeyPress, PhysicalKey, Pointer, PointerButton, PointerKind, UiTarget,
1154 };
1155 use crate::layout::layout;
1156 use crate::palette::Palette;
1157 use crate::runtime::RunnerCore;
1158 use crate::state::UiState;
1159 use crate::text::metrics;
1160
1161 const TEST_KEY: &str = "ti";
1166
1167 #[track_caller]
1172 fn text_input(value: &str, sel: TextSelection) -> El {
1173 super::text_input(TEST_KEY, value, &as_selection(sel))
1174 }
1175
1176 #[track_caller]
1177 fn text_input_with(value: &str, sel: TextSelection, opts: TextInputOpts<'_>) -> El {
1178 super::text_input_with(TEST_KEY, value, &as_selection(sel), opts)
1179 }
1180
1181 fn apply_event(value: &mut String, sel: &mut TextSelection, event: &UiEvent) -> bool {
1182 let mut g = as_selection(*sel);
1183 let changed = super::apply_event(value, &mut g, event, TEST_KEY);
1184 sync_back(sel, &g);
1185 changed
1186 }
1187
1188 fn apply_event_with(
1189 value: &mut String,
1190 sel: &mut TextSelection,
1191 event: &UiEvent,
1192 opts: &TextInputOpts<'_>,
1193 ) -> bool {
1194 let mut g = as_selection(*sel);
1195 let changed = super::apply_event_with(value, &mut g, event, TEST_KEY, opts);
1196 sync_back(sel, &g);
1197 changed
1198 }
1199
1200 fn as_selection(sel: TextSelection) -> Selection {
1201 Selection {
1202 range: Some(SelectionRange {
1203 anchor: SelectionPoint::new(TEST_KEY, sel.anchor),
1204 head: SelectionPoint::new(TEST_KEY, sel.head),
1205 }),
1206 }
1207 }
1208
1209 fn sync_back(local: &mut TextSelection, global: &Selection) {
1210 match global.within(TEST_KEY) {
1211 Some(view) => *local = view,
1212 None => *local = TextSelection::default(),
1213 }
1214 }
1215
1216 fn ev_text(s: &str) -> UiEvent {
1217 ev_text_with_mods(s, KeyModifiers::default())
1218 }
1219
1220 fn ev_text_with_mods(s: &str, modifiers: KeyModifiers) -> UiEvent {
1221 UiEvent {
1222 path: None,
1223 key: None,
1224 target: None,
1225 pointer: None,
1226 key_press: None,
1227 text: Some(s.into()),
1228 selection: None,
1229 modifiers,
1230 click_count: 0,
1231 pointer_kind: None,
1232 wheel_delta: None,
1233 kind: UiEventKind::TextInput,
1234 }
1235 }
1236
1237 fn ev_key(key: LogicalKey) -> UiEvent {
1238 ev_key_with_mods(key, KeyModifiers::default())
1239 }
1240
1241 fn ev_key_with_mods(key: LogicalKey, modifiers: KeyModifiers) -> UiEvent {
1242 UiEvent {
1243 path: None,
1244 key: None,
1245 target: None,
1246 pointer: None,
1247 key_press: Some(KeyPress {
1248 logical: key,
1249 physical: PhysicalKey::Unidentified,
1250 modifiers,
1251 repeat: false,
1252 }),
1253 text: None,
1254 selection: None,
1255 modifiers,
1256 click_count: 0,
1257 pointer_kind: None,
1258 wheel_delta: None,
1259 kind: UiEventKind::KeyDown,
1260 }
1261 }
1262
1263 fn ev_pointer_down(target: UiTarget, pointer: (f32, f32), modifiers: KeyModifiers) -> UiEvent {
1264 ev_pointer_down_with_count(target, pointer, modifiers, 1)
1265 }
1266
1267 fn ev_pointer_down_with_count(
1268 target: UiTarget,
1269 pointer: (f32, f32),
1270 modifiers: KeyModifiers,
1271 click_count: u8,
1272 ) -> UiEvent {
1273 UiEvent {
1274 path: None,
1275 key: Some(target.key.clone()),
1276 target: Some(target),
1277 pointer: Some(pointer),
1278 key_press: None,
1279 text: None,
1280 selection: None,
1281 modifiers,
1282 click_count,
1283 pointer_kind: None,
1284 wheel_delta: None,
1285 kind: UiEventKind::PointerDown,
1286 }
1287 }
1288
1289 fn ev_long_press(target: UiTarget, pointer: (f32, f32)) -> UiEvent {
1290 UiEvent {
1291 path: None,
1292 key: Some(target.key.clone()),
1293 target: Some(target),
1294 pointer: Some(pointer),
1295 key_press: None,
1296 text: None,
1297 selection: None,
1298 modifiers: KeyModifiers::default(),
1299 click_count: 0,
1300 pointer_kind: Some(PointerKind::Touch),
1301 wheel_delta: None,
1302 kind: UiEventKind::LongPress,
1303 }
1304 }
1305
1306 fn ev_drag(target: UiTarget, pointer: (f32, f32)) -> UiEvent {
1307 ev_drag_with_count(target, pointer, 0)
1308 }
1309
1310 fn ev_drag_with_count(target: UiTarget, pointer: (f32, f32), click_count: u8) -> UiEvent {
1311 UiEvent {
1312 path: None,
1313 key: Some(target.key.clone()),
1314 target: Some(target),
1315 pointer: Some(pointer),
1316 key_press: None,
1317 text: None,
1318 selection: None,
1319 modifiers: KeyModifiers::default(),
1320 click_count,
1321 pointer_kind: None,
1322 wheel_delta: None,
1323 kind: UiEventKind::Drag,
1324 }
1325 }
1326
1327 fn ev_middle_click(target: UiTarget, pointer: (f32, f32), text: Option<&str>) -> UiEvent {
1328 UiEvent {
1329 path: None,
1330 key: Some(target.key.clone()),
1331 target: Some(target),
1332 pointer: Some(pointer),
1333 key_press: None,
1334 text: text.map(str::to_string),
1335 selection: None,
1336 modifiers: KeyModifiers::default(),
1337 click_count: 1,
1338 pointer_kind: None,
1339 wheel_delta: None,
1340 kind: UiEventKind::MiddleClick,
1341 }
1342 }
1343
1344 fn ti_target() -> UiTarget {
1345 UiTarget {
1346 key: "ti".into(),
1347 node_id: "root.text_input[ti]".into(),
1348 rect: Rect::new(20.0, 20.0, 400.0, 36.0),
1349 tooltip: None,
1350 scroll_offset_y: 0.0,
1351 }
1352 }
1353
1354 #[test]
1355 fn apply_event_ignores_pointer_routed_to_another_widget() {
1356 let foreign = || UiTarget {
1361 key: "other".into(),
1362 node_id: "root.slider[other]".into(),
1363 rect: Rect::new(0.0, 0.0, 200.0, 20.0),
1364 tooltip: None,
1365 scroll_offset_y: 0.0,
1366 };
1367 let mut value = String::from("hello");
1368 let mut sel = TextSelection::range(1, 3);
1369
1370 let drag = ev_drag(foreign(), (40.0, 10.0));
1371 assert!(!apply_event(&mut value, &mut sel, &drag));
1372 let down = ev_pointer_down(foreign(), (40.0, 10.0), KeyModifiers::default());
1373 assert!(!apply_event(&mut value, &mut sel, &down));
1374
1375 assert_eq!(value, "hello");
1377 assert_eq!(sel, TextSelection::range(1, 3));
1378 }
1379
1380 fn content_children(el: &El) -> &[El] {
1388 assert_eq!(
1389 el.children.len(),
1390 1,
1391 "text_input wraps its content in a single inner group"
1392 );
1393 &el.children[0].children
1394 }
1395
1396 #[test]
1397 fn text_input_collapsed_renders_value_as_single_text_leaf_plus_caret() {
1398 let el = text_input("hello", TextSelection::caret(2));
1399 assert!(matches!(el.kind, Kind::Custom("text_input")));
1400 assert!(el.focusable);
1401 assert!(el.capture_keys);
1402 let cs = content_children(&el);
1406 assert_eq!(cs.len(), 2);
1407 assert!(matches!(cs[0].kind, Kind::Text));
1408 assert_eq!(cs[0].text.as_deref(), Some("hello"));
1409 assert!(matches!(cs[1].kind, Kind::Custom("text_input_caret")));
1410 assert!(cs[1].alpha_follows_focused_ancestor);
1411 }
1412
1413 #[test]
1414 fn text_input_declares_text_cursor() {
1415 let el = text_input("hello", TextSelection::caret(0));
1416 assert_eq!(el.cursor, Some(Cursor::Text));
1417 }
1418
1419 #[test]
1420 fn text_input_with_selection_inserts_selection_band_first() {
1421 let el = text_input("hello", TextSelection::range(2, 4));
1423 let cs = content_children(&el);
1424 assert_eq!(cs.len(), 3);
1426 assert!(matches!(cs[0].kind, Kind::Custom("text_input_selection")));
1427 assert_eq!(cs[1].text.as_deref(), Some("hello"));
1428 assert!(matches!(cs[2].kind, Kind::Custom("text_input_caret")));
1429 }
1430
1431 #[test]
1432 fn text_input_caret_translate_advances_with_head() {
1433 use crate::text::metrics::line_width;
1437 let value = "hello";
1438 let head = 3;
1439 let el = text_input(value, TextSelection::caret(head));
1440 let caret = content_children(&el)
1441 .iter()
1442 .find(|c| matches!(c.kind, Kind::Custom("text_input_caret")))
1443 .expect("caret child");
1444 let expected = line_width(
1445 &value[..head],
1446 tokens::TEXT_SM.size,
1447 FontWeight::Regular,
1448 false,
1449 );
1450 assert!(
1451 (caret.translate.0 - expected).abs() < 0.01,
1452 "caret translate.x = {}, expected {}",
1453 caret.translate.0,
1454 expected
1455 );
1456 }
1457
1458 #[test]
1459 fn text_input_clamps_off_utf8_boundary() {
1460 let el = text_input("é", TextSelection::caret(1));
1464 let cs = content_children(&el);
1465 assert_eq!(cs[0].text.as_deref(), Some("é"));
1466 let caret = cs
1467 .iter()
1468 .find(|c| matches!(c.kind, Kind::Custom("text_input_caret")))
1469 .expect("caret child");
1470 assert!(caret.translate.0.abs() < 0.01);
1472 }
1473
1474 #[test]
1475 fn selection_band_fill_dims_when_input_unfocused() {
1476 use crate::draw_ops::draw_ops;
1480 use crate::ir::DrawOp;
1481 use crate::shader::UniformValue;
1482 use crate::state::AnimationMode;
1483 use web_time::Instant;
1484
1485 let mut tree = crate::column([text_input("hello", TextSelection::range(0, 5)).key("ti")])
1486 .padding(20.0);
1487 let mut state = UiState::new();
1488 state.set_animation_mode(AnimationMode::Settled);
1489 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
1490 state.sync_focus_order(&tree);
1491
1492 state.apply_to_state();
1496 state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1497 let unfocused = band_fill(&tree, &state).expect("band quad emitted");
1498 let [ur, ug, ub, _] = unfocused.to_srgb_u8a();
1499 let [tr, tg, tb, _] = tokens::SELECTION_BG_UNFOCUSED.to_srgb_u8a();
1500 assert_eq!(
1501 (ur, ug, ub),
1502 (tr, tg, tb),
1503 "unfocused → band rgb is the muted token"
1504 );
1505
1506 let target = state
1509 .focus
1510 .order
1511 .iter()
1512 .find(|t| t.key == "ti")
1513 .expect("ti in focus order")
1514 .clone();
1515 state.set_focus(Some(target));
1516 state.apply_to_state();
1517 state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1518 let focused = band_fill(&tree, &state).expect("band quad emitted");
1519 let [fr, fg, fb, _] = focused.to_srgb_u8a();
1520 let [tr, tg, tb, _] = tokens::SELECTION_BG.to_srgb_u8a();
1521 assert_eq!(
1522 (fr, fg, fb),
1523 (tr, tg, tb),
1524 "focused → band rgb is the saturated token"
1525 );
1526
1527 fn band_fill(tree: &El, state: &UiState) -> Option<crate::tree::Color> {
1528 let ops = draw_ops(tree, state);
1529 for op in ops {
1530 if let DrawOp::Quad { id, uniforms, .. } = op
1531 && id.contains("text_input_selection")
1532 && let Some(UniformValue::Color(c)) = uniforms.get("fill")
1533 {
1534 return Some(*c);
1535 }
1536 }
1537 None
1538 }
1539 }
1540
1541 #[test]
1542 fn caret_alpha_follows_focus_envelope() {
1543 use crate::draw_ops::draw_ops;
1548 use crate::ir::DrawOp;
1549 use crate::shader::UniformValue;
1550 use crate::state::AnimationMode;
1551 use web_time::Instant;
1552
1553 let mut tree =
1554 crate::column([text_input("hi", TextSelection::caret(0)).key("ti")]).padding(20.0);
1555 let mut state = UiState::new();
1556 state.set_animation_mode(AnimationMode::Settled);
1557 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
1558 state.sync_focus_order(&tree);
1559
1560 state.apply_to_state();
1562 state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1563 let caret_alpha = caret_fill_alpha(&tree, &state);
1564 assert_eq!(caret_alpha, Some(0), "unfocused → caret invisible");
1565
1566 let target = state
1568 .focus
1569 .order
1570 .iter()
1571 .find(|t| t.key == "ti")
1572 .expect("ti in focus order")
1573 .clone();
1574 state.set_focus(Some(target));
1575 state.apply_to_state();
1576 state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1577 let caret_alpha = caret_fill_alpha(&tree, &state);
1578 assert_eq!(
1579 caret_alpha,
1580 Some(255),
1581 "focused → caret fully visible (alpha=255)"
1582 );
1583
1584 fn caret_fill_alpha(tree: &El, state: &UiState) -> Option<u8> {
1585 let ops = draw_ops(tree, state);
1586 for op in ops {
1587 if let DrawOp::Quad { id, uniforms, .. } = op
1588 && id.contains("text_input_caret")
1589 && let Some(UniformValue::Color(c)) = uniforms.get("fill")
1590 {
1591 return Some(c.to_srgb_u8a()[3]);
1592 }
1593 }
1594 None
1595 }
1596 }
1597
1598 #[test]
1599 fn caret_blink_alpha_holds_solid_through_grace_then_cycles() {
1600 use crate::state::caret_blink_alpha_for;
1603 use std::time::Duration;
1604 assert_eq!(caret_blink_alpha_for(Duration::from_millis(0)), 1.0);
1606 assert_eq!(caret_blink_alpha_for(Duration::from_millis(499)), 1.0);
1607 assert_eq!(caret_blink_alpha_for(Duration::from_millis(500)), 1.0);
1609 assert_eq!(caret_blink_alpha_for(Duration::from_millis(1029)), 1.0);
1610 assert_eq!(caret_blink_alpha_for(Duration::from_millis(1030)), 0.0);
1612 assert_eq!(caret_blink_alpha_for(Duration::from_millis(1559)), 0.0);
1613 assert_eq!(caret_blink_alpha_for(Duration::from_millis(1560)), 1.0);
1615 }
1616
1617 #[test]
1618 fn caret_paint_alpha_blinks_after_focus_in_live_mode() {
1619 use crate::draw_ops::draw_ops;
1623 use crate::ir::DrawOp;
1624 use crate::shader::UniformValue;
1625 use crate::state::AnimationMode;
1626 use std::time::Duration;
1627
1628 let mut tree =
1629 crate::column([text_input("hi", TextSelection::caret(0)).key("ti")]).padding(20.0);
1630 let mut state = UiState::new();
1631 state.set_animation_mode(AnimationMode::Live);
1632 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
1633 state.sync_focus_order(&tree);
1634
1635 let target = state
1637 .focus
1638 .order
1639 .iter()
1640 .find(|t| t.key == "ti")
1641 .unwrap()
1642 .clone();
1643 state.set_focus(Some(target));
1644 let activity_at = state.caret.activity_at.expect("set_focus bumps activity");
1645 let input_id = tree.children[0].computed_id.clone();
1646
1647 let pin_focus = |state: &mut UiState| {
1651 state.animation.envelopes.insert(
1652 (input_id.clone(), crate::state::EnvelopeKind::FocusRing),
1653 1.0,
1654 );
1655 };
1656
1657 state.tick_visual_animations(&mut tree, activity_at, &Palette::default());
1659 pin_focus(&mut state);
1660 assert_eq!(caret_alpha(&tree, &state), Some(255));
1661
1662 state.tick_visual_animations(
1664 &mut tree,
1665 activity_at + Duration::from_millis(1100),
1666 &Palette::default(),
1667 );
1668 pin_focus(&mut state);
1669 assert_eq!(caret_alpha(&tree, &state), Some(0));
1670
1671 state.tick_visual_animations(
1673 &mut tree,
1674 activity_at + Duration::from_millis(1600),
1675 &Palette::default(),
1676 );
1677 pin_focus(&mut state);
1678 assert_eq!(caret_alpha(&tree, &state), Some(255));
1679
1680 fn caret_alpha(tree: &El, state: &UiState) -> Option<u8> {
1681 for op in draw_ops(tree, state) {
1682 if let DrawOp::Quad { id, uniforms, .. } = op
1683 && id.contains("text_input_caret")
1684 && let Some(UniformValue::Color(c)) = uniforms.get("fill")
1685 {
1686 return Some(c.to_srgb_u8a()[3]);
1687 }
1688 }
1689 None
1690 }
1691 }
1692
1693 #[test]
1694 fn caret_blink_resumes_solid_after_selection_change() {
1695 use crate::state::AnimationMode;
1698 use std::time::Duration;
1699 use web_time::Instant;
1700
1701 let mut tree =
1702 crate::column([text_input("hi", TextSelection::caret(0)).key("ti")]).padding(20.0);
1703 let mut state = UiState::new();
1704 state.set_animation_mode(AnimationMode::Live);
1705 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
1706 state.sync_focus_order(&tree);
1707
1708 let t0 = Instant::now();
1710 state.bump_caret_activity(t0);
1711 state.tick_visual_animations(
1712 &mut tree,
1713 t0 + Duration::from_millis(1100),
1714 &Palette::default(),
1715 );
1716 assert_eq!(state.caret.blink_alpha, 0.0, "deep in off phase");
1717
1718 state.bump_caret_activity(t0 + Duration::from_millis(1100));
1720 assert_eq!(state.caret.blink_alpha, 1.0, "fresh activity → solid");
1721 }
1722
1723 #[test]
1724 fn caret_tick_requests_redraw_while_capture_keys_node_focused() {
1725 use crate::state::AnimationMode;
1729 use web_time::Instant;
1730
1731 let mut tree =
1732 crate::column([text_input("hi", TextSelection::caret(0)).key("ti")]).padding(20.0);
1733 let mut state = UiState::new();
1734 state.set_animation_mode(AnimationMode::Live);
1735 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
1736 state.sync_focus_order(&tree);
1737
1738 let no_focus = state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1740 assert!(!no_focus, "without focus, blink doesn't request redraws");
1741
1742 let target = state
1745 .focus
1746 .order
1747 .iter()
1748 .find(|t| t.key == "ti")
1749 .unwrap()
1750 .clone();
1751 state.set_focus(Some(target));
1752 let focused = state.tick_visual_animations(&mut tree, Instant::now(), &Palette::default());
1753 assert!(focused, "focused capture_keys node → tick demands redraws");
1754 }
1755
1756 #[test]
1757 fn apply_text_input_inserts_at_caret_when_collapsed() {
1758 let mut value = String::from("ho");
1759 let mut sel = TextSelection::caret(1);
1760 assert!(apply_event(&mut value, &mut sel, &ev_text("i, t")));
1761 assert_eq!(value, "hi, to");
1762 assert_eq!(sel, TextSelection::caret(5));
1763 }
1764
1765 #[test]
1766 fn apply_text_input_replaces_selection() {
1767 let mut value = String::from("hello world");
1768 let mut sel = TextSelection::range(6, 11); assert!(apply_event(&mut value, &mut sel, &ev_text("kit")));
1770 assert_eq!(value, "hello kit");
1771 assert_eq!(sel, TextSelection::caret(9));
1772 }
1773
1774 #[test]
1775 fn apply_backspace_removes_selection_when_non_empty() {
1776 let mut value = String::from("hello world");
1777 let mut sel = TextSelection::range(6, 11);
1778 assert!(apply_event(
1779 &mut value,
1780 &mut sel,
1781 &ev_key(LogicalKey::Named(NamedKey::Backspace))
1782 ));
1783 assert_eq!(value, "hello ");
1784 assert_eq!(sel, TextSelection::caret(6));
1785 }
1786
1787 #[test]
1788 fn apply_delete_removes_selection_when_non_empty() {
1789 let mut value = String::from("hello world");
1790 let mut sel = TextSelection::range(0, 6); assert!(apply_event(
1792 &mut value,
1793 &mut sel,
1794 &ev_key(LogicalKey::Named(NamedKey::Delete))
1795 ));
1796 assert_eq!(value, "world");
1797 assert_eq!(sel, TextSelection::caret(0));
1798 }
1799
1800 #[test]
1801 fn apply_escape_collapses_selection_without_editing() {
1802 let mut value = String::from("hello");
1803 let mut sel = TextSelection::range(1, 4);
1804 assert!(apply_event(
1805 &mut value,
1806 &mut sel,
1807 &ev_key(LogicalKey::Named(NamedKey::Escape))
1808 ));
1809 assert_eq!(value, "hello");
1810 assert_eq!(sel, TextSelection::caret(4));
1811 assert!(!apply_event(
1812 &mut value,
1813 &mut sel,
1814 &ev_key(LogicalKey::Named(NamedKey::Escape))
1815 ));
1816 }
1817
1818 #[test]
1819 fn apply_backspace_collapsed_at_start_is_noop() {
1820 let mut value = String::from("hi");
1821 let mut sel = TextSelection::caret(0);
1822 assert!(!apply_event(
1823 &mut value,
1824 &mut sel,
1825 &ev_key(LogicalKey::Named(NamedKey::Backspace))
1826 ));
1827 }
1828
1829 #[test]
1830 fn apply_arrow_walks_utf8_boundaries() {
1831 let mut value = String::from("aé");
1832 let mut sel = TextSelection::caret(0);
1833 apply_event(
1834 &mut value,
1835 &mut sel,
1836 &ev_key(LogicalKey::Named(NamedKey::ArrowRight)),
1837 );
1838 assert_eq!(sel.head, 1);
1839 apply_event(
1840 &mut value,
1841 &mut sel,
1842 &ev_key(LogicalKey::Named(NamedKey::ArrowRight)),
1843 );
1844 assert_eq!(sel.head, 3);
1845 assert!(!apply_event(
1846 &mut value,
1847 &mut sel,
1848 &ev_key(LogicalKey::Named(NamedKey::ArrowRight))
1849 ));
1850 apply_event(
1851 &mut value,
1852 &mut sel,
1853 &ev_key(LogicalKey::Named(NamedKey::ArrowLeft)),
1854 );
1855 assert_eq!(sel.head, 1);
1856 }
1857
1858 #[test]
1859 fn apply_arrow_collapses_selection_without_shift() {
1860 let mut value = String::from("hello");
1861 let mut sel = TextSelection::range(1, 4); assert!(apply_event(
1865 &mut value,
1866 &mut sel,
1867 &ev_key(LogicalKey::Named(NamedKey::ArrowLeft))
1868 ));
1869 assert_eq!(sel, TextSelection::caret(1));
1870
1871 let mut sel = TextSelection::range(1, 4);
1872 assert!(apply_event(
1874 &mut value,
1875 &mut sel,
1876 &ev_key(LogicalKey::Named(NamedKey::ArrowRight))
1877 ));
1878 assert_eq!(sel, TextSelection::caret(4));
1879 }
1880
1881 #[test]
1882 fn apply_shift_arrow_extends_selection() {
1883 let mut value = String::from("hello");
1884 let mut sel = TextSelection::caret(2);
1885 let shift = KeyModifiers {
1886 shift: true,
1887 ..Default::default()
1888 };
1889 assert!(apply_event(
1890 &mut value,
1891 &mut sel,
1892 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowRight), shift)
1893 ));
1894 assert_eq!(sel, TextSelection::range(2, 3));
1895 assert!(apply_event(
1896 &mut value,
1897 &mut sel,
1898 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowRight), shift)
1899 ));
1900 assert_eq!(sel, TextSelection::range(2, 4));
1901 assert!(apply_event(
1903 &mut value,
1904 &mut sel,
1905 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowLeft), shift)
1906 ));
1907 assert_eq!(sel, TextSelection::range(2, 3));
1908 }
1909
1910 #[test]
1911 fn apply_home_end_collapse_or_extend() {
1912 let mut value = String::from("hello");
1913 let mut sel = TextSelection::caret(2);
1914 assert!(apply_event(
1915 &mut value,
1916 &mut sel,
1917 &ev_key(LogicalKey::Named(NamedKey::End))
1918 ));
1919 assert_eq!(sel, TextSelection::caret(5));
1920 assert!(apply_event(
1921 &mut value,
1922 &mut sel,
1923 &ev_key(LogicalKey::Named(NamedKey::Home))
1924 ));
1925 assert_eq!(sel, TextSelection::caret(0));
1926
1927 let shift = KeyModifiers {
1929 shift: true,
1930 ..Default::default()
1931 };
1932 let mut sel = TextSelection::caret(2);
1933 assert!(apply_event(
1934 &mut value,
1935 &mut sel,
1936 &ev_key_with_mods(LogicalKey::Named(NamedKey::End), shift)
1937 ));
1938 assert_eq!(sel, TextSelection::range(2, 5));
1939 }
1940
1941 #[test]
1942 fn apply_ctrl_a_selects_all() {
1943 let mut value = String::from("hello");
1944 let mut sel = TextSelection::caret(2);
1945 let ctrl = KeyModifiers {
1946 ctrl: true,
1947 ..Default::default()
1948 };
1949 assert!(apply_event(
1950 &mut value,
1951 &mut sel,
1952 &ev_key_with_mods(LogicalKey::Character("a".into()), ctrl)
1953 ));
1954 assert_eq!(sel, TextSelection::range(0, 5));
1955 assert!(!apply_event(
1957 &mut value,
1958 &mut sel,
1959 &ev_key_with_mods(LogicalKey::Character("a".into()), ctrl)
1960 ));
1961 }
1962
1963 #[test]
1964 fn apply_pointer_down_sets_anchor_and_head() {
1965 let mut value = String::from("hello");
1966 let mut sel = TextSelection::range(0, 5);
1967 let down = ev_pointer_down(
1969 ti_target(),
1970 (ti_target().rect.x + 1.0, ti_target().rect.y + 18.0),
1971 KeyModifiers::default(),
1972 );
1973 assert!(apply_event(&mut value, &mut sel, &down));
1974 assert_eq!(sel, TextSelection::caret(0));
1975 }
1976
1977 #[test]
1978 fn apply_double_click_selects_word_at_caret() {
1979 let mut value = String::from("hello world");
1980 let mut sel = TextSelection::caret(0);
1981 let target = ti_target();
1983 let click_x = target.rect.x
1984 + tokens::SPACE_3
1985 + crate::text::metrics::line_width(
1986 "hello w",
1987 tokens::TEXT_SM.size,
1988 FontWeight::Regular,
1989 false,
1990 );
1991 let down = ev_pointer_down_with_count(
1992 target.clone(),
1993 (click_x, target.rect.y + 18.0),
1994 KeyModifiers::default(),
1995 2,
1996 );
1997 assert!(apply_event(&mut value, &mut sel, &down));
1998 assert_eq!(sel.anchor, 6);
2000 assert_eq!(sel.head, 11);
2001 }
2002
2003 #[test]
2004 fn apply_long_press_selects_word_at_caret() {
2005 let mut value = String::from("hello world");
2006 let mut sel = TextSelection::caret(0);
2007 let target = ti_target();
2008 let event = ev_long_press(target.clone(), (target.rect.x + 4.0, target.rect.y + 18.0));
2009
2010 assert!(apply_event(&mut value, &mut sel, &event));
2011 assert_eq!(sel, TextSelection::range(0, 5));
2012 }
2013
2014 #[test]
2015 fn apply_triple_click_selects_all() {
2016 let mut value = String::from("hello world");
2017 let mut sel = TextSelection::caret(0);
2018 let target = ti_target();
2019 let down = ev_pointer_down_with_count(
2020 target.clone(),
2021 (target.rect.x + 1.0, target.rect.y + 18.0),
2022 KeyModifiers::default(),
2023 3,
2024 );
2025 assert!(apply_event(&mut value, &mut sel, &down));
2026 assert_eq!(sel.anchor, 0);
2027 assert_eq!(sel.head, value.len());
2028 }
2029
2030 #[test]
2031 fn apply_shift_double_click_falls_back_to_extend_not_word_select() {
2032 let mut value = String::from("hello world");
2035 let mut sel = TextSelection::caret(0);
2036 let target = ti_target();
2037 let click_x = target.rect.x
2038 + tokens::SPACE_3
2039 + crate::text::metrics::line_width(
2040 "hello w",
2041 tokens::TEXT_SM.size,
2042 FontWeight::Regular,
2043 false,
2044 );
2045 let shift = KeyModifiers {
2046 shift: true,
2047 ..Default::default()
2048 };
2049 let down =
2050 ev_pointer_down_with_count(target.clone(), (click_x, target.rect.y + 18.0), shift, 2);
2051 assert!(apply_event(&mut value, &mut sel, &down));
2052 assert_eq!(sel.anchor, 0);
2054 assert!(sel.head > 0 && sel.head < value.len());
2055 }
2056
2057 #[test]
2058 fn apply_shift_pointer_down_only_moves_head() {
2059 let mut value = String::from("hello");
2060 let mut sel = TextSelection::caret(2);
2061 let shift = KeyModifiers {
2062 shift: true,
2063 ..Default::default()
2064 };
2065 let down = ev_pointer_down(
2067 ti_target(),
2068 (
2069 ti_target().rect.x + ti_target().rect.w - 4.0,
2070 ti_target().rect.y + 18.0,
2071 ),
2072 shift,
2073 );
2074 assert!(apply_event(&mut value, &mut sel, &down));
2075 assert_eq!(sel.anchor, 2);
2076 assert_eq!(sel.head, value.len());
2077 }
2078
2079 #[test]
2080 fn apply_drag_extends_head_only() {
2081 let mut value = String::from("hello world");
2082 let mut sel = TextSelection::caret(0);
2083 let down = ev_pointer_down(
2085 ti_target(),
2086 (ti_target().rect.x + 1.0, ti_target().rect.y + 18.0),
2087 KeyModifiers::default(),
2088 );
2089 apply_event(&mut value, &mut sel, &down);
2090 assert_eq!(sel, TextSelection::caret(0));
2091 let drag = ev_drag(
2093 ti_target(),
2094 (
2095 ti_target().rect.x + ti_target().rect.w - 4.0,
2096 ti_target().rect.y + 18.0,
2097 ),
2098 );
2099 assert!(apply_event(&mut value, &mut sel, &drag));
2100 assert_eq!(sel.anchor, 0);
2101 assert_eq!(sel.head, value.len());
2102 }
2103
2104 #[test]
2105 fn double_click_hold_drag_inside_word_keeps_word_selected() {
2106 let mut value = String::from("hello world");
2107 let mut sel = TextSelection::caret(0);
2108 let target = ti_target();
2109 let click_x = target.rect.x
2110 + tokens::SPACE_3
2111 + crate::text::metrics::line_width(
2112 "hello w",
2113 tokens::TEXT_SM.size,
2114 FontWeight::Regular,
2115 false,
2116 );
2117 let down = ev_pointer_down_with_count(
2118 target.clone(),
2119 (click_x, target.rect.y + 18.0),
2120 KeyModifiers::default(),
2121 2,
2122 );
2123 assert!(apply_event(&mut value, &mut sel, &down));
2124 assert_eq!(sel, TextSelection::range(6, 11));
2125
2126 let drag = ev_drag_with_count(target.clone(), (click_x + 1.0, target.rect.y + 18.0), 2);
2127 assert!(apply_event(&mut value, &mut sel, &drag));
2128 assert_eq!(sel, TextSelection::range(6, 11));
2129 }
2130
2131 #[test]
2132 fn apply_click_is_noop_for_selection() {
2133 let mut value = String::from("hello");
2137 let mut sel = TextSelection::range(0, 5);
2138 let click = UiEvent {
2139 path: None,
2140 key: Some("ti".into()),
2141 target: Some(ti_target()),
2142 pointer: Some((ti_target().rect.x + 1.0, ti_target().rect.y + 18.0)),
2143 key_press: None,
2144 text: None,
2145 selection: None,
2146 modifiers: KeyModifiers::default(),
2147 click_count: 1,
2148 pointer_kind: None,
2149 wheel_delta: None,
2150 kind: UiEventKind::Click,
2151 };
2152 assert!(!apply_event(&mut value, &mut sel, &click));
2153 assert_eq!(sel, TextSelection::range(0, 5));
2154 }
2155
2156 #[test]
2157 fn apply_middle_click_inserts_event_text_at_pointer() {
2158 let mut value = String::from("world");
2159 let mut sel = TextSelection::caret(value.len());
2160 let target = ti_target();
2161 let pointer = (
2162 target.rect.x + tokens::SPACE_3,
2163 target.rect.y + target.rect.h * 0.5,
2164 );
2165 let event = ev_middle_click(target, pointer, Some("hello "));
2166 assert!(apply_event(&mut value, &mut sel, &event));
2167 assert_eq!(value, "hello world");
2168 assert_eq!(sel, TextSelection::caret("hello ".len()));
2169 }
2170
2171 #[test]
2172 fn helpers_selected_text_and_replace_selection() {
2173 let value = String::from("hello world");
2174 let sel = TextSelection::range(6, 11);
2175 assert_eq!(selected_text(&value, sel), "world");
2176
2177 let mut value = value;
2178 let mut sel = sel;
2179 replace_selection(&mut value, &mut sel, "kit");
2180 assert_eq!(value, "hello kit");
2181 assert_eq!(sel, TextSelection::caret(9));
2182
2183 assert_eq!(select_all(&value), TextSelection::range(0, value.len()));
2184 }
2185
2186 #[test]
2187 fn apply_text_input_filters_control_chars() {
2188 let mut value = String::from("hi");
2192 let mut sel = TextSelection::caret(2);
2193 for ctrl in ["\u{8}", "\u{7f}", "\r", "\n", "\u{1b}", "\t"] {
2194 assert!(
2195 !apply_event(&mut value, &mut sel, &ev_text(ctrl)),
2196 "expected {ctrl:?} to be filtered"
2197 );
2198 assert_eq!(value, "hi");
2199 assert_eq!(sel, TextSelection::caret(2));
2200 }
2201 assert!(apply_event(&mut value, &mut sel, &ev_text("a\u{8}b")));
2203 assert_eq!(value, "hiab");
2204 assert_eq!(sel, TextSelection::caret(4));
2205 }
2206
2207 #[test]
2208 fn apply_text_input_drops_when_ctrl_or_cmd_is_held() {
2209 let mut value = String::from("hello");
2214 let mut sel = TextSelection::range(0, 5);
2215 let ctrl = KeyModifiers {
2216 ctrl: true,
2217 ..Default::default()
2218 };
2219 let cmd = KeyModifiers {
2220 logo: true,
2221 ..Default::default()
2222 };
2223 assert!(!apply_event(
2224 &mut value,
2225 &mut sel,
2226 &ev_text_with_mods("c", ctrl)
2227 ));
2228 assert_eq!(value, "hello");
2229 assert!(!apply_event(
2230 &mut value,
2231 &mut sel,
2232 &ev_text_with_mods("v", cmd)
2233 ));
2234 assert_eq!(value, "hello");
2235 let altgr = KeyModifiers {
2237 ctrl: true,
2238 alt: true,
2239 ..Default::default()
2240 };
2241 let mut value = String::from("");
2242 let mut sel = TextSelection::caret(0);
2243 assert!(apply_event(
2244 &mut value,
2245 &mut sel,
2246 &ev_text_with_mods("é", altgr)
2247 ));
2248 assert_eq!(value, "é");
2249 }
2250
2251 #[test]
2252 fn text_input_value_emits_a_single_glyph_run() {
2253 use crate::draw_ops::draw_ops;
2259 use crate::ir::DrawOp;
2260 let mut tree =
2261 crate::column([text_input("Type", TextSelection::caret(1)).key("ti")]).padding(20.0);
2262 let mut state = UiState::new();
2263 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
2264
2265 let ops = draw_ops(&tree, &state);
2266 let glyph_runs = ops
2267 .iter()
2268 .filter(|op| matches!(op, DrawOp::GlyphRun { id, .. } if id.contains("text_input[ti]")))
2269 .count();
2270 assert_eq!(
2271 glyph_runs, 1,
2272 "value should shape as one run; got {glyph_runs}"
2273 );
2274 }
2275
2276 #[test]
2277 fn clipboard_request_detects_ctrl_c_x_v() {
2278 let ctrl = KeyModifiers {
2279 ctrl: true,
2280 ..Default::default()
2281 };
2282 let cases = [
2283 ("c", ClipboardKind::Copy),
2284 ("C", ClipboardKind::Copy),
2285 ("x", ClipboardKind::Cut),
2286 ("v", ClipboardKind::Paste),
2287 ];
2288 for (ch, expected) in cases {
2289 let e = ev_key_with_mods(LogicalKey::Character(ch.into()), ctrl);
2290 assert_eq!(clipboard_request(&e), Some(expected), "char {ch:?}");
2291 }
2292 }
2293
2294 #[test]
2295 fn clipboard_request_accepts_cmd_on_macos() {
2296 let logo = KeyModifiers {
2299 logo: true,
2300 ..Default::default()
2301 };
2302 let e = ev_key_with_mods(LogicalKey::Character("c".into()), logo);
2303 assert_eq!(clipboard_request(&e), Some(ClipboardKind::Copy));
2304 }
2305
2306 #[test]
2307 fn clipboard_request_detects_semantic_clipboard_keys() {
2308 let cases = [
2309 (NamedKey::Copy, ClipboardKind::Copy),
2310 (NamedKey::Cut, ClipboardKind::Cut),
2311 (NamedKey::Paste, ClipboardKind::Paste),
2312 ];
2313 for (named, expected) in cases {
2314 let e = ev_key(LogicalKey::Named(named));
2315 assert_eq!(
2316 clipboard_request(&e),
2317 Some(expected),
2318 "semantic key {named:?}"
2319 );
2320 }
2321 }
2322
2323 #[test]
2324 fn clipboard_request_rejects_with_shift_or_alt() {
2325 let e = ev_key_with_mods(
2327 LogicalKey::Character("c".into()),
2328 KeyModifiers {
2329 ctrl: true,
2330 shift: true,
2331 ..Default::default()
2332 },
2333 );
2334 assert_eq!(clipboard_request(&e), None);
2335
2336 let e = ev_key_with_mods(
2337 LogicalKey::Character("v".into()),
2338 KeyModifiers {
2339 ctrl: true,
2340 alt: true,
2341 ..Default::default()
2342 },
2343 );
2344 assert_eq!(clipboard_request(&e), None);
2345 }
2346
2347 #[test]
2348 fn clipboard_request_ignores_other_keys_and_event_kinds() {
2349 let e = ev_key(LogicalKey::Character("c".into()));
2351 assert_eq!(clipboard_request(&e), None);
2352 let e = ev_key_with_mods(
2354 LogicalKey::Character("a".into()),
2355 KeyModifiers {
2356 ctrl: true,
2357 ..Default::default()
2358 },
2359 );
2360 assert_eq!(clipboard_request(&e), None);
2361 assert_eq!(clipboard_request(&ev_text("c")), None);
2363 }
2364
2365 fn password_opts() -> TextInputOpts<'static> {
2366 TextInputOpts::default().password()
2367 }
2368
2369 #[test]
2370 fn password_input_renders_value_as_bullets_not_plaintext() {
2371 let el = text_input_with("hunter2", TextSelection::caret(0), password_opts());
2374 let leaf = content_children(&el)
2375 .iter()
2376 .find(|c| matches!(c.kind, Kind::Text))
2377 .expect("text leaf");
2378 assert_eq!(leaf.text.as_deref(), Some("•••••••"));
2379 }
2380
2381 #[test]
2382 fn password_input_caret_position_uses_masked_widths() {
2383 use crate::text::metrics::line_width;
2387 let value = "abc";
2388 let head = 2;
2389 let el = text_input_with(value, TextSelection::caret(head), password_opts());
2390 let caret = content_children(&el)
2391 .iter()
2392 .find(|c| matches!(c.kind, Kind::Custom("text_input_caret")))
2393 .expect("caret child");
2394 let expected = line_width("••", tokens::TEXT_SM.size, FontWeight::Regular, false);
2396 assert!(
2397 (caret.translate.0 - expected).abs() < 0.01,
2398 "caret translate.x = {}, expected {}",
2399 caret.translate.0,
2400 expected
2401 );
2402 }
2403
2404 #[test]
2405 fn password_pointer_click_maps_back_to_original_byte() {
2406 let mut value = String::from("abcde");
2409 let mut sel = TextSelection::default();
2410 let target = ti_target();
2411 let down = ev_pointer_down(
2412 target.clone(),
2413 (target.rect.x + target.rect.w - 4.0, target.rect.y + 18.0),
2414 KeyModifiers::default(),
2415 );
2416 assert!(apply_event_with(
2417 &mut value,
2418 &mut sel,
2419 &down,
2420 &password_opts()
2421 ));
2422 assert_eq!(sel.head, value.len());
2423 }
2424
2425 #[test]
2426 fn password_pointer_click_with_multibyte_value() {
2427 let mut value = String::from("éé");
2431 let mut sel = TextSelection::default();
2432 let target = ti_target();
2433 let bullet_w = metrics::line_width("•", tokens::TEXT_SM.size, FontWeight::Regular, false);
2435 let click_x = target.rect.x + tokens::SPACE_3 + bullet_w * 1.4;
2436 let down = ev_pointer_down(
2437 target,
2438 (click_x, ti_target().rect.y + 18.0),
2439 KeyModifiers::default(),
2440 );
2441 assert!(apply_event_with(
2442 &mut value,
2443 &mut sel,
2444 &down,
2445 &password_opts()
2446 ));
2447 assert!(
2451 value.is_char_boundary(sel.head),
2452 "head={} not on a char boundary in {value:?}",
2453 sel.head
2454 );
2455 assert!(sel.head == 2 || sel.head == 4, "head={}", sel.head);
2456 }
2457
2458 #[test]
2459 fn password_clipboard_request_suppresses_copy_and_cut_only() {
2460 let ctrl = KeyModifiers {
2461 ctrl: true,
2462 ..Default::default()
2463 };
2464 let opts = password_opts();
2465 let copy = ev_key_with_mods(LogicalKey::Character("c".into()), ctrl);
2466 let cut = ev_key_with_mods(LogicalKey::Character("x".into()), ctrl);
2467 let paste = ev_key_with_mods(LogicalKey::Character("v".into()), ctrl);
2468 assert_eq!(clipboard_request_for(©, &opts), None);
2469 assert_eq!(clipboard_request_for(&cut, &opts), None);
2470 assert_eq!(
2471 clipboard_request_for(&paste, &opts),
2472 Some(ClipboardKind::Paste)
2473 );
2474 let plain = TextInputOpts::default();
2476 assert_eq!(
2477 clipboard_request_for(©, &plain),
2478 Some(ClipboardKind::Copy)
2479 );
2480 }
2481
2482 #[test]
2483 fn placeholder_renders_only_when_value_is_empty() {
2484 let opts = TextInputOpts::default().placeholder("Email");
2485 let empty = text_input_with("", TextSelection::default(), opts);
2486 let muted_leaf = content_children(&empty)
2487 .iter()
2488 .find(|c| matches!(c.kind, Kind::Text) && c.text.as_deref() == Some("Email"));
2489 assert!(muted_leaf.is_some(), "placeholder leaf should be present");
2490
2491 let nonempty = text_input_with("hi", TextSelection::caret(2), opts);
2492 let muted_leaf = content_children(&nonempty)
2493 .iter()
2494 .find(|c| matches!(c.kind, Kind::Text) && c.text.as_deref() == Some("Email"));
2495 assert!(
2496 muted_leaf.is_none(),
2497 "placeholder should not render once the field has a value"
2498 );
2499 }
2500
2501 #[test]
2502 fn long_value_with_caret_at_end_shifts_content_left_to_keep_caret_in_view() {
2503 use crate::tree::Size;
2512 let value = "abcdefghijklmnopqrstuvwxyz0123456789".repeat(2);
2513 let mut root = super::text_input(
2514 "ti",
2515 &value,
2516 &as_selection_in("ti", TextSelection::caret(value.len())),
2517 )
2518 .width(Size::Fixed(120.0));
2519 let mut ui_state = crate::state::UiState::new();
2520 crate::layout::layout(&mut root, &mut ui_state, Rect::new(0.0, 0.0, 120.0, 40.0));
2521
2522 let inner = &root.children[0];
2524 let text_leaf = inner
2525 .children
2526 .iter()
2527 .find(|c| matches!(c.kind, Kind::Text))
2528 .expect("text leaf");
2529 let leaf_rect = text_leaf.computed_rect;
2530
2531 let inner_rect = inner.computed_rect;
2535 assert!(
2536 leaf_rect.x < inner_rect.x,
2537 "text leaf rect.x={} should be left of inner rect.x={} after \
2538 horizontal caret-into-view; layout did not shift content",
2539 leaf_rect.x,
2540 inner_rect.x,
2541 );
2542 }
2543
2544 #[test]
2545 fn short_value_does_not_shift_content() {
2546 use crate::tree::Size;
2550 let mut root =
2551 super::text_input("ti", "hi", &as_selection_in("ti", TextSelection::caret(2)))
2552 .width(Size::Fixed(120.0));
2553 let mut ui_state = crate::state::UiState::new();
2554 crate::layout::layout(&mut root, &mut ui_state, Rect::new(0.0, 0.0, 120.0, 40.0));
2555
2556 let inner = &root.children[0];
2557 let text_leaf = inner
2558 .children
2559 .iter()
2560 .find(|c| matches!(c.kind, Kind::Text))
2561 .expect("text leaf");
2562 let leaf_rect = text_leaf.computed_rect;
2563 let inner_rect = inner.computed_rect;
2564 assert!(
2565 (leaf_rect.x - inner_rect.x).abs() < 0.5,
2566 "short value should not shift; got leaf.x={} inner.x={}",
2567 leaf_rect.x,
2568 inner_rect.x
2569 );
2570 }
2571
2572 fn as_selection_in(key: &str, sel: TextSelection) -> Selection {
2575 Selection {
2576 range: Some(SelectionRange {
2577 anchor: SelectionPoint::new(key, sel.anchor),
2578 head: SelectionPoint::new(key, sel.head),
2579 }),
2580 }
2581 }
2582
2583 #[test]
2584 fn max_length_truncates_text_input_inserts() {
2585 let mut value = String::from("ab");
2586 let mut sel = TextSelection::caret(2);
2587 let opts = TextInputOpts::default().max_length(4);
2588 assert!(apply_event_with(
2590 &mut value,
2591 &mut sel,
2592 &ev_text("cdef"),
2593 &opts
2594 ));
2595 assert_eq!(value, "abcd");
2596 assert_eq!(sel, TextSelection::caret(4));
2597 assert!(!apply_event_with(
2599 &mut value,
2600 &mut sel,
2601 &ev_text("z"),
2602 &opts
2603 ));
2604 assert_eq!(value, "abcd");
2605 }
2606
2607 #[test]
2608 fn max_length_replaces_selection_with_capacity_freed_by_removal() {
2609 let mut value = String::from("abc");
2612 let mut sel = TextSelection::range(0, 3); let opts = TextInputOpts::default().max_length(4);
2614 assert!(apply_event_with(
2615 &mut value,
2616 &mut sel,
2617 &ev_text("12345"),
2618 &opts
2619 ));
2620 assert_eq!(value, "1234");
2621 assert_eq!(sel, TextSelection::caret(4));
2622 }
2623
2624 #[test]
2625 fn replace_selection_with_max_length_clips_a_paste() {
2626 let mut value = String::from("ab");
2627 let mut sel = TextSelection::caret(2);
2628 let opts = TextInputOpts::default().max_length(5);
2629 let inserted = replace_selection_with(&mut value, &mut sel, "0123456789", &opts);
2631 assert_eq!(value, "ab012");
2632 assert_eq!(inserted, 3);
2633 assert_eq!(sel, TextSelection::caret(5));
2634 }
2635
2636 #[test]
2637 fn max_length_does_not_shrink_an_already_overlong_value() {
2638 let mut value = String::from("abcdef");
2641 let mut sel = TextSelection::caret(6);
2642 let opts = TextInputOpts::default().max_length(3);
2643 assert!(!apply_event_with(
2645 &mut value,
2646 &mut sel,
2647 &ev_text("z"),
2648 &opts
2649 ));
2650 assert_eq!(value, "abcdef");
2651 assert!(apply_event_with(
2654 &mut value,
2655 &mut sel,
2656 &ev_key(LogicalKey::Named(NamedKey::Backspace)),
2657 &opts
2658 ));
2659 assert_eq!(value, "abcde");
2660 }
2661
2662 #[test]
2663 fn end_to_end_drag_select_through_runner_core() {
2664 let mut value = String::from("hello world");
2668 let mut sel = TextSelection::default();
2669 let mut tree = crate::column([text_input(&value, sel).key("ti")]).padding(20.0);
2670 let mut core = RunnerCore::new();
2671 let mut state = UiState::new();
2672 layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
2673 core.ui_state = state;
2674 core.snapshot(&tree, &mut Default::default());
2675
2676 let rect = core.rect_of_key("ti").expect("ti rect");
2677 let down_x = rect.x + 8.0;
2678 let drag_x = rect.x + 80.0;
2679 let cy = rect.y + rect.h * 0.5;
2680
2681 core.pointer_moved(Pointer::moving(down_x, cy));
2682 let down = core
2683 .pointer_down(Pointer::mouse(down_x, cy, PointerButton::Primary))
2684 .into_iter()
2685 .find(|e| e.kind == UiEventKind::PointerDown)
2686 .expect("pointer_down emits PointerDown");
2687 assert!(apply_event(&mut value, &mut sel, &down));
2688
2689 let drag = core
2690 .pointer_moved(Pointer::moving(drag_x, cy))
2691 .events
2692 .into_iter()
2693 .find(|e| e.kind == UiEventKind::Drag)
2694 .expect("Drag while pressed");
2695 assert!(apply_event(&mut value, &mut sel, &drag));
2696
2697 let events = core.pointer_up(Pointer::mouse(drag_x, cy, PointerButton::Primary));
2698 for e in &events {
2699 apply_event(&mut value, &mut sel, e);
2700 }
2701 assert!(
2702 !sel.is_collapsed(),
2703 "expected drag-select to leave a non-empty selection"
2704 );
2705 assert_eq!(
2706 sel.anchor, 0,
2707 "anchor should sit at the down position (caret 0)"
2708 );
2709 assert!(
2710 sel.head > 0 && sel.head <= value.len(),
2711 "head={} value.len={}",
2712 sel.head,
2713 value.len()
2714 );
2715 }
2716
2717 #[test]
2725 fn apply_event_writes_back_under_the_inputs_key() {
2726 let mut value = String::new();
2728 let mut sel = Selection::default();
2729 let event = ev_text("h");
2730 assert!(super::apply_event(&mut value, &mut sel, &event, "name"));
2731 assert_eq!(value, "h");
2732 let r = sel.range.as_ref().expect("selection set");
2733 assert_eq!(r.anchor.key, "name");
2734 assert_eq!(r.head.key, "name");
2735 assert_eq!(r.head.byte, 1);
2736 }
2737
2738 #[test]
2739 fn apply_event_claims_selection_when_event_routed_from_elsewhere() {
2740 let mut value = String::new();
2746 let mut sel = Selection {
2747 range: Some(SelectionRange {
2748 anchor: SelectionPoint::new("para-a", 0),
2749 head: SelectionPoint::new("para-a", 5),
2750 }),
2751 };
2752 let event = ev_text("x");
2753 assert!(super::apply_event(&mut value, &mut sel, &event, "email"));
2754 assert_eq!(value, "x");
2755 let r = sel.range.as_ref().unwrap();
2756 assert_eq!(r.anchor.key, "email", "selection ownership migrated");
2757 assert_eq!(r.head.byte, 1);
2758 }
2759
2760 #[test]
2761 fn apply_event_leaves_selection_alone_when_event_is_unhandled() {
2762 let mut value = String::from("hi");
2766 let mut sel = Selection {
2767 range: Some(SelectionRange {
2768 anchor: SelectionPoint::new("para-a", 0),
2769 head: SelectionPoint::new("para-a", 3),
2770 }),
2771 };
2772 let event = ev_key(LogicalKey::Named(NamedKey::F1));
2773 assert!(!super::apply_event(&mut value, &mut sel, &event, "name"));
2774 let r = sel.range.as_ref().unwrap();
2776 assert_eq!(r.anchor.key, "para-a");
2777 assert_eq!(r.head.byte, 3);
2778 }
2779
2780 #[test]
2781 fn text_input_renders_caret_at_local_byte_when_selection_is_within_key() {
2782 let sel = Selection::caret("name", 2);
2783 let el = super::text_input("name", "hello", &sel);
2784 assert_eq!(el.key.as_deref(), Some("name"));
2786 let caret = content_children(&el)
2788 .iter()
2789 .find(|c| matches!(c.kind, Kind::Custom("text_input_caret")))
2790 .expect("caret child");
2791 let expected = metrics::line_width("he", tokens::TEXT_SM.size, FontWeight::Regular, false);
2792 assert!(
2793 (caret.translate.0 - expected).abs() < 0.01,
2794 "caret.x={} expected {}",
2795 caret.translate.0,
2796 expected
2797 );
2798 }
2799
2800 #[test]
2801 fn text_input_omits_caret_when_selection_lives_elsewhere() {
2802 let sel = Selection {
2809 range: Some(SelectionRange {
2810 anchor: SelectionPoint::new("other", 0),
2811 head: SelectionPoint::new("other", 5),
2812 }),
2813 };
2814 let el = super::text_input("name", "hello", &sel);
2815 let band = el
2816 .children
2817 .iter()
2818 .find(|c| matches!(c.kind, Kind::Custom("text_input_selection")));
2819 assert!(band.is_none(), "no band when selection lives elsewhere");
2820 let caret = el
2821 .children
2822 .iter()
2823 .find(|c| matches!(c.kind, Kind::Custom("text_input_caret")));
2824 assert!(
2825 caret.is_none(),
2826 "no caret when selection lives elsewhere — focus-fade has nothing to bring back to byte 0"
2827 );
2828 }
2829
2830 fn ctrl_mods() -> KeyModifiers {
2831 KeyModifiers {
2832 ctrl: true,
2833 ..Default::default()
2834 }
2835 }
2836
2837 fn ctrl_shift_mods() -> KeyModifiers {
2838 KeyModifiers {
2839 ctrl: true,
2840 shift: true,
2841 ..Default::default()
2842 }
2843 }
2844
2845 #[test]
2846 fn ctrl_backspace_deletes_previous_word() {
2847 let mut value = String::from("hello world foo");
2848 let mut sel = TextSelection::caret(value.len());
2849 assert!(apply_event(
2850 &mut value,
2851 &mut sel,
2852 &ev_key_with_mods(LogicalKey::Named(NamedKey::Backspace), ctrl_mods())
2853 ));
2854 assert_eq!(value, "hello world ");
2855 assert_eq!(sel, TextSelection::caret(value.len()));
2856 }
2857
2858 #[test]
2859 fn ctrl_backspace_at_caret_zero_is_noop() {
2860 let mut value = String::from("hello");
2861 let mut sel = TextSelection::caret(0);
2862 assert!(!apply_event(
2863 &mut value,
2864 &mut sel,
2865 &ev_key_with_mods(LogicalKey::Named(NamedKey::Backspace), ctrl_mods())
2866 ));
2867 assert_eq!(value, "hello");
2868 }
2869
2870 #[test]
2871 fn ctrl_w_deletes_previous_word_like_terminal() {
2872 let mut value = String::from("alpha beta gamma");
2873 let mut sel = TextSelection::caret(value.len());
2874 assert!(apply_event(
2875 &mut value,
2876 &mut sel,
2877 &ev_key_with_mods(LogicalKey::Character("w".into()), ctrl_mods())
2878 ));
2879 assert_eq!(value, "alpha beta ");
2880 }
2881
2882 #[test]
2883 fn ctrl_delete_deletes_next_word() {
2884 let mut value = String::from("alpha beta gamma");
2885 let mut sel = TextSelection::caret(0);
2886 assert!(apply_event(
2887 &mut value,
2888 &mut sel,
2889 &ev_key_with_mods(LogicalKey::Named(NamedKey::Delete), ctrl_mods())
2890 ));
2891 assert_eq!(value, " beta gamma");
2892 assert_eq!(sel, TextSelection::caret(0));
2893 }
2894
2895 #[test]
2896 fn ctrl_arrow_left_jumps_word_backward() {
2897 let mut value = String::from("alpha beta gamma");
2898 let mut sel = TextSelection::caret(value.len());
2899 assert!(apply_event(
2900 &mut value,
2901 &mut sel,
2902 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowLeft), ctrl_mods())
2903 ));
2904 assert_eq!(sel, TextSelection::caret(11));
2906 }
2907
2908 #[test]
2909 fn ctrl_arrow_right_jumps_word_forward() {
2910 let mut value = String::from("alpha beta gamma");
2911 let mut sel = TextSelection::caret(0);
2912 assert!(apply_event(
2913 &mut value,
2914 &mut sel,
2915 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowRight), ctrl_mods())
2916 ));
2917 assert_eq!(sel, TextSelection::caret(5));
2919 }
2920
2921 #[test]
2922 fn ctrl_shift_arrow_extends_selection_by_word() {
2923 let mut value = String::from("alpha beta gamma");
2924 let mut sel = TextSelection::caret(0);
2925 assert!(apply_event(
2926 &mut value,
2927 &mut sel,
2928 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowRight), ctrl_shift_mods())
2929 ));
2930 assert_eq!(sel, TextSelection::range(0, 5));
2931 assert!(apply_event(
2932 &mut value,
2933 &mut sel,
2934 &ev_key_with_mods(LogicalKey::Named(NamedKey::ArrowRight), ctrl_shift_mods())
2935 ));
2936 assert_eq!(sel, TextSelection::range(0, 10));
2937 }
2938}