1use gpui::{
7 prelude::*, px, App, Entity, FocusHandle, Focusable, IntoElement, KeyDownEvent, Pixels,
8 RenderOnce, SharedString, Styled, Window,
9};
10use herogpui_core::{element_id, FieldVariant};
11use herogpui_theme::ActiveTheme;
12
13use crate::a11y::A11y as _;
14
15pub struct InputState {
17 value: String,
18 cursor: usize,
20 anchor: Option<usize>,
22 marked: Option<std::ops::Range<usize>>,
23 pub(crate) focus_handle: FocusHandle,
24 pub(crate) clear_focus_handle: FocusHandle,
27 name: Option<SharedString>,
31 validation_behavior: crate::form::ValidationBehavior,
33 validity: crate::validation::Validity,
37 is_successful: bool,
41 is_read_only: bool,
49 routed_errors: Vec<SharedString>,
58 routed_revision: u64,
66}
67
68impl InputState {
69 pub fn new(cx: &mut App) -> Self {
71 Self {
72 value: String::new(),
73 cursor: 0,
74 anchor: None,
75 marked: None,
76 focus_handle: cx.focus_handle().tab_stop(true),
78 clear_focus_handle: cx.focus_handle(),
80 name: None,
81 validation_behavior: crate::form::ValidationBehavior::Native,
82 validity: crate::validation::Validity::default(),
83 is_successful: true,
84 is_read_only: false,
85 routed_errors: Vec::new(),
86 routed_revision: 0,
87 }
88 }
89
90 pub fn with_value(cx: &mut App, value: impl Into<String>) -> Self {
96 let mut state = Self::new(cx);
97 state.set_value(value);
98 state
99 }
100
101 pub fn value(&self) -> &str {
103 &self.value
104 }
105
106 pub fn name(&self) -> Option<SharedString> {
108 self.name.clone()
109 }
110
111 pub fn set_name(&mut self, name: Option<SharedString>) {
114 self.name = name;
115 }
116
117 pub fn validation_behavior(&self) -> crate::form::ValidationBehavior {
119 self.validation_behavior
120 }
121
122 pub fn set_validation_behavior(&mut self, behavior: crate::form::ValidationBehavior) {
124 self.validation_behavior = behavior;
125 }
126
127 pub(crate) fn set_validity(&mut self, validity: crate::validation::Validity) {
131 self.validity = validity;
132 }
133
134 pub(crate) fn validity(&self) -> &crate::validation::Validity {
136 &self.validity
137 }
138
139 pub(crate) fn is_successful(&self) -> bool {
140 self.is_successful
141 }
142
143 pub(crate) fn set_successful(&mut self, is_successful: bool) {
144 self.is_successful = is_successful;
145 }
146
147 pub(crate) fn is_read_only(&self) -> bool {
149 self.is_read_only
150 }
151
152 pub(crate) fn set_read_only(&mut self, is_read_only: bool) {
153 self.is_read_only = is_read_only;
154 }
155
156 pub fn routed_errors(&self) -> &[SharedString] {
161 &self.routed_errors
162 }
163
164 pub(crate) fn routed_revision(&self) -> u64 {
167 self.routed_revision
168 }
169
170 pub(crate) fn set_routed(&mut self, messages: Vec<SharedString>, revision: u64) {
174 self.routed_errors = messages;
175 self.routed_revision = revision;
176 }
177
178 pub(crate) fn clear_routed_errors(&mut self) {
184 self.routed_errors.clear();
185 }
186
187 pub fn set_value(&mut self, value: impl Into<String>) {
189 self.value = value.into();
190 self.marked = None;
191 self.anchor = None;
192 self.cursor = self.value.chars().count();
193 }
194
195 pub fn is_empty(&self) -> bool {
197 self.value.is_empty()
198 }
199
200 pub fn clear_focus_handle(&self) -> FocusHandle {
202 self.clear_focus_handle.clone()
203 }
204
205 pub fn selection(&self) -> Option<(usize, usize)> {
207 let a = self.anchor?;
208 let c = self.cursor;
209 if a == c {
210 None
211 } else {
212 Some((a.min(c), a.max(c)))
213 }
214 }
215}
216
217impl Focusable for InputState {
218 fn focus_handle(&self, _cx: &App) -> FocusHandle {
219 self.focus_handle.clone()
220 }
221}
222
223#[allow(clippy::too_many_arguments)]
227fn validate_value(
228 value: &str,
229 input_type: InputType,
230 min_length: Option<usize>,
231 min: Option<f64>,
232 max: Option<f64>,
233 step: Option<f64>,
234 pattern: Option<&dyn Fn(&str) -> bool>,
235) -> InputValidity {
236 if value.is_empty() {
239 return InputValidity::Valid;
240 }
241
242 if let Some(f) = pattern {
243 if !f(value) {
244 return InputValidity::PatternMismatch;
245 }
246 }
247
248 if min_length.is_some_and(|n| value.chars().count() < n) {
249 return InputValidity::TooShort;
250 }
251
252 if input_type == InputType::Number {
255 if let Ok(n) = value.parse::<f64>() {
256 if min.is_some_and(|lo| n < lo) {
257 return InputValidity::BelowMin;
258 }
259 if max.is_some_and(|hi| n > hi) {
260 return InputValidity::AboveMax;
261 }
262 if let Some(step) = step.filter(|s| *s > 0.0) {
263 let base = min.unwrap_or(0.0);
264 let steps = (n - base) / step;
265 if (steps - steps.round()).abs() > 1e-6 {
266 return InputValidity::OffStep;
267 }
268 }
269 }
270 }
271
272 InputValidity::Valid
273}
274
275fn refresh_stored_validity(
284 state: &Entity<InputState>,
285 is_invalid: bool,
286 validation_errors: &[SharedString],
287 validate: Option<&crate::validation::Validator<str>>,
288 error_message: Option<SharedString>,
289 native_valid: &dyn Fn(&str) -> bool,
290 cx: &mut App,
291) {
292 let value = state.read(cx).value().to_owned();
293 let mut validity = crate::validation::resolve(
294 is_invalid,
295 validation_errors,
296 validate.and_then(|f| f(&value)),
297 error_message,
298 );
299 if !native_valid(&value) {
300 validity.is_invalid = true;
301 }
302 if state.read(cx).validity() != &validity {
303 state.update(cx, |s, _| s.set_validity(validity));
304 }
305}
306
307fn accepts_char(
312 len_chars: usize,
313 selected_chars: usize,
314 ch: char,
315 input_type: InputType,
316 max_length: Option<usize>,
317) -> bool {
318 if !input_type.accepts(ch) {
319 return false;
320 }
321 match max_length {
322 Some(max) => len_chars.saturating_sub(selected_chars) < max,
324 None => true,
325 }
326}
327
328fn state_accepts(
330 state: &InputState,
331 ch: char,
332 input_type: InputType,
333 max_length: Option<usize>,
334) -> bool {
335 let selected = state.selection().map_or(0, |(lo, hi)| hi - lo);
336 accepts_char(
337 state.value.chars().count(),
338 selected,
339 ch,
340 input_type,
341 max_length,
342 )
343}
344
345fn insert_char(state: &mut InputState, ch: char) {
346 delete_selection(state);
347 state.anchor = None;
352 let byte_idx = char_to_byte(&state.value, state.cursor);
353 state.value.insert(byte_idx, ch);
354 state.cursor += 1;
355}
356
357fn delete_selection(state: &mut InputState) -> bool {
359 if let Some((lo, hi)) = state.selection() {
360 let lo_b = char_to_byte(&state.value, lo);
361 let hi_b = char_to_byte(&state.value, hi);
362 state.value.replace_range(lo_b..hi_b, "");
363 state.cursor = lo;
364 state.anchor = None;
365 true
366 } else {
367 false
368 }
369}
370
371fn prev_grapheme(value: &str, cursor: usize) -> usize {
375 if cursor == 0 {
376 return 0;
377 }
378 let byte = char_to_byte(value, cursor);
379 let mut graphemes = unicode_segmentation::GraphemeCursor::new(byte, value.len(), true);
380 match graphemes.prev_boundary(value, 0) {
381 Ok(Some(prev)) => byte_to_char(value, prev),
382 _ => cursor - 1,
383 }
384}
385
386fn next_grapheme(value: &str, cursor: usize) -> usize {
389 let byte = char_to_byte(value, cursor);
390 if byte >= value.len() {
391 return cursor;
392 }
393 let mut graphemes = unicode_segmentation::GraphemeCursor::new(byte, value.len(), true);
394 match graphemes.next_boundary(value, 0) {
395 Ok(Some(next)) => byte_to_char(value, next),
396 _ => cursor + 1,
397 }
398}
399
400fn backspace(state: &mut InputState) -> bool {
401 if delete_selection(state) {
402 return true;
403 }
404 if state.cursor == 0 {
405 return false;
406 }
407 let byte_idx = char_to_byte(&state.value, state.cursor);
408 let prev_char = prev_grapheme(&state.value, state.cursor);
409 let prev = char_to_byte(&state.value, prev_char);
410 state.value.replace_range(prev..byte_idx, "");
411 state.cursor = prev_char;
412 true
413}
414
415fn delete(state: &mut InputState) -> bool {
416 if delete_selection(state) {
417 return true;
418 }
419 let len = state.value.chars().count();
420 if state.cursor >= len {
421 return false;
422 }
423 let byte_idx = char_to_byte(&state.value, state.cursor);
424 let next = char_to_byte(&state.value, next_grapheme(&state.value, state.cursor));
425 state.value.replace_range(byte_idx..next, "");
426 true
427}
428
429fn move_left(state: &mut InputState, extend: bool) {
430 if !extend {
431 state.anchor = None;
432 } else if state.anchor.is_none() {
433 state.anchor = Some(state.cursor);
434 }
435 state.cursor = prev_grapheme(&state.value, state.cursor);
436}
437
438fn move_right(state: &mut InputState, extend: bool) {
439 if !extend {
440 state.anchor = None;
441 } else if state.anchor.is_none() {
442 state.anchor = Some(state.cursor);
443 }
444 state.cursor = next_grapheme(&state.value, state.cursor);
445}
446
447fn move_home(state: &mut InputState, extend: bool) {
448 if !extend {
449 state.anchor = None;
450 } else if state.anchor.is_none() {
451 state.anchor = Some(state.cursor);
452 }
453 state.cursor = 0;
454}
455
456fn move_end(state: &mut InputState, extend: bool) {
457 if !extend {
458 state.anchor = None;
459 } else if state.anchor.is_none() {
460 state.anchor = Some(state.cursor);
461 }
462 state.cursor = state.value.chars().count();
463}
464
465fn select_all(state: &mut InputState) {
466 state.anchor = Some(0);
467 state.cursor = state.value.chars().count();
468}
469
470fn slice_selection(value: &str, selection: Option<(usize, usize)>) -> Option<String> {
475 let (lo, hi) = selection?;
476 let lo_b = char_to_byte(value, lo);
477 let hi_b = char_to_byte(value, hi);
478 Some(value[lo_b..hi_b].to_owned())
479}
480
481fn before_move(state: &mut InputState, extend: bool) {
484 if !extend {
485 state.anchor = None;
486 } else if state.anchor.is_none() {
487 state.anchor = Some(state.cursor);
488 }
489}
490
491fn is_word(c: char) -> bool {
493 c.is_alphanumeric() || c == '_'
494}
495
496fn word_target(value: &str, cursor: usize, forward: bool) -> usize {
499 let chars: Vec<char> = value.chars().collect();
500 let mut i = cursor.min(chars.len());
501 if forward {
502 while i < chars.len() && !is_word(chars[i]) {
503 i += 1;
504 }
505 while i < chars.len() && is_word(chars[i]) {
506 i += 1;
507 }
508 } else {
509 while i > 0 && !is_word(chars[i - 1]) {
510 i -= 1;
511 }
512 while i > 0 && is_word(chars[i - 1]) {
513 i -= 1;
514 }
515 }
516 i
517}
518
519fn move_word(state: &mut InputState, forward: bool, extend: bool) {
520 before_move(state, extend);
521 state.cursor = word_target(&state.value, state.cursor, forward);
522}
523
524fn displayed_value(state: &InputState, masks: bool) -> String {
529 if masks {
530 "\u{2022}".repeat(state.value.chars().count())
531 } else {
532 state.value.clone()
533 }
534}
535
536fn char_at_x(
543 value: &str,
544 x: Pixels,
545 font: &gpui::Font,
546 font_size: Pixels,
547 window: &mut Window,
548) -> usize {
549 if value.is_empty() {
550 return 0;
551 }
552 let run = gpui::TextRun {
553 len: value.len(),
554 font: font.clone(),
555 color: gpui::black(),
557 background_color: None,
558 underline: None,
559 strikethrough: None,
560 };
561 let line = window.text_system().shape_line(
562 SharedString::from(value.to_owned()),
563 font_size,
564 &[run],
565 None,
566 );
567 let byte = line.closest_index_for_x(x.max(px(0.)));
568 value[..byte.min(value.len())].chars().count()
569}
570
571fn char_at_point(
579 value: &str,
580 point: gpui::Point<Pixels>,
581 paragraphs: &[gpui::Bounds<Pixels>],
582 font: &gpui::Font,
583 font_size: Pixels,
584 line_height: Pixels,
585 window: &mut Window,
586) -> Option<usize> {
587 let lines: Vec<&str> = value.split('\n').collect();
588 let usable = paragraphs.len().min(lines.len());
589 if usable == 0 {
590 return None;
591 }
592
593 let index = (0..usable)
596 .find(|&i| {
597 let b = paragraphs[i];
598 point.y >= b.origin.y && point.y < b.origin.y + b.size.height.max(line_height)
599 })
600 .unwrap_or_else(|| {
601 (0..usable)
602 .min_by_key(|&i| {
603 let b = paragraphs[i];
604 let mid = b.origin.y + b.size.height.max(line_height) / 2.;
605 (f32::from(point.y - mid)).abs() as i64
606 })
607 .unwrap_or(0)
608 });
609
610 let offset: usize = lines[..index].iter().map(|l| l.chars().count() + 1).sum();
611 let line = lines[index];
612 if line.is_empty() {
613 return Some(offset);
614 }
615
616 let bounds = paragraphs[index];
617 let run = gpui::TextRun {
618 len: line.len(),
619 font: font.clone(),
620 color: gpui::black(),
622 background_color: None,
623 underline: None,
624 strikethrough: None,
625 };
626 let shaped = window
627 .text_system()
628 .shape_text(
629 SharedString::from(line.to_owned()),
630 font_size,
631 &[run],
632 Some(bounds.size.width.max(px(1.))),
633 None,
634 )
635 .ok()?;
636 let wrapped = shaped.first()?;
637 let local = gpui::point(point.x - bounds.origin.x, point.y - bounds.origin.y);
638 let byte = match wrapped.closest_index_for_position(local, line_height) {
639 Ok(byte) | Err(byte) => byte,
640 };
641 Some(offset + line[..byte.min(line.len())].chars().count())
642}
643
644fn line_bounds(value: &str, cursor: usize) -> (usize, usize) {
646 let chars: Vec<char> = value.chars().collect();
647 let mut start = cursor.min(chars.len());
648 while start > 0 && chars[start - 1] != '\n' {
649 start -= 1;
650 }
651 let mut end = cursor.min(chars.len());
652 while end < chars.len() && chars[end] != '\n' {
653 end += 1;
654 }
655 (start, end)
656}
657
658fn vertical_target(value: &str, cursor: usize, down: bool) -> usize {
666 let (start, end) = line_bounds(value, cursor);
667 let column = cursor - start;
668 if down {
669 let len = value.chars().count();
670 if end >= len {
671 return len;
672 }
673 let (next_start, next_end) = line_bounds(value, end + 1);
674 (next_start + column).min(next_end)
675 } else {
676 if start == 0 {
677 return 0;
678 }
679 let (prev_start, prev_end) = line_bounds(value, start - 1);
680 (prev_start + column).min(prev_end)
681 }
682}
683
684fn move_vertical(state: &mut InputState, down: bool, extend: bool) {
685 before_move(state, extend);
686 state.cursor = vertical_target(&state.value, state.cursor, down);
687}
688
689fn char_to_byte(s: &str, char_idx: usize) -> usize {
690 s.char_indices().nth(char_idx).map_or(s.len(), |(b, _)| b)
691}
692
693fn byte_to_char(s: &str, byte: usize) -> usize {
695 s[..byte.min(s.len())].chars().count()
696}
697
698#[derive(Clone, Copy, Debug, PartialEq, Eq)]
704pub enum InputValidity {
705 Valid,
707 TooShort,
709 BelowMin,
711 AboveMax,
713 OffStep,
715 PatternMismatch,
717}
718
719impl InputValidity {
720 pub fn is_valid(self) -> bool {
722 matches!(self, InputValidity::Valid)
723 }
724}
725
726#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
729pub enum InputType {
730 #[default]
732 Text,
733 Password,
735 Email,
737 Number,
739 Tel,
741 Url,
743 Search,
745}
746
747impl InputType {
748 pub(crate) fn a11y_role(self) -> crate::a11y::Role {
755 match self {
756 InputType::Text => crate::a11y::Role::TextInput,
757 InputType::Password => crate::a11y::Role::PasswordInput,
758 InputType::Email => crate::a11y::Role::EmailInput,
759 InputType::Number => crate::a11y::Role::NumberInput,
760 InputType::Tel => crate::a11y::Role::PhoneNumberInput,
761 InputType::Url => crate::a11y::Role::UrlInput,
762 InputType::Search => crate::a11y::Role::SearchInput,
763 }
764 }
765
766 pub const ALL: [InputType; 7] = [
768 InputType::Text,
769 InputType::Password,
770 InputType::Email,
771 InputType::Number,
772 InputType::Tel,
773 InputType::Url,
774 InputType::Search,
775 ];
776
777 pub fn label(self) -> &'static str {
779 match self {
780 InputType::Text => "Text",
781 InputType::Password => "Password",
782 InputType::Email => "Email",
783 InputType::Number => "Number",
784 InputType::Tel => "Tel",
785 InputType::Url => "Url",
786 InputType::Search => "Search",
787 }
788 }
789
790 fn masks(self) -> bool {
791 matches!(self, InputType::Password)
792 }
793
794 fn accepts(self, ch: char) -> bool {
796 match self {
797 InputType::Number => ch.is_ascii_digit() || ch == '-' || ch == '.',
798 _ => true,
799 }
800 }
801}
802
803type TextCallback = std::sync::Arc<dyn Fn(&str, &mut Window, &mut App) + 'static>;
804type ClearCallback = std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>;
805
806fn char_to_utf16(value: &str, offset: usize) -> usize {
807 value.chars().take(offset).map(char::len_utf16).sum()
808}
809
810fn utf16_to_char(value: &str, offset: usize) -> usize {
811 let mut units = 0;
812 value
813 .chars()
814 .take_while(|ch| {
815 units += ch.len_utf16();
816 units <= offset
817 })
818 .count()
819}
820
821struct PlatformTextInput {
822 state: Entity<InputState>,
823 on_edit: TextCallback,
824 input_type: InputType,
825 max_length: Option<usize>,
826 multiline: bool,
827 bounds: gpui::Bounds<Pixels>,
828 font: gpui::Font,
829 paragraphs: Entity<Vec<gpui::Bounds<Pixels>>>,
830}
831
832impl PlatformTextInput {
833 fn replace(
834 &mut self,
835 range: Option<std::ops::Range<usize>>,
836 text: &str,
837 marked_selection: Option<Option<std::ops::Range<usize>>>,
838 window: &mut Window,
839 cx: &mut App,
840 ) {
841 let changed = self.state.update(cx, |state, cx| {
842 if state.is_read_only || !state.is_successful {
843 return false;
844 }
845 let range = range
846 .map(|range| {
847 utf16_to_char(&state.value, range.start)..utf16_to_char(&state.value, range.end)
848 })
849 .or_else(|| state.marked.clone())
850 .unwrap_or_else(|| {
851 let (start, end) = state.selection().unwrap_or((state.cursor, state.cursor));
852 start..end
853 });
854 let old_value = state.value.clone();
855 let old_cursor = state.cursor;
856 let old_anchor = state.anchor;
857 state.anchor = Some(range.start);
858 state.cursor = range.end;
859 let mut inserted = false;
860 for ch in text.chars() {
861 if (ch == '\n' && self.multiline || !ch.is_control())
862 && state_accepts(state, ch, self.input_type, self.max_length)
863 {
864 insert_char(state, ch);
865 inserted = true;
866 }
867 }
868 if text.is_empty() {
869 delete_selection(state);
870 } else if !inserted {
871 state.cursor = old_cursor;
872 state.anchor = old_anchor;
873 return false;
874 }
875 let end = state.cursor;
876 state.anchor = None;
877 state.marked = marked_selection
878 .as_ref()
879 .and_then(|_| (end > range.start).then_some(range.start..end));
880 if let Some(Some(selection)) = marked_selection {
881 let inserted: String = state
882 .value
883 .chars()
884 .skip(range.start)
885 .take(end - range.start)
886 .collect();
887 state.anchor = Some(range.start + utf16_to_char(&inserted, selection.start));
888 state.cursor = range.start + utf16_to_char(&inserted, selection.end);
889 }
890 cx.notify();
891 state.value != old_value
892 });
893 if changed {
894 let value = self.state.read(cx).value.clone();
895 (self.on_edit)(&value, window, cx);
896 }
897 }
898}
899
900impl gpui::InputHandler for PlatformTextInput {
901 fn selected_text_range(
902 &mut self,
903 _: bool,
904 _: &mut Window,
905 cx: &mut App,
906 ) -> Option<gpui::UTF16Selection> {
907 let state = self.state.read(cx);
908 let (start, end) = state.selection().unwrap_or((state.cursor, state.cursor));
909 Some(gpui::UTF16Selection {
910 range: char_to_utf16(&state.value, start)..char_to_utf16(&state.value, end),
911 reversed: state.anchor.is_some_and(|anchor| anchor > state.cursor),
912 })
913 }
914
915 fn marked_text_range(
916 &mut self,
917 _: &mut Window,
918 cx: &mut App,
919 ) -> Option<std::ops::Range<usize>> {
920 let state = self.state.read(cx);
921 state.marked.as_ref().map(|range| {
922 char_to_utf16(&state.value, range.start)..char_to_utf16(&state.value, range.end)
923 })
924 }
925
926 fn text_for_range(
927 &mut self,
928 range: std::ops::Range<usize>,
929 actual: &mut Option<std::ops::Range<usize>>,
930 _: &mut Window,
931 cx: &mut App,
932 ) -> Option<String> {
933 let value = &self.state.read(cx).value;
934 let start = utf16_to_char(value, range.start);
935 let end = utf16_to_char(value, range.end);
936 *actual = Some(char_to_utf16(value, start)..char_to_utf16(value, end));
937 Some(
938 value
939 .chars()
940 .skip(start)
941 .take(end.saturating_sub(start))
942 .collect(),
943 )
944 }
945
946 fn replace_text_in_range(
947 &mut self,
948 range: Option<std::ops::Range<usize>>,
949 text: &str,
950 window: &mut Window,
951 cx: &mut App,
952 ) {
953 self.replace(range, text, None, window, cx);
954 }
955
956 fn replace_and_mark_text_in_range(
957 &mut self,
958 range: Option<std::ops::Range<usize>>,
959 text: &str,
960 selection: Option<std::ops::Range<usize>>,
961 window: &mut Window,
962 cx: &mut App,
963 ) {
964 self.replace(range, text, Some(selection), window, cx);
965 }
966
967 fn unmark_text(&mut self, _: &mut Window, cx: &mut App) {
968 self.state.update(cx, |state, cx| {
969 state.marked = None;
970 cx.notify();
971 });
972 }
973
974 fn set_selected_text_range(
975 &mut self,
976 range: std::ops::Range<usize>,
977 _: &mut Window,
978 cx: &mut App,
979 ) {
980 self.state.update(cx, |state, cx| {
981 state.anchor = Some(utf16_to_char(&state.value, range.start));
982 state.cursor = utf16_to_char(&state.value, range.end);
983 cx.notify();
984 });
985 }
986
987 fn text_length_utf16(&mut self, _: &mut Window, cx: &mut App) -> Option<usize> {
988 Some(self.state.read(cx).value.encode_utf16().count())
989 }
990
991 fn accepts_text_input(&mut self, _: &mut Window, cx: &mut App) -> bool {
992 let state = self.state.read(cx);
993 !state.is_read_only && state.is_successful
994 }
995
996 fn prefers_ime_for_printable_keys(&mut self, _: &mut Window, _: &mut App) -> bool {
997 true
998 }
999
1000 fn element_bounds(&mut self, _: &mut Window, _: &mut App) -> Option<gpui::Bounds<Pixels>> {
1001 Some(self.bounds)
1002 }
1003
1004 fn bounds_for_range(
1005 &mut self,
1006 range: std::ops::Range<usize>,
1007 window: &mut Window,
1008 cx: &mut App,
1009 ) -> Option<gpui::Bounds<Pixels>> {
1010 let state = self.state.read(cx);
1011 let start = utf16_to_char(&state.value, range.start);
1012 let end = utf16_to_char(&state.value, range.end);
1013 let shown = displayed_value(state, self.input_type.masks());
1014 let (line, offset, bounds) = if self.multiline {
1015 let prefix: String = shown.chars().take(start).collect();
1016 let paragraph = prefix.chars().filter(|&ch| ch == '\n').count();
1017 let offset = prefix.rsplit('\n').next()?.chars().count();
1018 (
1019 shown.split('\n').nth(paragraph)?,
1020 offset,
1021 *self.paragraphs.read(cx).get(paragraph)?,
1022 )
1023 } else {
1024 (shown.as_str(), start, self.bounds)
1025 };
1026 let size = crate::util::FIELD_TEXT;
1027 let height = px(20.);
1028 let run = gpui::TextRun {
1029 len: line.len(),
1030 font: self.font.clone(),
1031 color: gpui::black(),
1032 background_color: None,
1033 underline: None,
1034 strikethrough: None,
1035 };
1036 let shaped = window
1037 .text_system()
1038 .shape_text(
1039 line.to_owned().into(),
1040 size,
1041 &[run],
1042 self.multiline.then_some(bounds.size.width.max(px(1.))),
1043 None,
1044 )
1045 .ok()?;
1046 let shaped = shaped.first()?;
1047 let left = shaped.position_for_index(char_to_byte(line, offset), height)?;
1048 let right = shaped
1049 .position_for_index(
1050 char_to_byte(line, offset + end.saturating_sub(start)),
1051 height,
1052 )
1053 .unwrap_or(left);
1054 let y = if self.multiline {
1055 bounds.top()
1056 } else {
1057 bounds.top() + (bounds.size.height - height) / 2.
1058 };
1059 Some(gpui::Bounds::new(
1060 gpui::point(bounds.left() + left.x, y + left.y),
1061 gpui::size(
1062 if left.y == right.y {
1063 (right.x - left.x).max(px(1.))
1064 } else {
1065 px(1.)
1066 },
1067 height,
1068 ),
1069 ))
1070 }
1071
1072 fn character_index_for_point(
1073 &mut self,
1074 point: gpui::Point<Pixels>,
1075 window: &mut Window,
1076 cx: &mut App,
1077 ) -> Option<usize> {
1078 let state = self.state.read(cx);
1079 let shown = displayed_value(state, self.input_type.masks());
1080 let offset = if self.multiline {
1081 char_at_point(
1082 &shown,
1083 point,
1084 self.paragraphs.read(cx),
1085 &self.font,
1086 crate::util::FIELD_TEXT,
1087 px(20.),
1088 window,
1089 )?
1090 } else {
1091 char_at_x(
1092 &shown,
1093 point.x - self.bounds.left(),
1094 &self.font,
1095 crate::util::FIELD_TEXT,
1096 window,
1097 )
1098 };
1099 Some(char_to_utf16(&state.value, offset))
1100 }
1101}
1102
1103const NEWLINE: char = '\n';
1105
1106struct MultilineBody<'a> {
1108 value: &'a str,
1109 cursor: usize,
1110 selection: Option<(usize, usize)>,
1111 focused: bool,
1112 caret: (gpui::ElementId, Pixels, gpui::Hsla),
1114 selection_bg: gpui::Hsla,
1115 paragraphs: Entity<Vec<gpui::Bounds<Pixels>>>,
1118}
1119
1120fn multiline_body(b: MultilineBody<'_>, cx: &App) -> gpui::AnyElement {
1127 let (caret_id, caret_h, caret_color) = b.caret;
1128 let mut col = gpui::div().flex().flex_col().items_start().w_full();
1129 let mut start = 0usize;
1132 let lines: Vec<&str> = b.value.split('\n').collect();
1133 let last = lines.len().saturating_sub(1);
1134 for (i, line) in lines.iter().enumerate() {
1135 let len = line.chars().count();
1136 let end = start + len;
1137 let mut para = gpui::div()
1140 .flex()
1141 .flex_wrap()
1142 .items_center()
1143 .w_full()
1144 .min_w_0();
1145
1146 para = para.child(
1150 gpui::canvas(
1151 {
1152 let sink = b.paragraphs.clone();
1153 move |bounds: gpui::Bounds<Pixels>, _window, cx| {
1154 sink.update(cx, |slots, _| {
1155 if slots.len() <= i {
1156 slots.resize(i + 1, gpui::Bounds::default());
1157 }
1158 slots[i] = bounds;
1159 });
1160 bounds
1161 }
1162 },
1163 |_, _, _, _| {},
1164 )
1165 .w_full()
1166 .h(px(0.))
1167 .flex_shrink_0(),
1168 );
1169
1170 let local_sel = b.selection.and_then(|(lo, hi)| {
1172 let lo = lo.max(start);
1173 let hi = hi.min(end);
1174 (lo < hi).then_some((lo - start, hi - start))
1175 });
1176
1177 if let Some((lo, hi)) = local_sel {
1178 let before: String = line.chars().take(lo).collect();
1179 let selected: String = line.chars().skip(lo).take(hi - lo).collect();
1180 let after: String = line.chars().skip(hi).collect();
1181 para = para
1182 .child(gpui::div().min_w_0().child(before))
1183 .child(
1184 gpui::div()
1185 .min_w_0()
1186 .px(px(1.))
1187 .rounded(px(4.))
1188 .bg(b.selection_bg)
1189 .child(selected),
1190 )
1191 .child(gpui::div().min_w_0().child(after));
1192 } else if b.selection.is_none() && b.cursor >= start && b.cursor <= end {
1193 let at = b.cursor - start;
1194 let before: String = line.chars().take(at).collect();
1195 let after: String = line.chars().skip(at).collect();
1196 para = para.child(gpui::div().min_w_0().child(before));
1197 if b.focused {
1198 para = para.child(crate::anim::caret_blink(
1199 gpui::div()
1200 .w(px(1.5))
1201 .h(caret_h)
1202 .bg(caret_color)
1203 .flex_shrink_0(),
1204 caret_id.clone(),
1205 cx,
1206 ));
1207 }
1208 para = para.child(gpui::div().min_w_0().child(after));
1209 } else {
1210 para = para.child(gpui::div().w_full().min_w_0().child(line.to_string()));
1211 }
1212
1213 col = col.child(para);
1214 start = end + usize::from(i < last);
1216 }
1217 col.into_any_element()
1218}
1219
1220struct InputFieldRenderState {
1221 focus: crate::util::FieldFocus,
1222 is_disabled: bool,
1223 is_invalid: bool,
1224 is_read_only: bool,
1225 is_required: bool,
1226 value: SharedString,
1227}
1228
1229#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
1231#[derive(IntoElement)]
1232pub struct Input {
1233 content: Option<std::sync::Arc<dyn Fn(crate::util::FieldFocus) -> gpui::AnyElement + 'static>>,
1235 field_content:
1236 Option<std::sync::Arc<dyn Fn(InputFieldRenderState) -> gpui::AnyElement + 'static>>,
1237 validation_behavior: Option<crate::form::ValidationBehavior>,
1239 state: Entity<InputState>,
1240 label: Option<SharedString>,
1241 a11y_label: Option<SharedString>,
1243 placeholder: Option<SharedString>,
1244 description: Option<SharedString>,
1245 error_message: Option<SharedString>,
1246 validate: Option<crate::validation::Validator<str>>,
1248 validation_errors: Vec<SharedString>,
1250 variant: FieldVariant,
1251 variant_is_set: bool,
1252 input_type: InputType,
1253 max_length: Option<usize>,
1254 min_length: Option<usize>,
1255 min: Option<f64>,
1257 max: Option<f64>,
1258 step: Option<f64>,
1259 pattern: Option<std::sync::Arc<dyn Fn(&str) -> bool + 'static>>,
1261 start_content: Option<gpui::AnyElement>,
1262 end_content: Option<gpui::AnyElement>,
1263 min_h: Option<Pixels>,
1267 height: Option<Pixels>,
1270 padding_x: Option<Pixels>,
1274 group_padding_x: Option<Pixels>,
1278 is_bare: bool,
1281 is_bare_is_set: bool,
1282 focus_ring: Option<bool>,
1285 text_size: Option<Pixels>,
1287 font_family: Option<SharedString>,
1290 radius: Option<Pixels>,
1292 in_group: Option<(bool, bool)>,
1296 group_dim: bool,
1300 full_width: bool,
1301 is_disabled: bool,
1302 is_read_only: bool,
1303 is_required: bool,
1304 is_invalid: bool,
1305 auto_focus: bool,
1307 name: Option<SharedString>,
1309 multiline: bool,
1312 default_value: Option<SharedString>,
1314 value: Option<SharedString>,
1317 is_clearable: bool,
1318 clear_content: Option<gpui::AnyElement>,
1319 clear_hover_bg: Option<gpui::Hsla>,
1322 clear_on_escape: bool,
1324 on_clear: Option<ClearCallback>,
1326 on_change: Option<TextCallback>,
1327 on_submit: Option<TextCallback>,
1328 field_anchor: Option<(
1335 std::rc::Rc<std::cell::Cell<Option<gpui::Bounds<Pixels>>>>,
1336 SharedString,
1337 )>,
1338 sx: Option<Box<gpui::StyleRefinement>>,
1340 recipes: Vec<SharedString>,
1341}
1342
1343impl Input {
1344 pub fn state(&self) -> &Entity<InputState> {
1346 &self.state
1347 }
1348
1349 pub(crate) fn builder_is_disabled(&self) -> bool {
1356 self.is_disabled
1357 }
1358
1359 pub fn content(
1367 mut self,
1368 render: impl Fn(crate::util::FieldFocus) -> gpui::AnyElement + 'static,
1369 ) -> Self {
1370 self.content = Some(std::sync::Arc::new(render));
1371 self
1372 }
1373
1374 fn field_content(
1375 mut self,
1376 render: impl Fn(InputFieldRenderState) -> gpui::AnyElement + 'static,
1377 ) -> Self {
1378 self.field_content = Some(std::sync::Arc::new(render));
1379 self
1380 }
1381
1382 pub fn value(mut self, value: impl Into<SharedString>) -> Self {
1391 self.value = Some(value.into());
1392 self
1393 }
1394
1395 pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
1402 Self {
1403 content: None,
1404 field_content: None,
1405 validation_behavior: None,
1406 state: state.borrow().clone(),
1407 label: None,
1408 a11y_label: None,
1409 placeholder: None,
1410 description: None,
1411 error_message: None,
1412 validate: None,
1413 validation_errors: Vec::new(),
1414 variant: FieldVariant::Primary,
1415 variant_is_set: false,
1416 input_type: InputType::Text,
1417 max_length: None,
1418 min_length: None,
1419 min: None,
1420 max: None,
1421 step: None,
1422 pattern: None,
1423 start_content: None,
1424 end_content: None,
1425 min_h: None,
1426 height: None,
1427 padding_x: None,
1428 group_padding_x: None,
1429 is_bare: false,
1430 is_bare_is_set: false,
1431 focus_ring: None,
1432 text_size: None,
1433 font_family: None,
1434 radius: None,
1435 in_group: None,
1436 group_dim: false,
1437 full_width: false,
1438 is_disabled: false,
1439 is_read_only: false,
1440 is_required: false,
1441 is_invalid: false,
1442 auto_focus: false,
1443 name: None,
1444 default_value: None,
1445 value: None,
1446 multiline: false,
1447 is_clearable: false,
1448 clear_content: None,
1449 clear_hover_bg: None,
1450 clear_on_escape: false,
1451 on_clear: None,
1452 on_change: None,
1453 on_submit: None,
1454 field_anchor: None,
1455 sx: None,
1456 recipes: Vec::new(),
1457 }
1458 }
1459
1460 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
1466 self.validation_behavior = Some(behavior);
1467 self
1468 }
1469
1470 pub(crate) fn multiline(mut self, v: bool) -> Self {
1475 self.multiline = v;
1476 self
1477 }
1478
1479 pub(crate) fn min_h(mut self, h: Pixels) -> Self {
1481 self.min_h = Some(h);
1482 self
1483 }
1484
1485 pub(crate) fn in_group(mut self, has_prefix: bool, has_suffix: bool) -> Self {
1488 self.in_group = Some((has_prefix, has_suffix));
1489 self
1490 }
1491
1492 pub(crate) fn group_dim(mut self, v: bool) -> Self {
1495 self.group_dim = v;
1496 self
1497 }
1498
1499 pub(crate) fn field_anchor(
1504 mut self,
1505 anchor: std::rc::Rc<std::cell::Cell<Option<gpui::Bounds<Pixels>>>>,
1506 selector: impl Into<SharedString>,
1507 ) -> Self {
1508 self.field_anchor = Some((anchor, selector.into()));
1509 self
1510 }
1511
1512 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
1518 self.name = Some(name.into());
1519 self
1520 }
1521
1522 pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
1529 self.default_value = Some(text.into());
1530 self
1531 }
1532
1533 pub fn label(mut self, l: impl Into<SharedString>) -> Self {
1535 self.label = Some(l.into());
1536 self
1537 }
1538
1539 pub(crate) fn a11y_label(mut self, l: impl Into<SharedString>) -> Self {
1548 self.a11y_label = Some(l.into());
1549 self
1550 }
1551
1552 pub fn placeholder(mut self, p: impl Into<SharedString>) -> Self {
1554 self.placeholder = Some(p.into());
1555 self
1556 }
1557
1558 pub fn description(mut self, d: impl Into<SharedString>) -> Self {
1560 self.description = Some(d.into());
1561 self
1562 }
1563
1564 pub fn auto_focus(mut self, v: bool) -> Self {
1566 self.auto_focus = v;
1567 self
1568 }
1569
1570 pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
1575 self.validate = Some(std::sync::Arc::new(f));
1576 self
1577 }
1578
1579 pub fn validation_errors(
1582 mut self,
1583 errors: impl IntoIterator<Item = impl Into<SharedString>>,
1584 ) -> Self {
1585 self.validation_errors = errors.into_iter().map(Into::into).collect();
1586 self
1587 }
1588
1589 pub fn error_message(mut self, e: impl Into<SharedString>) -> Self {
1591 self.error_message = Some(e.into());
1592 self
1593 }
1594
1595 pub fn input_type(mut self, input_type: InputType) -> Self {
1597 self.input_type = input_type;
1598 self
1599 }
1600
1601 pub fn max_length(mut self, n: usize) -> Self {
1603 self.max_length = Some(n);
1604 self
1605 }
1606
1607 pub fn min_length(mut self, n: usize) -> Self {
1609 self.min_length = Some(n);
1610 self
1611 }
1612
1613 pub fn min(mut self, v: f64) -> Self {
1615 self.min = Some(v);
1616 self
1617 }
1618
1619 pub fn max(mut self, v: f64) -> Self {
1621 self.max = Some(v);
1622 self
1623 }
1624
1625 pub fn step(mut self, v: f64) -> Self {
1627 self.step = Some(v);
1628 self
1629 }
1630
1631 pub fn pattern(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
1633 self.pattern = Some(std::sync::Arc::new(f));
1634 self
1635 }
1636
1637 pub fn validity(&self, value: &str) -> InputValidity {
1642 validate_value(
1643 value,
1644 self.input_type,
1645 self.min_length,
1646 self.min,
1647 self.max,
1648 self.step,
1649 self.pattern.as_deref(),
1650 )
1651 }
1652
1653 pub fn variant(mut self, v: FieldVariant) -> Self {
1655 self.variant = v;
1656 self.variant_is_set = true;
1657 self
1658 }
1659
1660 pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
1663 self.recipes.push(name.into());
1664 self
1665 }
1666
1667 pub(crate) fn with_recipes(mut self, recipes: Vec<SharedString>) -> Self {
1668 self.recipes = recipes;
1669 self
1670 }
1671
1672 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
1682 self.height = Some(h.into());
1683 self
1684 }
1685
1686 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
1693 self.padding_x = Some(p.into());
1694 self
1695 }
1696
1697 pub(crate) fn group_padding_x(mut self, padding_x: impl Into<Pixels>) -> Self {
1701 self.group_padding_x = Some(padding_x.into());
1702 self
1703 }
1704
1705 pub(crate) fn with_sx_refinement(mut self, sx: Option<Box<gpui::StyleRefinement>>) -> Self {
1712 if sx.is_some() {
1713 self.sx = sx;
1714 }
1715 self
1716 }
1717
1718 pub(crate) fn with_field_box(mut self, field: crate::util::FieldBox) -> Self {
1719 if let Some(height) = field.height {
1720 self = self.height(height);
1721 }
1722 if let Some(padding_x) = field.padding_x {
1723 self = self.padding_x(padding_x);
1724 }
1725 if field.is_bare_is_set {
1726 self = self.is_bare(field.is_bare);
1727 }
1728 if let Some(focus_ring) = field.focus_ring {
1729 self = self.focus_ring(focus_ring);
1730 }
1731 self
1732 }
1733
1734 pub fn is_bare(mut self, v: bool) -> Self {
1743 self.is_bare = v;
1744 self.is_bare_is_set = true;
1745 self
1746 }
1747
1748 pub fn focus_ring(mut self, v: bool) -> Self {
1756 self.focus_ring = Some(v);
1757 self
1758 }
1759
1760 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
1762 self.text_size = Some(size.into());
1763 self
1764 }
1765
1766 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
1774 self.font_family = Some(family.into());
1775 self
1776 }
1777
1778 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
1785 self.radius = Some(radius.into());
1786 self
1787 }
1788
1789 pub fn start_content(mut self, el: impl IntoElement) -> Self {
1791 self.start_content = Some(el.into_any_element());
1792 self
1793 }
1794
1795 pub fn end_content(mut self, el: impl IntoElement) -> Self {
1797 self.end_content = Some(el.into_any_element());
1798 self
1799 }
1800
1801 pub fn full_width(mut self) -> Self {
1803 self.full_width = true;
1804 self
1805 }
1806
1807 pub fn is_disabled(mut self, v: bool) -> Self {
1809 self.is_disabled = v;
1810 self
1811 }
1812
1813 pub fn is_read_only(mut self, v: bool) -> Self {
1815 self.is_read_only = v;
1816 self
1817 }
1818
1819 pub fn is_required(mut self, v: bool) -> Self {
1821 self.is_required = v;
1822 self
1823 }
1824
1825 pub fn is_invalid(mut self, v: bool) -> Self {
1827 self.is_invalid = v;
1828 self
1829 }
1830
1831 pub fn clear_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
1834 self.clear_hover_bg = Some(color.into());
1835 self
1836 }
1837
1838 pub fn is_clearable(mut self, v: bool) -> Self {
1840 self.is_clearable = v;
1841 self
1842 }
1843
1844 fn clear_content(mut self, content: impl IntoElement) -> Self {
1845 self.clear_content = Some(content.into_any_element());
1846 self
1847 }
1848
1849 pub(crate) fn clear_on_escape(mut self, v: bool) -> Self {
1852 self.clear_on_escape = v;
1853 self
1854 }
1855
1856 pub(crate) fn on_clear(mut self, f: ClearCallback) -> Self {
1859 self.on_clear = Some(f);
1860 self
1861 }
1862
1863 pub fn on_change(mut self, f: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
1865 self.on_change = Some(std::sync::Arc::new(f));
1866 self
1867 }
1868
1869 pub fn on_submit(mut self, f: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
1871 self.on_submit = Some(std::sync::Arc::new(f));
1872 self
1873 }
1874
1875 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
1882 crate::util::refine_sx(&mut self.sx, style);
1883 self
1884 }
1885}
1886
1887impl Input {
1888 pub(crate) fn state_focus(&self, cx: &App) -> FocusHandle {
1893 self.state.read(cx).focus_handle.clone()
1894 }
1895
1896 pub(crate) fn sx_refinement(mut self, sx: Option<Box<gpui::StyleRefinement>>) -> Self {
1900 self.sx = sx;
1901 self
1902 }
1903}
1904
1905impl RenderOnce for Input {
1906 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1907 let base_id =
1913 gpui::ElementId::named_usize("input", self.state.entity_id().as_u64() as usize);
1914 if let Some(behavior) = self.validation_behavior {
1916 let state = self.state.clone();
1917 if state.read(cx).validation_behavior() != behavior {
1918 state.update(cx, |s, _| s.set_validation_behavior(behavior));
1919 }
1920 }
1921 let seed = self.value.clone().or(self.default_value.clone());
1928 if let Some(text) = seed {
1929 let state = self.state.clone();
1930 crate::util::seed_once(
1931 window,
1932 cx,
1933 element_id::scoped(&base_id, "default"),
1934 move |cx| {
1935 state.update(cx, |s, cx| {
1936 s.set_value(text.to_string());
1937 cx.notify();
1938 });
1939 },
1940 );
1941 }
1942 if self.state.read(cx).name() != self.name {
1945 let name = self.name.clone();
1946 self.state.update(cx, |s, _| s.set_name(name));
1947 }
1948 let is_successful = !self.is_disabled;
1949 if self.state.read(cx).is_successful() != is_successful {
1950 self.state
1951 .update(cx, |s, _| s.set_successful(is_successful));
1952 }
1953 if self.state.read(cx).is_read_only() != self.is_read_only {
1957 let read_only = self.is_read_only;
1958 self.state.update(cx, |s, _| s.set_read_only(read_only));
1959 }
1960 let value_now = self.state.read(cx).value().to_owned();
1967 let mut server_errors = self.state.read(cx).routed_errors().to_vec();
1968 server_errors.extend(self.validation_errors.iter().cloned());
1969 let mut validity = crate::validation::resolve(
1970 self.is_invalid,
1971 &server_errors,
1972 self.validate.as_ref().and_then(|f| f(&value_now)),
1973 self.error_message.clone(),
1974 );
1975 if !self.validity(&value_now).is_valid() {
1981 validity.is_invalid = true;
1982 }
1983 let validity_state = self.state.clone();
1987 if validity_state.read(cx).validity() != &validity {
1988 validity_state.update(cx, |s, _| s.set_validity(validity.clone()));
1989 }
1990 let focus_handle = self.state.read(cx).focus_handle.clone();
1991 if self.auto_focus {
1992 crate::util::focus_once(
1993 window,
1994 cx,
1995 element_id::scoped(&base_id, "autofocus"),
1996 &focus_handle,
1997 );
1998 }
1999 let text_origin =
2004 window.use_keyed_state(element_id::scoped(&base_id, "text-origin"), cx, |_, _| {
2005 None::<Pixels>
2006 });
2007 let paragraph_bounds =
2009 window.use_keyed_state(element_id::scoped(&base_id, "paragraphs"), cx, |_, _| {
2010 Vec::<gpui::Bounds<Pixels>>::new()
2011 });
2012 let mut text_font = window.text_style().font();
2021 if let Some(family) = &self.font_family {
2022 text_font.family = family.clone();
2023 }
2024 let disabled_opacity = cx.layout().disabled_opacity;
2025 let focused = focus_handle.is_focused(window);
2026 if !focused && self.state.read(cx).marked.is_some() {
2027 self.state.update(cx, |state, _| state.marked = None);
2028 }
2029 let colors = cx.colors().clone();
2033 let field_theme = cx.theme().components.text_field.resolve(&self.recipes);
2034 if !self.variant_is_set {
2035 if let Some(variant) = field_theme.variant {
2036 self.variant = variant;
2037 }
2038 }
2039 if !self.is_bare_is_set {
2040 if let Some(is_bare) = field_theme.is_bare {
2041 self.is_bare = is_bare;
2042 }
2043 }
2044 if self.focus_ring.is_none() {
2045 self.focus_ring = field_theme.focus_ring;
2046 }
2047 self.height = self.height.or(field_theme.height);
2048 self.padding_x = self.padding_x.or(field_theme.padding_x);
2049 self.radius = self.radius.or(field_theme.radius);
2050 let theme_background = field_theme.background.map(|color| color.resolve(&colors));
2051 let theme_foreground = field_theme.foreground.map(|color| color.resolve(&colors));
2052 let theme_placeholder = field_theme
2053 .placeholder
2054 .map_or(colors.muted, |color| color.resolve(&colors));
2055 let accent = colors.accent;
2056 let field_focus = crate::util::FieldFocus {
2057 is_focused: focused,
2058 is_focus_within: focus_handle.contains_focused(window, cx),
2059 is_focus_visible: focused && crate::util::focus_visible(cx),
2060 };
2061
2062 if let Some(render) = self.field_content.clone() {
2065 return render(InputFieldRenderState {
2066 focus: field_focus,
2067 is_disabled: self.is_disabled,
2068 is_invalid: validity.is_invalid,
2069 is_read_only: self.is_read_only,
2070 is_required: self.is_required,
2071 value: value_now.into(),
2072 });
2073 }
2074 if let Some(render) = self.content.clone() {
2075 return render(field_focus);
2076 }
2077
2078 let (h, text) = (crate::util::FIELD_HEIGHT, crate::util::FIELD_TEXT);
2086 let h = self.height.unwrap_or(h);
2087 let text = self.text_size.or(field_theme.text_size).unwrap_or(text);
2088
2089 let is_invalid = validity.is_invalid;
2090 let show_focus_ring = self.focus_ring.unwrap_or(true);
2091 let _border_color = if is_invalid {
2092 colors.danger.color
2093 } else if focused {
2094 colors.focus
2095 } else {
2096 colors.border
2097 };
2098 let multiline = self.multiline;
2099 let multiline_h = self.min_h.unwrap_or(px(38.));
2103 let a11y_role = if multiline {
2109 crate::a11y::Role::MultilineTextInput
2110 } else {
2111 self.input_type.a11y_role()
2112 };
2113 let a11y_name = crate::a11y::Name::field(
2114 self.label.as_ref().or(self.a11y_label.as_ref()),
2115 self.description.as_ref(),
2116 &validity,
2117 );
2118 let radius = self.radius.unwrap_or_else(|| crate::util::field_radius(cx));
2120 let mut field = gpui::div()
2121 .id(base_id.clone())
2122 .a11y_named(a11y_role, &a11y_name)
2123 .a11y_text(&value_now, self.placeholder.as_ref())
2124 .flex()
2125 .map(|f| {
2128 if multiline {
2129 f.items_start()
2130 .min_h(multiline_h)
2131 .py(px(8.))
2132 .w_full()
2133 .overflow_hidden()
2134 } else {
2135 f.items_center().h(h)
2136 }
2137 })
2138 .gap(px(8.))
2139 .map(|f| {
2142 let padding_x = self.group_padding_x.unwrap_or(px(12.));
2146 match self.in_group {
2147 None => f.px(px(12.)),
2148 Some((prefix, suffix)) => f
2149 .flex_1()
2150 .pl(if prefix { px(0.) } else { padding_x })
2151 .pr(if suffix { px(0.) } else { padding_x }),
2152 }
2153 })
2154 .when_some(
2157 self.padding_x.filter(|_| self.in_group.is_none()),
2158 |f, padding_x| f.px(padding_x),
2159 )
2160 .text_size(text)
2161 .line_height(px(20.))
2162 .when_some(self.font_family.clone(), |f, family| f.font_family(family))
2163 .when(self.in_group.is_none(), |f| f.rounded(radius))
2169 .when(!self.is_disabled, |e| {
2170 e.cursor(gpui::CursorStyle::IBeam)
2171 .track_focus(&focus_handle)
2172 .key_context("Input")
2173 .on_mouse_down(gpui::MouseButton::Left, {
2179 let fh = focus_handle.clone();
2180 let st = self.state.clone();
2181 let origin = text_origin.clone();
2182 let font = text_font.clone();
2183 let masks = self.input_type.masks();
2184 let multiline = self.multiline;
2185 let paras = paragraph_bounds.clone();
2186 let line_h = px(20.);
2188 let size = text;
2189 move |ev: &gpui::MouseDownEvent, window, cx| {
2190 window.focus(&fh, cx);
2191 let shown = displayed_value(st.read(cx), masks);
2192 let at = if multiline {
2193 let boxes = paras.read(cx).clone();
2194 let Some(at) = char_at_point(
2195 &shown,
2196 ev.position,
2197 &boxes,
2198 &font,
2199 size,
2200 line_h,
2201 window,
2202 ) else {
2203 return;
2204 };
2205 at
2206 } else {
2207 let Some(left) = *origin.read(cx) else {
2208 return;
2209 };
2210 char_at_x(&shown, ev.position.x - left, &font, size, window)
2211 };
2212 st.update(cx, |s, cx| {
2213 s.marked = None;
2214 s.cursor = at;
2215 s.anchor = None;
2216 match ev.click_count {
2220 2 => {
2221 s.anchor = Some(word_target(&s.value, at, false));
2222 s.cursor = word_target(&s.value, at, true);
2223 }
2224 n if n >= 3 => select_all(s),
2225 _ => {}
2226 }
2227 cx.notify();
2228 });
2229 }
2230 })
2231 .on_mouse_move({
2232 let st = self.state.clone();
2233 let origin = text_origin.clone();
2234 let font = text_font.clone();
2235 let masks = self.input_type.masks();
2236 let multiline = self.multiline;
2237 let paras = paragraph_bounds.clone();
2238 let line_h = text * 1.3;
2241 let size = text;
2242 move |ev: &gpui::MouseMoveEvent, window, cx| {
2243 if ev.pressed_button != Some(gpui::MouseButton::Left) {
2244 return;
2245 }
2246 let shown = displayed_value(st.read(cx), masks);
2247 let at = if multiline {
2248 let boxes = paras.read(cx).clone();
2249 let Some(at) = char_at_point(
2250 &shown,
2251 ev.position,
2252 &boxes,
2253 &font,
2254 size,
2255 line_h,
2256 window,
2257 ) else {
2258 return;
2259 };
2260 at
2261 } else {
2262 let Some(left) = *origin.read(cx) else {
2263 return;
2264 };
2265 char_at_x(&shown, ev.position.x - left, &font, size, window)
2266 };
2267 st.update(cx, |s, cx| {
2268 if s.anchor.is_none() {
2269 s.anchor = Some(s.cursor);
2270 }
2271 s.marked = None;
2272 s.cursor = at;
2273 cx.notify();
2274 });
2275 }
2276 })
2277 })
2278 .when(self.is_disabled && !self.group_dim, |e| e.opacity(disabled_opacity));
2283
2284 let mut carrier_ring = None;
2287
2288 if self.in_group.is_none() && !self.is_bare {
2294 let ring = crate::util::field_ring_color(is_invalid, focused, show_focus_ring, cx);
2300 field = crate::util::apply_field_chrome_ringless(
2301 field,
2302 self.variant,
2303 is_invalid,
2304 focused,
2305 show_focus_ring,
2306 Some(radius),
2307 cx,
2308 );
2309 if multiline {
2310 carrier_ring = Some(ring);
2311 } else {
2312 field = crate::util::with_field_ring_overlay(field, ring, radius, cx);
2313 }
2314
2315 if !self.is_disabled && !is_invalid && !focused && theme_background.is_none() {
2316 let idle_bg = match self.variant {
2317 FieldVariant::Primary => colors.field.background,
2318 FieldVariant::Secondary => colors.default.color,
2319 };
2320 let hover_bg = match self.variant {
2321 FieldVariant::Primary => colors.field.hover(),
2322 FieldVariant::Secondary => colors.default.hover(),
2324 };
2325 let hover_border = colors.field.border_hover();
2326 field = crate::anim::hover_fade_with_duration_and_easing(
2331 field,
2332 element_id::scoped(&base_id, "hover-fade"),
2333 (idle_bg, hover_bg),
2334 None,
2335 Some(hover_border),
2336 |fill| fill.rounded(radius),
2337 Some(150),
2338 crate::anim::HoverFadeEasing::EaseSmooth,
2339 window,
2340 cx,
2341 );
2342 }
2343 }
2344 if let Some(background) = theme_background {
2345 field = field.bg(background);
2346 }
2347 if let Some(foreground) = theme_foreground {
2348 field = field.text_color(foreground);
2349 }
2350
2351 let st = self.state.read(cx);
2353 let value = if self.input_type.masks() {
2356 "\u{2022}".repeat(st.value.chars().count())
2357 } else {
2358 st.value.clone()
2359 };
2360 let cursor = st.cursor;
2361 let is_empty = value.is_empty();
2362 let selection = st.selection();
2363
2364 let mut row = if self.multiline {
2367 gpui::div()
2368 .flex()
2369 .flex_col()
2370 .items_start()
2371 .w_full()
2372 .min_w_0()
2373 .flex_1()
2374 } else {
2375 let origin = text_origin;
2376 gpui::div()
2377 .flex()
2378 .items_center()
2379 .min_w_0()
2380 .flex_1()
2381 .child(
2385 gpui::canvas(
2386 move |bounds, _window, cx| {
2387 let left = bounds.origin.x;
2388 if *origin.read(cx) != Some(left) {
2389 origin.update(cx, |v, _| *v = Some(left));
2390 }
2391 },
2392 |_, _, _, _| {},
2393 )
2394 .w(px(0.))
2395 .h(px(0.))
2396 .flex_shrink_0(),
2397 )
2398 };
2399
2400 if self.multiline && !(is_empty && !focused && self.placeholder.is_some()) {
2401 let caret_id = element_id::scoped(&base_id, "caret");
2404 row = row.child(multiline_body(
2405 MultilineBody {
2406 paragraphs: paragraph_bounds.clone(),
2407 value: &value,
2408 cursor,
2409 selection,
2410 focused,
2411 caret: (caret_id, text * 1.3, accent.color),
2412 selection_bg: accent.with_alpha(0.24),
2413 },
2414 cx,
2415 ));
2416 } else if is_empty && !focused && self.placeholder.is_some() {
2417 row = row.child(
2418 gpui::div()
2419 .text_color(theme_placeholder)
2420 .truncate()
2421 .child(self.placeholder.clone().unwrap().to_string()),
2422 );
2423 } else if let Some((lo, hi)) = selection {
2424 let before: String = value.chars().take(lo).collect();
2426 let selected: String = value.chars().skip(lo).take(hi - lo).collect();
2427 let after: String = value.chars().skip(hi).collect();
2428 let sel_bg = accent.with_alpha(0.24);
2429 row = row.child(
2430 gpui::div()
2431 .flex()
2432 .items_center()
2433 .whitespace_nowrap()
2434 .overflow_hidden()
2435 .child(before)
2436 .child(
2437 gpui::div()
2438 .px(px(1.))
2439 .py(px(1.))
2440 .rounded(px(4.))
2441 .bg(sel_bg)
2442 .child(selected),
2443 )
2444 .child(after),
2445 );
2446 } else {
2447 let before: String = value.chars().take(cursor).collect();
2448 let after: String = value.chars().skip(cursor).collect();
2449 row = row.child(
2450 gpui::div()
2451 .flex()
2452 .items_center()
2453 .whitespace_nowrap()
2454 .overflow_hidden()
2455 .child(before)
2456 .when(focused, |r| {
2457 r.child(crate::anim::caret_blink(
2459 gpui::div()
2460 .w(px(1.5))
2461 .h(text * 1.3)
2462 .bg(accent.color)
2463 .flex_shrink_0(),
2464 element_id::scoped(&base_id, "caret"),
2465 cx,
2466 ))
2467 })
2468 .child(after),
2469 );
2470 }
2471
2472 let edit_is_invalid = self.is_invalid;
2477 let edit_validation_errors = self.validation_errors.clone();
2478 let edit_validate = self.validate.clone();
2479 let edit_error_message = self.error_message.clone();
2480 let native_validity = {
2481 let input_type = self.input_type;
2482 let min_length = self.min_length;
2483 let min = self.min;
2484 let max = self.max;
2485 let step = self.step;
2486 let pattern = self.pattern.clone();
2487 move |value: &str| {
2488 validate_value(
2489 value,
2490 input_type,
2491 min_length,
2492 min,
2493 max,
2494 step,
2495 pattern.as_deref(),
2496 )
2497 .is_valid()
2498 }
2499 };
2500
2501 let edit_state = self.state.clone();
2502 let on_change = self.on_change.clone();
2503 let platform_native_validity = native_validity.clone();
2504 let platform_error_message = edit_error_message.clone();
2505 let platform_validate = edit_validate.clone();
2506 let platform_validation_errors = edit_validation_errors.clone();
2507 let edit_callback: TextCallback = crate::util::shared(move |value, window, cx| {
2508 edit_state.update(cx, |s, _| s.clear_routed_errors());
2509 refresh_stored_validity(
2510 &edit_state,
2511 edit_is_invalid,
2512 &platform_validation_errors,
2513 platform_validate.as_ref(),
2514 platform_error_message.clone(),
2515 &platform_native_validity,
2516 cx,
2517 );
2518 if let Some(callback) = &on_change {
2519 callback(value, window, cx);
2520 }
2521 });
2522 let platform_state = self.state.clone();
2523 let platform_edit = edit_callback.clone();
2524 let platform_focus = focus_handle.clone();
2525 let platform_font = text_font;
2526 let platform_paragraphs = paragraph_bounds;
2527 let platform_type = self.input_type;
2528 let platform_max = self.max_length;
2529 let platform_multiline = self.multiline;
2530 let platform_disabled = self.is_disabled;
2531 row = row.relative().child(
2532 gpui::canvas(
2533 |_, _, _| {},
2534 move |bounds, _, window, cx| {
2535 if !platform_disabled {
2536 window.handle_input(
2537 &platform_focus,
2538 PlatformTextInput {
2539 state: platform_state.clone(),
2540 on_edit: platform_edit.clone(),
2541 input_type: platform_type,
2542 max_length: platform_max,
2543 multiline: platform_multiline,
2544 bounds,
2545 font: platform_font.clone(),
2546 paragraphs: platform_paragraphs.clone(),
2547 },
2548 cx,
2549 );
2550 }
2551 },
2552 )
2553 .absolute()
2554 .size_full(),
2555 );
2556
2557 field = field.children(self.start_content);
2558 field = field.child(row);
2559 if self.is_clearable && !is_empty && !self.is_disabled && !self.is_read_only {
2561 let clear_focus_handle = self.state.read(cx).clear_focus_handle.clone();
2562 let clear_content = match self.clear_content {
2563 Some(content) => content,
2564 None => gpui::svg()
2565 .size(px(12.))
2566 .path(crate::icons::CLOSE)
2567 .text_color(colors.muted)
2568 .into_any_element(),
2569 };
2570 let clear_state = self.state.clone();
2571 let clear_on_change = self.on_change.clone();
2572 let on_clear = self.on_clear.clone();
2573 let clear_validation_errors = edit_validation_errors;
2574 let clear_validate = edit_validate.clone();
2575 let clear_error_message = edit_error_message;
2576 let clear_native = native_validity.clone();
2577 let clear_hover_bg = self.clear_hover_bg.unwrap_or(colors.default.hover());
2581 let clear_box = px(20.);
2582 let clear_radius = crate::util::small_radius(cx);
2583 let clear_selector = format!("input-clear-{}", self.state.entity_id().as_u64());
2584 let input_focus_after_clear = focus_handle.clone();
2585 let input_focus_handle = focus_handle.clone();
2586 let mut clear = gpui::div()
2587 .id(element_id::scoped(&base_id, "clear"))
2588 .debug_selector(move || clear_selector)
2589 .flex()
2590 .items_center()
2591 .justify_center()
2592 .size(clear_box)
2593 .p(px(4.))
2594 .rounded(clear_radius)
2595 .cursor(crate::util::interactive_cursor(cx))
2596 .text_color(colors.muted)
2597 .hover(move |s| s.bg(clear_hover_bg))
2598 .active({
2599 const PRESS_SCALE: f32 = 0.93;
2603 let inset = px(f32::from(clear_box) * (1.0 - PRESS_SCALE) / 2.0);
2604 let pressed = px(f32::from(clear_box) * PRESS_SCALE);
2605 let radius = px(f32::from(clear_radius) * PRESS_SCALE);
2606 move |s| {
2607 s.h(pressed)
2608 .w(pressed)
2609 .mt(inset)
2610 .mb(inset)
2611 .ml(inset)
2612 .mr(inset)
2613 .rounded(radius)
2614 }
2615 })
2616 .track_focus(&clear_focus_handle)
2622 .on_key_down(|event, _, cx| {
2623 if event.keystroke.key != "tab" {
2624 cx.stop_propagation();
2625 }
2626 })
2627 .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
2628 cx.stop_propagation();
2629 window.focus(&input_focus_handle, cx);
2630 window.prevent_default();
2631 })
2632 .on_click(move |_, window, cx| {
2633 window.focus(&input_focus_after_clear, cx);
2634 clear_state.update(cx, |s, cx| {
2635 s.value.clear();
2636 s.marked = None;
2637 s.cursor = 0;
2638 s.anchor = None;
2639 s.clear_routed_errors();
2642 cx.notify();
2643 });
2644 refresh_stored_validity(
2649 &clear_state,
2650 edit_is_invalid,
2651 &clear_validation_errors,
2652 clear_validate.as_ref(),
2653 clear_error_message.clone(),
2654 &clear_native,
2655 cx,
2656 );
2657 if let Some(cb) = &clear_on_change {
2658 cb("", window, cx);
2659 }
2660 if let Some(cb) = &on_clear {
2661 cb(window, cx);
2662 }
2663 })
2664 .child(clear_content);
2665 clear = crate::util::ring_overlay_if_focused(
2670 clear,
2671 &clear_focus_handle,
2672 true,
2673 clear_radius,
2674 Vec::new(),
2675 window,
2676 cx,
2677 );
2678 field = field.child(clear);
2679 }
2680 field = field.children(self.end_content);
2681
2682 let state_entity = self.state.clone();
2684 let on_submit = self.on_submit.clone();
2685 let on_clear = self.on_clear.clone();
2686 let clear_on_escape = self.clear_on_escape;
2687 let is_read_only = self.is_read_only;
2688 let is_disabled = self.is_disabled;
2689 field = field.on_key_down(move |ev: &KeyDownEvent, window, cx| {
2690 if is_disabled {
2691 return;
2692 }
2693 let key: &str = &ev.keystroke.key;
2694 if crate::util::key_enables_focus_visible(key) {
2695 crate::util::set_focus_visible(true, cx);
2696 }
2697 let mods = ev.keystroke.modifiers;
2698 if state_entity.read(cx).marked.is_some() && !matches!(key, "escape" | "tab") {
2699 cx.stop_propagation();
2700 return;
2701 }
2702 let input_type = self.input_type;
2703 let max_length = self.max_length;
2704 let multiline = self.multiline;
2705 let owns_key = (!mods.control
2706 && !mods.alt
2707 && !mods.platform
2708 && (matches!(key, "backspace" | "delete" | "left" | "right")
2709 || multiline && matches!(key, "up" | "down" | "home" | "end" | "enter")))
2710 || (mods.control || mods.platform)
2711 && !mods.alt
2712 && matches!(key, "a" | "left" | "right");
2713 if owns_key {
2714 cx.stop_propagation();
2715 }
2716 if owns_key || key == "escape" {
2717 state_entity.update(cx, |state, _| state.marked = None);
2718 }
2719 let mut changed = false;
2720 let mut cleared = false;
2721 let mut submit = false;
2722
2723 if key == "a" && (mods.control || mods.platform) {
2724 if !mods.alt {
2726 state_entity.update(cx, |s, cx| {
2727 select_all(s);
2728 cx.notify();
2729 });
2730 }
2731 } else if mods.control || mods.alt || mods.platform {
2732 let cmd = mods.control || mods.platform;
2733 if !is_read_only && cmd && key == "v" {
2734 if let Some(text) = cx.read_from_clipboard().and_then(|c| c.text()) {
2735 changed = state_entity.update(cx, |s, cx| {
2736 let mut inserted = false;
2737 for ch in text.chars() {
2738 if state_accepts(s, ch, input_type, max_length) {
2739 insert_char(s, ch);
2740 inserted = true;
2741 }
2742 }
2743 if inserted {
2744 cx.notify();
2745 }
2746 inserted
2747 });
2748 }
2749 } else if cmd && (key == "c" || key == "x") {
2750 if input_type.masks() {
2756 return;
2757 }
2758 let selected = {
2759 let st = state_entity.read(cx);
2760 slice_selection(&st.value, st.selection())
2761 };
2762 if let Some(text) = selected {
2763 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
2764 if key == "x" && !is_read_only {
2765 state_entity.update(cx, |s, cx| {
2766 delete_selection(s);
2767 cx.notify();
2768 });
2769 changed = true;
2770 }
2771 }
2772 } else if cmd && (key == "left" || key == "right") {
2773 state_entity.update(cx, |s, cx| {
2777 move_word(s, key == "right", mods.shift);
2778 cx.notify();
2779 });
2780 }
2781 } else {
2782 let shift = mods.shift;
2783 match key {
2784 "backspace" => {
2785 if is_read_only {
2786 return;
2787 }
2788 changed = state_entity.update(cx, |s, cx| {
2789 let deleted = backspace(s);
2790 if deleted {
2791 cx.notify();
2792 }
2793 deleted
2794 });
2795 }
2796 "delete" => {
2797 if is_read_only {
2798 return;
2799 }
2800 changed = state_entity.update(cx, |s, cx| {
2801 let deleted = delete(s);
2802 if deleted {
2803 cx.notify();
2804 }
2805 deleted
2806 });
2807 }
2808 "left" => state_entity.update(cx, |s, cx| {
2809 move_left(s, shift);
2810 cx.notify();
2811 }),
2812 "right" => state_entity.update(cx, |s, cx| {
2813 move_right(s, shift);
2814 cx.notify();
2815 }),
2816 "home" => state_entity.update(cx, |s, cx| {
2820 if multiline {
2821 before_move(s, shift);
2822 s.cursor = line_bounds(&s.value, s.cursor).0;
2823 } else {
2824 move_home(s, shift);
2825 }
2826 cx.notify();
2827 }),
2828 "end" => state_entity.update(cx, |s, cx| {
2829 if multiline {
2830 before_move(s, shift);
2831 s.cursor = line_bounds(&s.value, s.cursor).1;
2832 } else {
2833 move_end(s, shift);
2834 }
2835 cx.notify();
2836 }),
2837 "up" | "down" if multiline => state_entity.update(cx, |s, cx| {
2841 move_vertical(s, key == "down", shift);
2842 cx.notify();
2843 }),
2844 "enter" if multiline => {
2847 cx.stop_propagation();
2848 if is_read_only {
2849 return;
2850 }
2851 changed = state_entity.update(cx, |s, cx| {
2852 let inserted = state_accepts(s, NEWLINE, input_type, max_length);
2853 if inserted {
2854 insert_char(s, NEWLINE);
2855 cx.notify();
2856 }
2857 inserted
2858 });
2859 }
2860 "enter" => submit = true,
2861 "escape" => {
2862 let had_value = !state_entity.read(cx).is_empty();
2863 state_entity.update(cx, |s, cx| {
2864 s.anchor = None;
2865 if clear_on_escape && !is_read_only {
2866 s.value.clear();
2867 s.cursor = 0;
2868 }
2869 cx.notify();
2870 });
2871 if clear_on_escape && !is_read_only && had_value {
2872 changed = true;
2873 cleared = true;
2874 }
2875 }
2876 _ => {}
2877 }
2878 }
2879
2880 if changed {
2881 if mods.control || mods.platform {
2882 cx.stop_propagation();
2883 }
2884 state_entity.update(cx, |state, _| state.marked = None);
2885 let value = state_entity.read(cx).value().to_owned();
2886 edit_callback(&value, window, cx);
2887 }
2888 if cleared {
2889 if let Some(cb) = &on_clear {
2890 cb(window, cx);
2891 }
2892 }
2893 if submit {
2894 if let Some(cb) = &on_submit {
2895 {
2896 let v = state_entity.read(cx).value().to_owned();
2897 cb(&v, window, cx);
2898 }
2899 cx.stop_propagation();
2907 }
2908 }
2909 });
2910
2911 let field = if self.in_group.is_some() {
2916 crate::util::apply_sx(field, &self.sx)
2917 } else {
2918 field
2919 };
2920 let field = if self.is_disabled {
2921 field
2922 } else {
2923 crate::util::record_focus_bounds(field, &focus_handle, window, cx)
2924 };
2925
2926 let field_element: gpui::AnyElement = if let Some((anchor, selector)) = self.field_anchor {
2932 let selector_text = selector.to_string();
2933 crate::popover::PopoverTriggerMeasure::new(
2934 field.debug_selector(move || selector_text),
2935 anchor,
2936 )
2937 .into_any_element()
2938 } else {
2939 field.into_any_element()
2940 };
2941
2942 let field_element: gpui::AnyElement = match carrier_ring {
2946 Some(ring) => {
2947 crate::util::field_ring_carrier(field_element, ring, radius, cx).into_any_element()
2948 }
2949 None => field_element,
2950 };
2951
2952 if self.in_group.is_some() {
2957 return field_element;
2958 }
2959
2960 let mut el = gpui::div().flex().flex_col().gap(px(4.));
2962 if self.full_width {
2963 el = el.w_full();
2964 } else {
2965 el = el.max_w(px(320.));
2966 }
2967 if let Some(label) = self.label {
2968 let mut label_row = gpui::div()
2969 .flex()
2970 .items_center()
2971 .gap(px(4.))
2972 .text_size(px(14.))
2973 .line_height(px(20.))
2974 .font_weight(gpui::FontWeight::MEDIUM)
2975 .text_color(colors.foreground)
2976 .child(label.to_string());
2977 if self.is_required {
2978 label_row = label_row.child(
2979 gpui::div()
2980 .text_color(colors.danger.color)
2981 .child("*".to_owned()),
2982 );
2983 }
2984 el = el.child(label_row);
2985 }
2986 el = el.child(field_element);
2987 let error = (!validity.messages.is_empty()).then(|| validity.joined().into());
2992 if let Some(error) = crate::anim::field_error_panel(&base_id, error, window, cx) {
2993 el = el.child(error);
2994 } else if let Some(desc) = self.description {
2995 el = el.child(
2996 gpui::div()
2997 .text_size(px(12.))
2998 .line_height(px(16.))
2999 .text_color(colors.muted)
3000 .child(desc.to_string()),
3001 );
3002 }
3003
3004 el = crate::util::apply_sx(el, &self.sx);
3005 el.into_any_element()
3006 }
3007}
3008
3009#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3015#[non_exhaustive]
3016pub struct TextFieldRenderState {
3017 pub is_disabled: bool,
3019 pub is_invalid: bool,
3021 pub is_read_only: bool,
3023 pub is_required: bool,
3025 pub is_focused: bool,
3027 pub is_focus_within: bool,
3029 pub is_focus_visible: bool,
3031}
3032
3033#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
3039#[derive(IntoElement)]
3040pub struct TextField {
3041 inner: Input,
3042}
3043
3044impl TextField {
3045 pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
3048 Self {
3049 inner: Input::new(state),
3050 }
3051 }
3052
3053 pub fn input_type(mut self, input_type: InputType) -> Self {
3056 self.inner = self.inner.input_type(input_type);
3057 self
3058 }
3059
3060 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
3062 self.inner = self.inner.label(text);
3063 self
3064 }
3065
3066 pub fn content(
3069 mut self,
3070 render: impl Fn(TextFieldRenderState) -> gpui::AnyElement + 'static,
3071 ) -> Self {
3072 self.inner = self.inner.field_content(move |state| {
3073 render(TextFieldRenderState {
3074 is_disabled: state.is_disabled,
3075 is_invalid: state.is_invalid,
3076 is_read_only: state.is_read_only,
3077 is_required: state.is_required,
3078 is_focused: state.focus.is_focused,
3079 is_focus_within: state.focus.is_focus_within,
3080 is_focus_visible: state.focus.is_focus_visible,
3081 })
3082 });
3083 self
3084 }
3085
3086 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
3088 self.inner = self.inner.placeholder(text);
3089 self
3090 }
3091
3092 pub fn description(mut self, text: impl Into<SharedString>) -> Self {
3094 self.inner = self.inner.description(text);
3095 self
3096 }
3097
3098 pub fn auto_focus(mut self, v: bool) -> Self {
3100 self.inner = self.inner.auto_focus(v);
3101 self
3102 }
3103
3104 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
3106 self.inner = self.inner.name(name);
3107 self
3108 }
3109
3110 pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
3112 self.inner = self.inner.default_value(text);
3113 self
3114 }
3115
3116 pub fn value(mut self, text: impl Into<SharedString>) -> Self {
3118 self.inner = self.inner.value(text);
3119 self
3120 }
3121
3122 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
3124 self.inner = self.inner.validation_behavior(behavior);
3125 self
3126 }
3127
3128 pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
3130 self.inner = self.inner.validate(f);
3131 self
3132 }
3133
3134 pub fn validation_errors(
3136 mut self,
3137 errors: impl IntoIterator<Item = impl Into<SharedString>>,
3138 ) -> Self {
3139 self.inner = self.inner.validation_errors(errors);
3140 self
3141 }
3142
3143 pub fn error_message(mut self, text: impl Into<SharedString>) -> Self {
3145 self.inner = self.inner.error_message(text);
3146 self
3147 }
3148
3149 pub fn variant(mut self, variant: FieldVariant) -> Self {
3151 self.inner = self.inner.variant(variant);
3152 self
3153 }
3154
3155 pub fn full_width(mut self) -> Self {
3157 self.inner = self.inner.full_width();
3158 self
3159 }
3160
3161 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
3163 self.inner = self.inner.height(h);
3164 self
3165 }
3166
3167 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
3169 self.inner = self.inner.padding_x(p);
3170 self
3171 }
3172
3173 pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
3175 self.inner = self.inner.recipe(name);
3176 self
3177 }
3178
3179 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
3181 self.inner = self.inner.text_size(size);
3182 self
3183 }
3184
3185 pub fn radius(mut self, r: impl Into<Pixels>) -> Self {
3187 self.inner = self.inner.radius(r);
3188 self
3189 }
3190
3191 pub fn is_bare(mut self, v: bool) -> Self {
3193 self.inner = self.inner.is_bare(v);
3194 self
3195 }
3196
3197 pub fn focus_ring(mut self, v: bool) -> Self {
3200 self.inner = self.inner.focus_ring(v);
3201 self
3202 }
3203
3204 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
3206 self.inner = self.inner.font_family(family);
3207 self
3208 }
3209
3210 pub fn start_content(mut self, el: impl IntoElement) -> Self {
3212 self.inner = self.inner.start_content(el);
3213 self
3214 }
3215
3216 pub fn is_disabled(mut self, v: bool) -> Self {
3218 self.inner = self.inner.is_disabled(v);
3219 self
3220 }
3221
3222 pub fn is_read_only(mut self, v: bool) -> Self {
3224 self.inner = self.inner.is_read_only(v);
3225 self
3226 }
3227
3228 pub fn is_required(mut self, v: bool) -> Self {
3230 self.inner = self.inner.is_required(v);
3231 self
3232 }
3233
3234 pub fn is_invalid(mut self, v: bool) -> Self {
3236 self.inner = self.inner.is_invalid(v);
3237 self
3238 }
3239
3240 pub fn on_change(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3242 self.inner = self.inner.on_change(handler);
3243 self
3244 }
3245
3246 pub fn on_submit(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3248 self.inner = self.inner.on_submit(handler);
3249 self
3250 }
3251
3252 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
3258 self.inner = self.inner.sx(style);
3259 self
3260 }
3261}
3262
3263impl RenderOnce for TextField {
3264 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
3265 self.inner.render(window, cx)
3266 }
3267}
3268
3269#[derive(Clone, Debug, PartialEq, Eq)]
3271#[non_exhaustive]
3272pub struct SearchFieldRenderState {
3273 pub is_disabled: bool,
3275 pub is_invalid: bool,
3277 pub is_read_only: bool,
3279 pub is_required: bool,
3281 pub is_focused: bool,
3283 pub is_focus_within: bool,
3285 pub is_focus_visible: bool,
3287 pub value: SharedString,
3289 pub is_empty: bool,
3291}
3292
3293#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
3299#[derive(IntoElement)]
3300pub struct SearchField {
3301 state: Entity<InputState>,
3302 content: Option<std::sync::Arc<dyn Fn(SearchFieldRenderState) -> gpui::AnyElement + 'static>>,
3304 name: Option<SharedString>,
3306 default_value: Option<SharedString>,
3308 value: Option<SharedString>,
3310 validation_behavior: Option<crate::form::ValidationBehavior>,
3312 label: Option<SharedString>,
3313 placeholder: SharedString,
3314 placeholder_is_set: bool,
3317 description: Option<SharedString>,
3318 variant: FieldVariant,
3319 variant_is_set: bool,
3320 recipes: Vec<SharedString>,
3321 text_size: Option<Pixels>,
3322 font_family: Option<SharedString>,
3324 radius: Option<Pixels>,
3325 full_width: bool,
3326 field: crate::util::FieldBox,
3328 is_disabled: bool,
3329 is_read_only: bool,
3330 is_required: bool,
3331 is_invalid: bool,
3332 auto_focus: bool,
3334 validate: Option<crate::validation::Validator<str>>,
3336 validation_errors: Vec<SharedString>,
3338 on_change: Option<TextCallback>,
3339 on_submit: Option<TextCallback>,
3340 on_clear: Option<std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
3341 search_icon: Option<gpui::AnyElement>,
3343 clear_icon: Option<gpui::AnyElement>,
3345 end_content: Option<gpui::AnyElement>,
3348 sx: Option<Box<gpui::StyleRefinement>>,
3350}
3351
3352impl SearchField {
3353 pub fn content(
3356 mut self,
3357 render: impl Fn(SearchFieldRenderState) -> gpui::AnyElement + 'static,
3358 ) -> Self {
3359 self.content = Some(std::sync::Arc::new(render));
3360 self
3361 }
3362
3363 pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
3366 Self {
3367 content: None,
3368 state: state.borrow().clone(),
3369 name: None,
3370 default_value: None,
3371 value: None,
3372 validation_behavior: None,
3373 label: None,
3374 placeholder: "Search".into(),
3375 placeholder_is_set: false,
3376 description: None,
3377 variant: FieldVariant::Primary,
3378 variant_is_set: false,
3379 recipes: Vec::new(),
3380 text_size: None,
3381 font_family: None,
3382 radius: None,
3383 full_width: false,
3384 field: crate::util::FieldBox::default(),
3385 is_disabled: false,
3386 is_read_only: false,
3387 is_required: false,
3388 is_invalid: false,
3389 auto_focus: false,
3390 validate: None,
3391 validation_errors: Vec::new(),
3392 on_change: None,
3393 on_submit: None,
3394 on_clear: None,
3395 search_icon: None,
3396 clear_icon: None,
3397 end_content: None,
3398 sx: None,
3399 }
3400 }
3401
3402 pub fn label(mut self, text: impl Into<SharedString>) -> Self {
3404 self.label = Some(text.into());
3405 self
3406 }
3407
3408 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
3410 self.placeholder = text.into();
3411 self.placeholder_is_set = true;
3412 self
3413 }
3414
3415 pub fn description(mut self, text: impl Into<SharedString>) -> Self {
3417 self.description = Some(text.into());
3418 self
3419 }
3420
3421 pub fn variant(mut self, variant: FieldVariant) -> Self {
3423 self.variant = variant;
3424 self.variant_is_set = true;
3425 self
3426 }
3427
3428 pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
3430 self.recipes.push(name.into());
3431 self
3432 }
3433
3434 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
3436 self.text_size = Some(size.into());
3437 self
3438 }
3439
3440 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
3444 self.font_family = Some(family.into());
3445 self
3446 }
3447
3448 pub fn radius(mut self, r: impl Into<Pixels>) -> Self {
3450 self.radius = Some(r.into());
3451 self
3452 }
3453
3454 pub fn full_width(mut self) -> Self {
3456 self.full_width = true;
3457 self
3458 }
3459
3460 pub fn height(mut self, h: impl Into<Pixels>) -> Self {
3462 self.field.height = Some(h.into());
3463 self
3464 }
3465
3466 pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
3468 self.field.padding_x = Some(p.into());
3469 self
3470 }
3471
3472 pub fn is_bare(mut self, v: bool) -> Self {
3475 self.field.is_bare = v;
3476 self.field.is_bare_is_set = true;
3477 self
3478 }
3479
3480 pub fn focus_ring(mut self, v: bool) -> Self {
3483 self.field.focus_ring = Some(v);
3484 self
3485 }
3486
3487 pub fn is_disabled(mut self, v: bool) -> Self {
3489 self.is_disabled = v;
3490 self
3491 }
3492
3493 pub fn on_change(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3495 self.on_change = Some(std::sync::Arc::new(handler));
3496 self
3497 }
3498
3499 pub fn on_submit(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3501 self.on_submit = Some(std::sync::Arc::new(handler));
3502 self
3503 }
3504
3505 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
3507 self.name = Some(name.into());
3508 self
3509 }
3510
3511 pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
3513 self.default_value = Some(text.into());
3514 self
3515 }
3516
3517 pub fn value(mut self, text: impl Into<SharedString>) -> Self {
3519 self.value = Some(text.into());
3520 self
3521 }
3522
3523 pub fn search_icon(mut self, el: impl IntoElement) -> Self {
3525 self.search_icon = Some(el.into_any_element());
3526 self
3527 }
3528
3529 pub fn clear_icon(mut self, el: impl IntoElement) -> Self {
3531 self.clear_icon = Some(el.into_any_element());
3532 self
3533 }
3534
3535 pub fn end_content(mut self, el: impl IntoElement) -> Self {
3537 self.end_content = Some(el.into_any_element());
3538 self
3539 }
3540
3541 pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
3543 self.validation_behavior = Some(behavior);
3544 self
3545 }
3546
3547 pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
3549 self.validate = Some(std::sync::Arc::new(f));
3550 self
3551 }
3552
3553 pub fn validation_errors(
3555 mut self,
3556 errors: impl IntoIterator<Item = impl Into<SharedString>>,
3557 ) -> Self {
3558 self.validation_errors = errors.into_iter().map(Into::into).collect();
3559 self
3560 }
3561
3562 pub fn auto_focus(mut self, v: bool) -> Self {
3564 self.auto_focus = v;
3565 self
3566 }
3567
3568 pub fn is_read_only(mut self, v: bool) -> Self {
3570 self.is_read_only = v;
3571 self
3572 }
3573
3574 pub fn is_required(mut self, v: bool) -> Self {
3576 self.is_required = v;
3577 self
3578 }
3579
3580 pub fn is_invalid(mut self, v: bool) -> Self {
3582 self.is_invalid = v;
3583 self
3584 }
3585
3586 pub fn on_clear(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
3588 self.on_clear = Some(std::sync::Arc::new(handler));
3589 self
3590 }
3591
3592 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
3599 crate::util::refine_sx(&mut self.sx, style);
3600 self
3601 }
3602}
3603
3604impl RenderOnce for SearchField {
3605 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
3606 let colors = cx.colors();
3607 let validate = self.validate.clone();
3608 let mut input = Input::new(self.state)
3611 .input_type(InputType::Search)
3612 .when_some(self.content, |i, render| {
3613 i.field_content(move |state| {
3614 let is_empty = state.value.is_empty();
3615 render(SearchFieldRenderState {
3616 is_disabled: state.is_disabled,
3617 is_invalid: state.is_invalid,
3618 is_read_only: state.is_read_only,
3619 is_required: state.is_required,
3620 is_focused: state.focus.is_focused,
3621 is_focus_within: state.focus.is_focus_within,
3622 is_focus_visible: state.focus.is_focus_visible,
3623 value: state.value,
3624 is_empty,
3625 })
3626 })
3627 })
3628 .placeholder(if self.placeholder_is_set {
3629 self.placeholder
3630 } else {
3631 crate::i18n::ui_string(crate::i18n::UiString::Search, cx)
3632 })
3633 .when_some(self.name, |i, n| i.name(n))
3634 .when_some(self.value, |i, v| i.value(v))
3635 .when_some(self.default_value, |i, v| i.default_value(v))
3636 .when_some(self.validation_behavior, |i, b| i.validation_behavior(b))
3637 .with_recipes(self.recipes)
3638 .is_disabled(self.is_disabled)
3639 .is_read_only(self.is_read_only)
3640 .is_required(self.is_required)
3641 .is_invalid(self.is_invalid)
3642 .validation_errors(self.validation_errors.clone())
3643 .auto_focus(self.auto_focus)
3644 .when_some(validate, |i, f| i.validate(move |v| f(v)))
3645 .is_clearable(true)
3646 .start_content(match self.search_icon {
3647 Some(icon) => icon,
3648 None => gpui::svg()
3649 .size(crate::util::FIELD_ICON)
3650 .path(crate::icons::SEARCH)
3651 .flex_shrink_0()
3652 .text_color(colors.muted)
3653 .into_any_element(),
3654 });
3655 if self.variant_is_set {
3656 input = input.variant(self.variant);
3657 }
3658 if let Some(size) = self.text_size {
3659 input = input.text_size(size);
3660 }
3661 if let Some(family) = self.font_family {
3662 input = input.font_family(family);
3663 }
3664 if let Some(r) = self.radius {
3665 input = input.radius(r);
3666 }
3667 input = input.with_field_box(self.field);
3668 if let Some(icon) = self.clear_icon {
3669 input = input.clear_content(icon);
3670 }
3671 if let Some(end) = self.end_content {
3672 input = input.end_content(end);
3673 }
3674
3675 if self.full_width {
3676 input = input.full_width();
3677 }
3678 if let Some(label) = self.label {
3679 input = input.label(label);
3680 }
3681 if let Some(description) = self.description {
3682 input = input.description(description);
3683 }
3684
3685 input = input
3688 .clear_on_escape(true)
3689 .when_some(self.on_clear, |input, on_clear| input.on_clear(on_clear));
3690 if let Some(on_change) = self.on_change {
3691 input = input.on_change(move |text, window, cx| on_change(text, window, cx));
3692 }
3693
3694 if let Some(on_submit) = self.on_submit {
3695 input = input.on_submit(move |text, window, cx| on_submit(text, window, cx));
3696 }
3697
3698 input = input.sx_refinement(self.sx);
3701
3702 input.render(window, cx)
3703 }
3704}
3705
3706#[cfg(test)]
3707mod tests {
3708 use super::*;
3709
3710 #[gpui::test]
3714 fn editing_steps_by_grapheme_cluster(cx: &mut gpui::TestAppContext) {
3715 let value = "ae\u{301}b\u{1F44D}\u{1F3FD}\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{1F1FA}\u{1F1E6}";
3718 let state = cx.new(|cx| InputState::with_value(cx, value));
3719 cx.update(|cx| {
3720 state.update(cx, |s, _| {
3721 let len = s.value.chars().count();
3722 s.cursor = len;
3723 assert!(backspace(s));
3725 assert_eq!(s.value, value.trim_end_matches(['\u{1F1FA}', '\u{1F1E6}']));
3726 move_left(s, false);
3728 assert!(delete(s));
3729 assert_eq!(s.value, "ae\u{301}b\u{1F44D}\u{1F3FD}");
3730 move_right(s, false);
3732 assert!(backspace(s));
3733 assert_eq!(s.value, "ae\u{301}b");
3734 move_left(s, false);
3737 move_left(s, false);
3738 assert_eq!(s.cursor, 1);
3739 move_right(s, false);
3740 assert_eq!(s.cursor, 3);
3741 assert!(backspace(s));
3743 assert_eq!(s.value, "ab");
3744 assert_eq!(s.cursor, 1);
3745 move_right(s, true);
3747 assert_eq!(s.selection(), Some((1, 2)));
3748 });
3749 });
3750 }
3751
3752 #[gpui::test]
3753 fn platform_multi_character_insert_keeps_all_characters(cx: &mut gpui::TestAppContext) {
3754 use gpui::InputHandler;
3755 let cx = cx.add_empty_window();
3756 cx.update(|window, cx| {
3757 let state = cx.new(|cx| InputState::new(cx));
3758 let mut handler = PlatformTextInput {
3759 state: state.clone(),
3760 on_edit: crate::util::shared(|_, _, _| {}),
3761 input_type: InputType::Text,
3762 max_length: None,
3763 multiline: false,
3764 bounds: gpui::Bounds::default(),
3765 font: window.text_style().font(),
3766 paragraphs: cx.new(|_| Vec::new()),
3767 };
3768 handler.replace_text_in_range(None, "Go", window, cx);
3769 assert_eq!(state.read(cx).value(), "Go");
3770 assert_eq!(
3771 handler
3772 .selected_text_range(false, window, cx)
3773 .unwrap()
3774 .range,
3775 2..2
3776 );
3777 });
3778 }
3779
3780 #[gpui::test]
3781 fn platform_composition_replaces_marked_utf16_ranges(cx: &mut gpui::TestAppContext) {
3782 use gpui::InputHandler;
3783 let cx = cx.add_empty_window();
3784 cx.update(|window, cx| {
3785 let state = cx.new(|cx| InputState::with_value(cx, "a😀z"));
3786 let mut handler = PlatformTextInput {
3787 state: state.clone(),
3788 on_edit: crate::util::shared(|_, _, _| {}),
3789 input_type: InputType::Text,
3790 max_length: None,
3791 multiline: false,
3792 bounds: gpui::Bounds::default(),
3793 font: window.text_style().font(),
3794 paragraphs: cx.new(|_| Vec::new()),
3795 };
3796 handler.set_selected_text_range(1..3, window, cx);
3797 handler.replace_and_mark_text_in_range(None, "に", Some(1..1), window, cx);
3798 assert_eq!(state.read(cx).value(), "aにz");
3799 assert_eq!(handler.marked_text_range(window, cx), Some(1..2));
3800 handler.replace_and_mark_text_in_range(None, "日本😀", Some(2..4), window, cx);
3801 assert_eq!(state.read(cx).value(), "a日本😀z");
3802 assert_eq!(handler.marked_text_range(window, cx), Some(1..5));
3803 assert_eq!(
3804 handler
3805 .selected_text_range(false, window, cx)
3806 .unwrap()
3807 .range,
3808 3..5
3809 );
3810 let mut actual = None;
3811 assert_eq!(
3812 handler
3813 .text_for_range(3..5, &mut actual, window, cx)
3814 .as_deref(),
3815 Some("😀")
3816 );
3817 assert_eq!(actual, Some(3..5));
3818 handler.replace_text_in_range(None, "東京", window, cx);
3819 assert_eq!(state.read(cx).value(), "a東京z");
3820 assert_eq!(handler.marked_text_range(window, cx), None);
3821 assert_eq!(
3822 handler
3823 .selected_text_range(false, window, cx)
3824 .unwrap()
3825 .range,
3826 3..3
3827 );
3828 handler.replace_text_in_range(Some(1..3), "", window, cx);
3829 assert_eq!(state.read(cx).value(), "az");
3830 });
3831 }
3832
3833 #[gpui::test]
3834 fn platform_edits_preserve_filters_and_read_only_state(cx: &mut gpui::TestAppContext) {
3835 use gpui::InputHandler;
3836 let cx = cx.add_empty_window();
3837 cx.update(|window, cx| {
3838 let state = cx.new(|cx| InputState::with_value(cx, "12"));
3839 let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
3840 let recorded = changes.clone();
3841 let mut handler = PlatformTextInput {
3842 state: state.clone(),
3843 on_edit: crate::util::shared(move |value, _, _| {
3844 recorded.borrow_mut().push(value.to_owned());
3845 }),
3846 input_type: InputType::Number,
3847 max_length: Some(3),
3848 multiline: false,
3849 bounds: gpui::Bounds::default(),
3850 font: window.text_style().font(),
3851 paragraphs: cx.new(|_| Vec::new()),
3852 };
3853 handler.set_selected_text_range(0..2, window, cx);
3854 handler.replace_text_in_range(None, "abc", window, cx);
3855 assert_eq!(state.read(cx).value(), "12");
3856 assert_eq!(
3857 handler
3858 .selected_text_range(false, window, cx)
3859 .unwrap()
3860 .range,
3861 0..2
3862 );
3863 handler.replace_text_in_range(None, "3x456", window, cx);
3864 assert_eq!(state.read(cx).value(), "345");
3865 assert_eq!(changes.borrow().as_slice(), ["345"]);
3866 state.update(cx, |state, _| state.set_read_only(true));
3867 handler.replace_text_in_range(Some(0..3), "7", window, cx);
3868 assert_eq!(state.read(cx).value(), "345");
3869 assert_eq!(changes.borrow().as_slice(), ["345"]);
3870 assert!(!handler.accepts_text_input(window, cx));
3871 });
3872 }
3873
3874 #[gpui::test]
3875 fn platform_geometry_uses_rendered_row_spacing_and_password_offsets(
3876 cx: &mut gpui::TestAppContext,
3877 ) {
3878 use gpui::InputHandler;
3879 let cx = cx.add_empty_window();
3880 cx.update(|window, cx| {
3881 let state = cx.new(|cx| InputState::with_value(cx, "😀secret"));
3882 let origin = gpui::point(px(30.), px(70.));
3883 let bounds = gpui::Bounds::new(origin, gpui::size(px(80.), px(20.)));
3884 let mut handler = PlatformTextInput {
3885 state: state.clone(),
3886 on_edit: crate::util::shared(|_, _, _| {}),
3887 input_type: InputType::Password,
3888 max_length: None,
3889 multiline: false,
3890 bounds,
3891 font: window.text_style().font(),
3892 paragraphs: cx.new(|_| vec![bounds]),
3893 };
3894 let caret = handler.bounds_for_range(2..2, window, cx).unwrap();
3895 assert!(caret.left() > origin.x);
3896 assert_eq!(caret.top(), origin.y);
3897 assert_eq!(
3898 handler.character_index_for_point(caret.origin, window, cx),
3899 Some(2)
3900 );
3901 state.update(cx, |state, _| state.set_value(""));
3902 assert_eq!(
3903 handler.bounds_for_range(0..0, window, cx).unwrap().origin,
3904 origin
3905 );
3906 state.update(cx, |state, _| {
3907 state.set_value("one two three four five six seven eight nine ten");
3908 });
3909 handler.input_type = InputType::Text;
3910 handler.multiline = true;
3911 let text = state.read(cx).value.clone();
3912 let run = gpui::TextRun {
3913 len: text.len(),
3914 font: handler.font.clone(),
3915 color: gpui::black(),
3916 background_color: None,
3917 underline: None,
3918 strikethrough: None,
3919 };
3920 let shaped = window
3921 .text_system()
3922 .shape_text(
3923 text.clone().into(),
3924 crate::util::FIELD_TEXT,
3925 &[run],
3926 Some(bounds.size.width),
3927 None,
3928 )
3929 .unwrap();
3930 let third_row = (0..text.len())
3931 .find(|&index| {
3932 shaped[0]
3933 .position_for_index(index, px(20.))
3934 .is_some_and(|point| point.y == px(40.))
3935 })
3936 .expect("fixture must wrap into at least three rows");
3937 let caret = handler
3938 .bounds_for_range(third_row..third_row, window, cx)
3939 .unwrap();
3940 assert_eq!(caret.top(), origin.y + px(40.));
3941 assert_eq!(caret.size.height, px(20.));
3942 });
3943 }
3944
3945 #[gpui::test]
3946 fn composition_enter_does_not_submit_or_insert_a_newline(cx: &mut gpui::TestAppContext) {
3947 struct View {
3948 state: Entity<InputState>,
3949 submitted: std::rc::Rc<std::cell::Cell<usize>>,
3950 multiline: bool,
3951 }
3952 impl Render for View {
3953 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
3954 let submitted = self.submitted.clone();
3955 Input::new(self.state.clone())
3956 .multiline(self.multiline)
3957 .on_submit(move |_, _, _| {
3958 submitted.set(submitted.get() + 1);
3959 })
3960 }
3961 }
3962 cx.update(herogpui_theme::ThemeProvider::init);
3963 for multiline in [false, true] {
3964 let state = cx.new(|cx| InputState::with_value(cx, "ab日本z"));
3965 let submitted = std::rc::Rc::new(std::cell::Cell::new(0));
3966 let window = cx.add_window(|window, cx| {
3967 let focus = state.read(cx).focus_handle.clone();
3968 window.focus(&focus, cx);
3969 View {
3970 state: state.clone(),
3971 submitted: submitted.clone(),
3972 multiline,
3973 }
3974 });
3975 let mut visual = gpui::VisualTestContext::from_window(window.into(), cx);
3976 visual.update(|window, cx| {
3977 state.update(cx, |state, cx| {
3978 state.marked = Some(2..4);
3979 state.cursor = 4;
3980 cx.notify();
3981 });
3982 window.refresh();
3983 });
3984 visual.simulate_keystrokes("enter");
3985 assert_eq!(submitted.get(), 0);
3986 assert_eq!(
3987 state.read_with(&visual, |state, _| state.value.clone()),
3988 "ab日本z"
3989 );
3990 visual.simulate_keystrokes("escape");
3991 assert_eq!(
3992 state.read_with(&visual, |state, _| state.marked.clone()),
3993 None
3994 );
3995 }
3996 }
3997
3998 fn check(
3999 value: &str,
4000 ty: InputType,
4001 min_length: Option<usize>,
4002 min: Option<f64>,
4003 max: Option<f64>,
4004 step: Option<f64>,
4005 ) -> InputValidity {
4006 validate_value(value, ty, min_length, min, max, step, None)
4007 }
4008
4009 #[test]
4010 fn empty_is_valid_regardless_of_bounds() {
4011 assert_eq!(
4013 check(
4014 "",
4015 InputType::Number,
4016 Some(5),
4017 Some(10.0),
4018 Some(20.0),
4019 Some(2.0)
4020 ),
4021 InputValidity::Valid
4022 );
4023 }
4024
4025 #[test]
4026 fn min_length_counts_chars_not_bytes() {
4027 assert_eq!(
4028 check("ab", InputType::Text, Some(3), None, None, None),
4029 InputValidity::TooShort
4030 );
4031 assert_eq!(
4032 check("abc", InputType::Text, Some(3), None, None, None),
4033 InputValidity::Valid
4034 );
4035 assert_eq!(
4037 check("éé", InputType::Text, Some(3), None, None, None),
4038 InputValidity::TooShort
4039 );
4040 }
4041
4042 #[test]
4043 fn numeric_bounds_are_inclusive() {
4044 let n = InputType::Number;
4045 assert_eq!(
4046 check("9", n, None, Some(10.0), None, None),
4047 InputValidity::BelowMin
4048 );
4049 assert_eq!(
4050 check("10", n, None, Some(10.0), None, None),
4051 InputValidity::Valid
4052 );
4053 assert_eq!(
4054 check("20", n, None, None, Some(20.0), None),
4055 InputValidity::Valid
4056 );
4057 assert_eq!(
4058 check("21", n, None, None, Some(20.0), None),
4059 InputValidity::AboveMax
4060 );
4061 }
4062
4063 #[test]
4064 fn bounds_are_ignored_for_non_numeric_types() {
4065 assert_eq!(
4067 check("5", InputType::Text, None, Some(10.0), None, None),
4068 InputValidity::Valid
4069 );
4070 }
4071
4072 #[test]
4073 fn unparsable_numeric_input_is_not_a_bounds_error() {
4074 assert_eq!(
4075 check("-", InputType::Number, None, Some(0.0), Some(9.0), None),
4076 InputValidity::Valid
4077 );
4078 }
4079
4080 #[test]
4081 fn step_is_measured_from_min() {
4082 let n = InputType::Number;
4083 assert_eq!(
4085 check("4", n, None, Some(1.0), None, Some(3.0)),
4086 InputValidity::Valid
4087 );
4088 assert_eq!(
4089 check("5", n, None, Some(1.0), None, Some(3.0)),
4090 InputValidity::OffStep
4091 );
4092 assert_eq!(
4094 check("6", n, None, None, None, Some(3.0)),
4095 InputValidity::Valid
4096 );
4097 assert_eq!(
4098 check("5", n, None, None, None, Some(3.0)),
4099 InputValidity::OffStep
4100 );
4101 }
4102
4103 #[test]
4104 fn fractional_steps_tolerate_float_error() {
4105 assert_eq!(
4106 check("0.3", InputType::Number, None, None, None, Some(0.1)),
4107 InputValidity::Valid
4108 );
4109 }
4110
4111 #[test]
4112 fn zero_step_is_ignored_rather_than_dividing_by_zero() {
4113 assert_eq!(
4114 check("5", InputType::Number, None, None, None, Some(0.0)),
4115 InputValidity::Valid
4116 );
4117 }
4118
4119 #[test]
4120 fn pattern_is_checked_before_the_other_rules() {
4121 let deny = |_: &str| false;
4122 assert_eq!(
4123 validate_value(
4124 "ab",
4125 InputType::Text,
4126 Some(10),
4127 None,
4128 None,
4129 None,
4130 Some(&deny)
4131 ),
4132 InputValidity::PatternMismatch
4133 );
4134 let allow = |v: &str| v.starts_with('a');
4135 assert_eq!(
4136 validate_value("abc", InputType::Text, None, None, None, None, Some(&allow)),
4137 InputValidity::Valid
4138 );
4139 }
4140
4141 #[test]
4142 fn validity_helper_reports_valid() {
4143 assert!(InputValidity::Valid.is_valid());
4144 assert!(!InputValidity::TooShort.is_valid());
4145 }
4146
4147 #[test]
4148 fn number_type_rejects_letters() {
4149 assert!(accepts_char(2, 0, '3', InputType::Number, None));
4150 assert!(accepts_char(2, 0, '-', InputType::Number, None));
4151 assert!(accepts_char(2, 0, '.', InputType::Number, None));
4152 assert!(!accepts_char(2, 0, 'a', InputType::Number, None));
4153 }
4154
4155 #[test]
4156 fn text_type_accepts_anything() {
4157 assert!(accepts_char(2, 0, 'c', InputType::Text, None));
4158 assert!(accepts_char(2, 0, '!', InputType::Text, None));
4159 }
4160
4161 #[test]
4162 fn no_max_length_never_blocks() {
4163 assert!(accepts_char(9999, 0, 'x', InputType::Text, None));
4164 }
4165
4166 #[test]
4167 fn max_length_blocks_at_the_limit() {
4168 assert!(accepts_char(2, 0, 'c', InputType::Text, Some(3)));
4169 assert!(!accepts_char(3, 0, 'd', InputType::Text, Some(3)));
4170 assert!(!accepts_char(4, 0, 'd', InputType::Text, Some(3)));
4172 }
4173
4174 #[test]
4175 fn max_length_counts_a_replaced_selection_as_free() {
4176 assert!(accepts_char(3, 3, 'd', InputType::Text, Some(3)));
4178 assert!(accepts_char(3, 1, 'd', InputType::Text, Some(3)));
4180 }
4181
4182 #[test]
4183 fn type_filter_wins_over_available_room() {
4184 assert!(!accepts_char(0, 0, 'a', InputType::Number, Some(10)));
4185 }
4186
4187 #[test]
4188 fn password_masking_preserves_char_count() {
4189 let value = "sécret";
4190 let masked = "•".repeat(value.chars().count());
4191 assert_eq!(masked.chars().count(), value.chars().count());
4192 }
4193
4194 #[test]
4195 fn word_motion_crosses_the_separators_then_the_word() {
4196 let v = "hello world, again";
4197 assert_eq!(word_target(v, 0, true), 5); assert_eq!(word_target(v, 5, true), 11); assert_eq!(word_target(v, 11, false), 6); assert_eq!(word_target(v, 0, false), 0); assert_eq!(word_target(v, 18, true), 18);
4202 }
4203
4204 #[test]
4205 fn line_bounds_exclude_the_newlines() {
4206 let value = "one
4207two
4208three";
4209 assert_eq!(line_bounds(value, 0), (0, 3));
4210 assert_eq!(line_bounds(value, 5), (4, 7));
4211 assert_eq!(line_bounds(value, 13), (8, 13));
4212 }
4213
4214 #[test]
4215 fn vertical_motion_keeps_the_column_where_the_line_is_long_enough() {
4216 let v = "hello
4217hi
4218world";
4219 assert_eq!(vertical_target(v, 3, true), 8); assert_eq!(vertical_target(v, 8, true), 11); assert_eq!(vertical_target(v, 11, false), 8);
4222 }
4223
4224 #[test]
4225 fn vertical_motion_runs_to_the_ends_at_the_edges() {
4226 let v = "one
4227two";
4228 assert_eq!(vertical_target(v, 1, false), 0);
4229 assert_eq!(vertical_target(v, 5, true), 7);
4230 }
4231
4232 #[test]
4233 fn a_selection_slices_on_char_boundaries() {
4234 assert_eq!(
4235 slice_selection("hello world", Some((6, 11))).as_deref(),
4236 Some("world")
4237 );
4238 assert_eq!(
4240 slice_selection("sécret", Some((0, 2))).as_deref(),
4241 Some("sé")
4242 );
4243 assert_eq!(slice_selection("hello", None), None);
4244 }
4245}
4246
4247#[cfg(test)]
4251mod hover_tokens {
4252 #[test]
4253 fn the_field_uses_the_pinned_hover_transition() {
4254 let source = include_str!("input.rs")
4255 .split("#[cfg(test)]")
4256 .next()
4257 .expect("the implementation section is always present");
4258 assert!(
4259 source.contains("colors.field.hover()")
4260 && source.contains("colors.default.hover()")
4261 && source.contains("colors.field.border_hover()")
4262 );
4263 assert!(
4264 source.contains("hover_fade_with_duration_and_easing")
4265 && source.contains("HoverFadeEasing::EaseSmooth")
4266 && source.contains("Some(150)")
4267 );
4268 }
4269
4270 #[test]
4271 fn the_clear_button_hovers_the_role_hover_token() {
4272 let source = include_str!("input.rs")
4275 .split("#[cfg(test)]")
4276 .next()
4277 .expect("the implementation section is always present");
4278 assert!(
4279 source.contains(".hover(move |s| s.bg(clear_hover_bg))")
4280 && source.contains(
4281 "let clear_hover_bg = self.clear_hover_bg.unwrap_or(colors.default.hover());"
4282 ),
4283 "the clear button must hover `bg-default-hover` \
4284 (pinned `.close-button:hover`)"
4285 );
4286 assert!(
4287 !source.contains("with_alpha(0.15)"),
4288 "the clear button must not hover a hand-mixed wash"
4289 );
4290 }
4291}
4292
4293crate::util::impl_component_styled!(Input, SearchField);