1mod element;
28
29use std::ops::Range;
30use std::sync::{Arc, Mutex};
31
32use gpui::{
33 AccessibleAction, App, Bounds, ClipboardItem, Context, CursorStyle, EntityInputHandler,
34 EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, MouseButton,
35 MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ShapedLine,
36 SharedString, StatefulInteractiveElement, Styled, Subscription, UTF16Selection, Window,
37 accesskit::ActionData, actions, div, prelude::FluentBuilder as _, px,
38};
39use gpui_kit_semantics::{NodeSpec, Role, Semantic};
40use gpui_kit_theme::{ActiveTheme, ControlSize};
41use unicode_segmentation::UnicodeSegmentation;
42
43use crate::controls::field::{FieldState, field_shell};
44use crate::controls::text_edit;
45use crate::foundation::{ActiveDirection, Disableable, Ident, Sizable};
46use element::TextElement;
47
48actions!(
49 gpui_kit_input,
50 [
51 Backspace,
52 Delete,
53 DeleteToLineStart,
54 DeleteWordLeft,
55 DeleteWordRight,
56 Left,
57 Right,
58 WordLeft,
59 WordRight,
60 SelectLeft,
61 SelectRight,
62 SelectWordLeft,
63 SelectWordRight,
64 SelectToLineStart,
65 SelectToLineEnd,
66 SelectAll,
67 LineStart,
68 LineEnd,
69 Copy,
70 Cut,
71 Paste,
72 Submit,
73 Cancel,
74 ShowCharacterPalette,
75 ]
76);
77
78pub const KEY_CONTEXT: &str = "TextInput";
81
82pub(crate) fn install(cx: &mut App) {
87 let primary = if cfg!(target_os = "macos") {
88 "cmd"
89 } else {
90 "ctrl"
91 };
92 let word = if cfg!(target_os = "macos") {
93 "alt"
94 } else {
95 "ctrl"
96 };
97 let line = if cfg!(target_os = "macos") { "cmd" } else { "" };
98
99 let mut bindings = vec![
100 KeyBinding::new("backspace", Backspace, Some(KEY_CONTEXT)),
101 KeyBinding::new("delete", Delete, Some(KEY_CONTEXT)),
102 KeyBinding::new("left", Left, Some(KEY_CONTEXT)),
103 KeyBinding::new("right", Right, Some(KEY_CONTEXT)),
104 KeyBinding::new("shift-left", SelectLeft, Some(KEY_CONTEXT)),
105 KeyBinding::new("shift-right", SelectRight, Some(KEY_CONTEXT)),
106 KeyBinding::new("home", LineStart, Some(KEY_CONTEXT)),
107 KeyBinding::new("end", LineEnd, Some(KEY_CONTEXT)),
108 KeyBinding::new("shift-home", SelectToLineStart, Some(KEY_CONTEXT)),
109 KeyBinding::new("shift-end", SelectToLineEnd, Some(KEY_CONTEXT)),
110 KeyBinding::new("enter", Submit, Some(KEY_CONTEXT)),
111 KeyBinding::new("escape", Cancel, Some(KEY_CONTEXT)),
112 KeyBinding::new(&format!("{word}-left"), WordLeft, Some(KEY_CONTEXT)),
113 KeyBinding::new(&format!("{word}-right"), WordRight, Some(KEY_CONTEXT)),
114 KeyBinding::new(
115 &format!("{word}-shift-left"),
116 SelectWordLeft,
117 Some(KEY_CONTEXT),
118 ),
119 KeyBinding::new(
120 &format!("{word}-shift-right"),
121 SelectWordRight,
122 Some(KEY_CONTEXT),
123 ),
124 KeyBinding::new(
125 &format!("{word}-backspace"),
126 DeleteWordLeft,
127 Some(KEY_CONTEXT),
128 ),
129 KeyBinding::new(
130 &format!("{word}-delete"),
131 DeleteWordRight,
132 Some(KEY_CONTEXT),
133 ),
134 KeyBinding::new(&format!("{primary}-a"), SelectAll, Some(KEY_CONTEXT)),
135 KeyBinding::new(&format!("{primary}-c"), Copy, Some(KEY_CONTEXT)),
136 KeyBinding::new(&format!("{primary}-x"), Cut, Some(KEY_CONTEXT)),
137 KeyBinding::new(&format!("{primary}-v"), Paste, Some(KEY_CONTEXT)),
138 ];
139
140 if !line.is_empty() {
141 bindings.extend([
142 KeyBinding::new(&format!("{line}-left"), LineStart, Some(KEY_CONTEXT)),
143 KeyBinding::new(&format!("{line}-right"), LineEnd, Some(KEY_CONTEXT)),
144 KeyBinding::new(
145 &format!("{line}-shift-left"),
146 SelectToLineStart,
147 Some(KEY_CONTEXT),
148 ),
149 KeyBinding::new(
150 &format!("{line}-shift-right"),
151 SelectToLineEnd,
152 Some(KEY_CONTEXT),
153 ),
154 KeyBinding::new(
155 &format!("{line}-backspace"),
156 DeleteToLineStart,
157 Some(KEY_CONTEXT),
158 ),
159 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(KEY_CONTEXT)),
160 ]);
161 }
162
163 cx.bind_keys(bindings);
164}
165
166#[derive(Clone, PartialEq, Eq)]
168pub enum TextInputEvent {
169 Change(SharedString),
171 Submit,
173 Cancel,
175 BackspaceAtStart,
181 Focus,
182 Blur,
183}
184
185impl std::fmt::Debug for TextInputEvent {
186 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Self::Change(_) => formatter
192 .debug_tuple("Change")
193 .field(&"[REDACTED]")
194 .finish(),
195 Self::Submit => formatter.write_str("Submit"),
196 Self::Cancel => formatter.write_str("Cancel"),
197 Self::BackspaceAtStart => formatter.write_str("BackspaceAtStart"),
198 Self::Focus => formatter.write_str("Focus"),
199 Self::Blur => formatter.write_str("Blur"),
200 }
201 }
202}
203
204impl EventEmitter<TextInputEvent> for TextInput {}
205
206pub struct TextInput {
213 ident: Ident,
214 focus_handle: FocusHandle,
215 content: SharedString,
216 placeholder: SharedString,
217 name: SharedString,
221 selected_range: Range<usize>,
223 selection_reversed: bool,
224 marked_range: Option<Range<usize>>,
227 size: ControlSize,
228 disabled: bool,
229 invalid: bool,
230 required: bool,
231 read_only: bool,
232 secret: bool,
235 visually_masked: bool,
239 bare: bool,
241 max_length: Option<usize>,
242 max_graphemes: Option<usize>,
245 visual_slots: Option<usize>,
248 scroll_offset: Pixels,
249 is_selecting: bool,
250 last_layout: Option<ShapedLine>,
251 last_bounds: Option<Bounds<Pixels>>,
252 accessibility_revision: u64,
253 accessible_snapshot: Arc<Mutex<Option<text_edit::PublishedAccessibleText>>>,
254 _subscriptions: Vec<Subscription>,
256}
257
258impl TextInput {
259 pub fn new(ident: impl Into<Ident>, window: &mut Window, cx: &mut Context<Self>) -> Self {
260 let focus_handle = cx.focus_handle();
261 let subscriptions = vec![
262 cx.on_focus(&focus_handle, window, |_, _, cx| {
263 cx.emit(TextInputEvent::Focus)
264 }),
265 cx.on_blur(&focus_handle, window, |_, _, cx| {
266 cx.emit(TextInputEvent::Blur)
267 }),
268 ];
269 Self {
270 ident: ident.into(),
271 focus_handle,
272 content: SharedString::default(),
273 placeholder: SharedString::default(),
274 name: SharedString::default(),
275 selected_range: 0..0,
276 selection_reversed: false,
277 marked_range: None,
278 size: ControlSize::Md,
279 disabled: false,
280 invalid: false,
281 required: false,
282 read_only: false,
283 secret: false,
284 visually_masked: false,
285 bare: false,
286 max_length: None,
287 max_graphemes: None,
288 visual_slots: None,
289 scroll_offset: px(0.0),
290 is_selecting: false,
291 last_layout: None,
292 last_bounds: None,
293 accessibility_revision: 0,
294 accessible_snapshot: Arc::default(),
295 _subscriptions: subscriptions,
296 }
297 }
298
299 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
300 self.placeholder = placeholder.into();
301 self
302 }
303
304 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
307 self.name = name.into();
308 self
309 }
310
311 pub fn set_name(&mut self, name: impl Into<SharedString>, cx: &mut Context<Self>) {
313 self.name = name.into();
314 cx.notify();
315 }
316
317 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
319 let text = text.into();
320 self.content = text_edit::normalize_single_line(&text).into();
321 self.selected_range = self.content.len()..self.content.len();
322 self
323 }
324
325 pub fn invalid(mut self, invalid: bool) -> Self {
326 self.invalid = invalid;
327 self
328 }
329
330 pub fn required(mut self, required: bool) -> Self {
331 self.required = required;
332 self
333 }
334
335 pub fn read_only(mut self, read_only: bool) -> Self {
338 self.read_only = read_only;
339 self
340 }
341
342 pub fn secret(mut self, secret: bool) -> Self {
344 self.secret = secret;
345 self.visually_masked = secret;
346 self
347 }
348
349 pub(crate) fn set_visually_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
354 self.visually_masked = self.secret && masked;
355 cx.notify();
356 }
357
358 pub(crate) fn set_sensitive_slots(&mut self, slots: usize, cx: &mut Context<Self>) {
361 let slots = slots.max(1);
362 self.secret = true;
363 self.visually_masked = true;
364 self.max_graphemes = Some(slots);
365 self.visual_slots = Some(slots);
366 cx.notify();
367 }
368
369 pub fn bare(mut self, bare: bool) -> Self {
375 self.bare = bare;
376 self
377 }
378
379 pub fn max_length(mut self, max_length: usize) -> Self {
381 self.max_length = Some(max_length);
382 self
383 }
384
385 pub fn value(&self) -> &SharedString {
386 &self.content
387 }
388
389 pub fn is_empty(&self) -> bool {
390 self.content.is_empty()
391 }
392
393 pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
395 let value = value.into();
396 self.content = text_edit::normalize_single_line(&value).into();
397 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
398 let end = self.content.len();
399 self.selected_range = end..end;
400 self.marked_range = None;
401 self.scroll_offset = px(0.0);
402 cx.emit(TextInputEvent::Change(self.content.clone()));
403 cx.notify();
404 }
405
406 pub fn set_placeholder(
407 &mut self,
408 placeholder: impl Into<SharedString>,
409 cx: &mut Context<Self>,
410 ) {
411 self.placeholder = placeholder.into();
412 cx.notify();
413 }
414
415 pub fn set_text_quietly(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
421 let value = value.into();
422 self.content = text_edit::normalize_single_line(&value).into();
423 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
424 let end = self.content.len();
425 self.selected_range = end..end;
426 self.marked_range = None;
427 self.scroll_offset = px(0.0);
428 cx.notify();
429 }
430
431 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
432 self.disabled = disabled;
433 if disabled {
434 self.marked_range = None;
435 self.is_selecting = false;
436 }
437 cx.notify();
438 }
439
440 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
441 self.read_only = read_only;
442 cx.notify();
443 }
444
445 pub(crate) fn set_required(&mut self, required: bool, cx: &mut Context<Self>) {
446 self.required = required;
447 cx.notify();
448 }
449
450 pub(crate) fn set_control_size(&mut self, size: ControlSize, cx: &mut Context<Self>) {
451 self.size = size;
452 cx.notify();
453 }
454
455 pub fn set_invalid(&mut self, invalid: bool, cx: &mut Context<Self>) {
456 self.invalid = invalid;
457 cx.notify();
458 }
459
460 pub fn is_disabled(&self) -> bool {
461 self.disabled
462 }
463
464 pub fn is_secret(&self) -> bool {
465 self.secret
466 }
467
468 pub(crate) fn visual_slots(&self) -> Option<usize> {
469 self.visual_slots
470 }
471
472 pub fn selected_range(&self) -> Range<usize> {
473 self.selected_range.clone()
474 }
475
476 pub fn cursor_offset(&self) -> usize {
477 if self.selection_reversed {
478 self.selected_range.start
479 } else {
480 self.selected_range.end
481 }
482 }
483
484 pub(crate) fn placeholder_text(&self) -> &SharedString {
485 &self.placeholder
486 }
487
488 pub(crate) fn accessible_name(&self) -> &SharedString {
489 &self.name
490 }
491
492 pub(crate) fn marked_range(&self) -> Option<Range<usize>> {
493 self.marked_range.clone()
494 }
495
496 pub(crate) fn scroll_offset(&self) -> Pixels {
497 self.scroll_offset
498 }
499
500 pub(crate) fn set_scroll_offset(&mut self, offset: Pixels) {
501 self.scroll_offset = offset;
502 }
503
504 pub(crate) fn set_last_layout(&mut self, line: ShapedLine, bounds: Bounds<Pixels>) {
505 self.last_layout = Some(line);
506 self.last_bounds = Some(bounds);
507 }
508
509 pub(crate) fn display_text(&self) -> SharedString {
514 if !self.visually_masked || self.content.is_empty() {
515 return self.content.clone();
516 }
517 SharedString::from("•".repeat(self.content.graphemes(true).count()))
518 }
519
520 pub(crate) fn display_offset(&self, offset: usize) -> usize {
523 if !self.visually_masked {
524 return offset;
525 }
526 let graphemes = self.content[..offset.min(self.content.len())]
527 .graphemes(true)
528 .count();
529 graphemes * "•".len()
530 }
531
532 fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
533 self.selected_range = offset..offset;
534 self.selection_reversed = false;
535 cx.notify();
536 }
537
538 fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
539 if self.selection_reversed {
540 self.selected_range.start = offset;
541 } else {
542 self.selected_range.end = offset;
543 }
544 if self.selected_range.end < self.selected_range.start {
545 self.selection_reversed = !self.selection_reversed;
546 self.selected_range = self.selected_range.end..self.selected_range.start;
547 }
548 cx.notify();
549 }
550
551 fn previous_boundary(&self, offset: usize) -> usize {
552 text_edit::previous_boundary(&self.content, offset)
553 }
554
555 fn next_boundary(&self, offset: usize) -> usize {
556 text_edit::next_boundary(&self.content, offset)
557 }
558
559 fn previous_word_boundary(&self, offset: usize) -> usize {
560 text_edit::previous_word_boundary(&self.content, offset)
561 }
562
563 fn next_word_boundary(&self, offset: usize) -> usize {
564 text_edit::next_word_boundary(&self.content, offset)
565 }
566
567 pub(crate) fn index_for_position(&self, position: Point<Pixels>, rtl: bool) -> usize {
568 let Some(bounds) = self.last_bounds.as_ref() else {
569 return 0;
570 };
571 if let Some(slots) = self.visual_slots {
572 let x = (position.x - bounds.left()).clamp(px(0.0), bounds.size.width);
573 let width = bounds.size.width.max(px(1.0));
574 let slot_width = width / slots as f32;
575 let physical_slot = ((x / slot_width).floor() as usize).min(slots.saturating_sub(1));
576 let logical_slot = if rtl {
577 slots - physical_slot - 1
578 } else {
579 physical_slot
580 };
581 let after_midpoint = if rtl {
582 x - slot_width * (physical_slot as f32) < slot_width / 2.0
583 } else {
584 x - slot_width * physical_slot as f32 >= slot_width / 2.0
585 };
586 let boundary = (logical_slot + usize::from(after_midpoint))
587 .min(self.content.graphemes(true).count());
588 return self.content_offset_for_grapheme(boundary);
589 }
590 if position.y < bounds.top() {
591 return 0;
592 }
593 if position.y > bounds.bottom() {
594 return self.content.len();
595 }
596 let Some(line) = self.last_layout.as_ref() else {
597 return 0;
598 };
599 let x = position.x - bounds.left() + self.scroll_offset;
600 let display_index = if x <= px(0.0) {
601 0
602 } else if x >= line.width {
603 self.display_text().len()
604 } else {
605 line.index_for_x(x).unwrap_or(self.display_text().len())
606 };
607 self.content_offset_for_display(display_index)
608 }
609
610 fn content_offset_for_display(&self, display_index: usize) -> usize {
612 if !self.visually_masked {
613 return display_index;
614 }
615 let dots = display_index / "•".len();
616 self.content_offset_for_grapheme(dots)
617 }
618
619 fn content_offset_for_grapheme(&self, grapheme: usize) -> usize {
620 self.content
621 .grapheme_indices(true)
622 .nth(grapheme)
623 .map(|(index, _)| index)
624 .unwrap_or(self.content.len())
625 }
626
627 fn grapheme_offset(&self, offset: usize) -> usize {
628 self.content[..offset.min(self.content.len())]
629 .graphemes(true)
630 .count()
631 }
632
633 fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
634 let backwards = self.visual_slots.is_none() || !cx.layout_direction().is_rtl();
635 if self.selected_range.is_empty() {
636 let offset = if backwards {
637 self.previous_boundary(self.cursor_offset())
638 } else {
639 self.next_boundary(self.cursor_offset())
640 };
641 self.move_to(offset, cx);
642 } else {
643 let offset = if backwards {
644 self.selected_range.start
645 } else {
646 self.selected_range.end
647 };
648 self.move_to(offset, cx);
649 }
650 }
651
652 fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
653 let forwards = self.visual_slots.is_none() || !cx.layout_direction().is_rtl();
654 if self.selected_range.is_empty() {
655 let offset = if forwards {
656 self.next_boundary(self.cursor_offset())
657 } else {
658 self.previous_boundary(self.cursor_offset())
659 };
660 self.move_to(offset, cx);
661 } else {
662 let offset = if forwards {
663 self.selected_range.end
664 } else {
665 self.selected_range.start
666 };
667 self.move_to(offset, cx);
668 }
669 }
670
671 fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
672 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
673 self.next_word_boundary(self.cursor_offset())
674 } else {
675 self.previous_word_boundary(self.cursor_offset())
676 };
677 self.move_to(offset, cx);
678 }
679
680 fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
681 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
682 self.previous_word_boundary(self.cursor_offset())
683 } else {
684 self.next_word_boundary(self.cursor_offset())
685 };
686 self.move_to(offset, cx);
687 }
688
689 fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
690 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
691 self.next_boundary(self.cursor_offset())
692 } else {
693 self.previous_boundary(self.cursor_offset())
694 };
695 self.select_to(offset, cx);
696 }
697
698 fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
699 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
700 self.previous_boundary(self.cursor_offset())
701 } else {
702 self.next_boundary(self.cursor_offset())
703 };
704 self.select_to(offset, cx);
705 }
706
707 fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
708 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
709 self.next_word_boundary(self.cursor_offset())
710 } else {
711 self.previous_word_boundary(self.cursor_offset())
712 };
713 self.select_to(offset, cx);
714 }
715
716 fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
717 let offset = if self.visual_slots.is_some() && cx.layout_direction().is_rtl() {
718 self.previous_word_boundary(self.cursor_offset())
719 } else {
720 self.next_word_boundary(self.cursor_offset())
721 };
722 self.select_to(offset, cx);
723 }
724
725 fn select_to_line_start(
726 &mut self,
727 _: &SelectToLineStart,
728 _: &mut Window,
729 cx: &mut Context<Self>,
730 ) {
731 self.select_to(0, cx);
732 }
733
734 fn select_to_line_end(&mut self, _: &SelectToLineEnd, _: &mut Window, cx: &mut Context<Self>) {
735 self.select_to(self.content.len(), cx);
736 }
737
738 fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
739 self.move_to(0, cx);
740 self.select_to(self.content.len(), cx);
741 }
742
743 fn line_start(&mut self, _: &LineStart, _: &mut Window, cx: &mut Context<Self>) {
744 self.move_to(0, cx);
745 }
746
747 fn line_end(&mut self, _: &LineEnd, _: &mut Window, cx: &mut Context<Self>) {
748 self.move_to(self.content.len(), cx);
749 }
750
751 fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
752 if self.selected_range.is_empty() {
753 if self.cursor_offset() == 0 {
754 cx.emit(TextInputEvent::BackspaceAtStart);
755 return;
756 }
757 self.select_to(self.previous_boundary(self.cursor_offset()), cx);
758 }
759 self.replace_text_in_range(None, "", window, cx);
760 }
761
762 fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
763 if self.selected_range.is_empty() {
764 self.select_to(self.next_boundary(self.cursor_offset()), cx);
765 }
766 self.replace_text_in_range(None, "", window, cx);
767 }
768
769 fn delete_word_left(
770 &mut self,
771 _: &DeleteWordLeft,
772 window: &mut Window,
773 cx: &mut Context<Self>,
774 ) {
775 if self.selected_range.is_empty() {
776 self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
777 }
778 self.replace_text_in_range(None, "", window, cx);
779 }
780
781 fn delete_word_right(
782 &mut self,
783 _: &DeleteWordRight,
784 window: &mut Window,
785 cx: &mut Context<Self>,
786 ) {
787 if self.selected_range.is_empty() {
788 self.select_to(self.next_word_boundary(self.cursor_offset()), cx);
789 }
790 self.replace_text_in_range(None, "", window, cx);
791 }
792
793 fn delete_to_line_start(
794 &mut self,
795 _: &DeleteToLineStart,
796 window: &mut Window,
797 cx: &mut Context<Self>,
798 ) {
799 if self.selected_range.is_empty() {
800 self.select_to(0, cx);
801 }
802 self.replace_text_in_range(None, "", window, cx);
803 }
804
805 fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
806 if self.selected_range.is_empty() || self.secret {
809 return;
810 }
811 let selected = self.content[self.selected_range.clone()].to_string();
812 cx.write_to_clipboard(ClipboardItem::new_string(selected));
813 }
814
815 fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
816 if self.selected_range.is_empty() || self.secret {
817 return;
818 }
819 let selected = self.content[self.selected_range.clone()].to_string();
820 cx.write_to_clipboard(ClipboardItem::new_string(selected));
821 self.replace_text_in_range(None, "", window, cx);
822 }
823
824 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
825 let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
826 return;
827 };
828 let text = text.replace(['\n', '\r'], " ");
831 self.replace_text_in_range(None, &text, window, cx);
832 }
833
834 fn submit(&mut self, _: &Submit, _: &mut Window, cx: &mut Context<Self>) {
835 cx.emit(TextInputEvent::Submit);
836 }
837
838 fn cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
839 cx.emit(TextInputEvent::Cancel);
840 }
841
842 fn show_character_palette(
843 &mut self,
844 _: &ShowCharacterPalette,
845 window: &mut Window,
846 _: &mut Context<Self>,
847 ) {
848 window.show_character_palette();
849 }
850
851 fn on_mouse_down(
852 &mut self,
853 event: &MouseDownEvent,
854 window: &mut Window,
855 cx: &mut Context<Self>,
856 ) {
857 if self.disabled {
858 return;
859 }
860 window.focus(&self.focus_handle, cx);
861 self.is_selecting = true;
862 let offset = self.index_for_position(event.position, cx.layout_direction().is_rtl());
863 if event.modifiers.shift {
864 self.select_to(offset, cx);
865 } else if event.click_count > 1 {
866 self.move_to(0, cx);
867 self.select_to(self.content.len(), cx);
868 } else {
869 self.move_to(offset, cx);
870 }
871 }
872
873 fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
874 if self.is_selecting {
875 self.select_to(
876 self.index_for_position(event.position, cx.layout_direction().is_rtl()),
877 cx,
878 );
879 }
880 }
881
882 fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
883 self.is_selecting = false;
884 }
885
886 fn offset_to_utf16(&self, offset: usize) -> usize {
887 text_edit::offset_to_utf16(&self.content, offset)
888 }
889
890 fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
891 text_edit::range_to_utf16(&self.content, range)
892 }
893
894 fn range_from_utf16(&self, range: &Range<usize>) -> Range<usize> {
895 text_edit::range_from_utf16(&self.content, range)
896 }
897
898 fn semantics(&self, window: &Window) -> NodeSpec {
899 let role = if self.secret {
900 Role::PasswordInput
901 } else {
902 Role::Input
903 };
904 let mut spec = NodeSpec::new(self.ident.semantic_id(), role)
905 .disabled(self.disabled)
906 .read_only(self.read_only)
907 .invalid(self.invalid)
908 .required(self.required);
909 if !self.disabled {
910 spec = spec.focus(&self.focus_handle);
911 }
912 if !self.placeholder.is_empty() {
913 spec = spec.placeholder(self.placeholder.clone());
914 }
915 if !self.name.is_empty() {
916 spec = spec.text(self.name.clone());
917 }
918 if self.secret {
921 if !self.content.is_empty() {
922 spec = spec.value("[REDACTED]");
923 }
924 if let Some(slots) = self.visual_slots {
925 spec = spec.description(SharedString::from(format!(
926 "{}/{}",
927 self.content.graphemes(true).count(),
928 slots
929 )));
930 }
931 } else if !self.content.is_empty() {
932 spec = spec.value(self.content.clone());
933 }
934 let _ = window;
935 spec
936 }
937}
938
939impl std::fmt::Debug for TextInput {
940 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
941 formatter
944 .debug_struct("TextInput")
945 .field("id", &self.ident)
946 .field("size", &self.size)
947 .field("disabled", &self.disabled)
948 .field("invalid", &self.invalid)
949 .field("secret", &self.secret)
950 .field("length", &self.content.graphemes(true).count())
951 .finish()
952 }
953}
954
955impl Disableable for TextInput {
956 fn disabled(mut self, disabled: bool) -> Self {
957 self.disabled = disabled;
958 self
959 }
960}
961
962impl Sizable for TextInput {
963 fn control_size(mut self, size: ControlSize) -> Self {
964 self.size = size;
965 self
966 }
967}
968
969impl Focusable for TextInput {
970 fn focus_handle(&self, _cx: &App) -> FocusHandle {
971 self.focus_handle.clone()
972 }
973}
974
975impl EntityInputHandler for TextInput {
976 fn text_for_range(
977 &mut self,
978 range_utf16: Range<usize>,
979 actual_range: &mut Option<Range<usize>>,
980 _window: &mut Window,
981 _cx: &mut Context<Self>,
982 ) -> Option<String> {
983 let range = self.range_from_utf16(&range_utf16);
984 actual_range.replace(self.range_to_utf16(&range));
985 Some(self.content.get(range)?.to_string())
986 }
987
988 fn selected_text_range(
989 &mut self,
990 _ignore_disabled_input: bool,
991 _window: &mut Window,
992 _cx: &mut Context<Self>,
993 ) -> Option<UTF16Selection> {
994 Some(UTF16Selection {
995 range: self.range_to_utf16(&self.selected_range),
996 reversed: self.selection_reversed,
997 })
998 }
999
1000 fn marked_text_range(
1001 &self,
1002 _window: &mut Window,
1003 _cx: &mut Context<Self>,
1004 ) -> Option<Range<usize>> {
1005 self.marked_range
1006 .as_ref()
1007 .map(|range| self.range_to_utf16(range))
1008 }
1009
1010 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
1011 self.marked_range = None;
1012 }
1013
1014 fn replace_text_in_range(
1015 &mut self,
1016 range_utf16: Option<Range<usize>>,
1017 new_text: &str,
1018 _window: &mut Window,
1019 cx: &mut Context<Self>,
1020 ) {
1021 if self.disabled || self.read_only {
1022 return;
1023 }
1024 let range = range_utf16
1025 .as_ref()
1026 .map(|range| self.range_from_utf16(range))
1027 .or_else(|| self.marked_range.clone())
1028 .unwrap_or_else(|| self.selected_range.clone());
1029
1030 let new_text = text_edit::normalize_single_line(new_text);
1031 let new_text =
1032 text_edit::fit_to_max_length(&self.content, self.max_length, &range, &new_text);
1033 let new_text =
1034 text_edit::fit_to_max_graphemes(&self.content, self.max_graphemes, &range, &new_text);
1035 let next_content =
1036 self.content[..range.start].to_owned() + &new_text + &self.content[range.end..];
1037 let changed = self.content.as_ref() != next_content.as_str();
1038 if changed {
1039 self.content = next_content.into();
1040 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
1041 }
1042 let caret = range.start + new_text.len();
1043 self.selected_range = caret..caret;
1044 self.selection_reversed = false;
1045 self.marked_range = None;
1046 if changed {
1047 cx.emit(TextInputEvent::Change(self.content.clone()));
1048 }
1049 cx.notify();
1050 }
1051
1052 fn replace_and_mark_text_in_range(
1053 &mut self,
1054 range_utf16: Option<Range<usize>>,
1055 new_text: &str,
1056 new_selected_range_utf16: Option<Range<usize>>,
1057 _window: &mut Window,
1058 cx: &mut Context<Self>,
1059 ) {
1060 if self.disabled || self.read_only {
1061 return;
1062 }
1063 let range = range_utf16
1064 .as_ref()
1065 .map(|range| self.range_from_utf16(range))
1066 .or_else(|| self.marked_range.clone())
1067 .unwrap_or_else(|| self.selected_range.clone());
1068
1069 let new_text = text_edit::normalize_single_line(new_text);
1070 let new_text =
1071 text_edit::fit_to_max_length(&self.content, self.max_length, &range, &new_text);
1072 let new_text =
1073 text_edit::fit_to_max_graphemes(&self.content, self.max_graphemes, &range, &new_text);
1074 let next_content =
1075 self.content[..range.start].to_owned() + &new_text + &self.content[range.end..];
1076 let changed = self.content.as_ref() != next_content.as_str();
1077 if changed {
1078 self.content = next_content.into();
1079 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
1080 }
1081 self.marked_range =
1082 (!new_text.is_empty()).then(|| range.start..range.start + new_text.len());
1083 self.selected_range = new_selected_range_utf16
1084 .as_ref()
1085 .map(|range_utf16| text_edit::range_from_utf16(&new_text, range_utf16))
1090 .map(|new_range| new_range.start + range.start..new_range.end + range.start)
1091 .unwrap_or_else(|| {
1092 let caret = range.start + new_text.len();
1093 caret..caret
1094 });
1095 self.selection_reversed = false;
1096 if changed {
1097 cx.emit(TextInputEvent::Change(self.content.clone()));
1098 }
1099 cx.notify();
1100 }
1101
1102 fn bounds_for_range(
1103 &mut self,
1104 range_utf16: Range<usize>,
1105 bounds: Bounds<Pixels>,
1106 _window: &mut Window,
1107 cx: &mut Context<Self>,
1108 ) -> Option<Bounds<Pixels>> {
1109 let range = self.range_from_utf16(&range_utf16);
1110 if let Some(slots) = self.visual_slots {
1111 let boundary_x = |offset| {
1112 let boundary = self.grapheme_offset(offset) as f32 / slots as f32;
1113 if cx.layout_direction().is_rtl() {
1114 bounds.right() - bounds.size.width * boundary
1115 } else {
1116 bounds.left() + bounds.size.width * boundary
1117 }
1118 };
1119 let start = boundary_x(range.start);
1120 let end = boundary_x(range.end);
1121 return Some(Bounds::from_corners(
1122 gpui::point(start.min(end), bounds.top()),
1123 gpui::point(start.max(end), bounds.bottom()),
1124 ));
1125 }
1126 let line = self.last_layout.as_ref()?;
1127 Some(Bounds::from_corners(
1128 gpui::point(
1129 bounds.left() + line.x_for_index(self.display_offset(range.start))
1130 - self.scroll_offset,
1131 bounds.top(),
1132 ),
1133 gpui::point(
1134 bounds.left() + line.x_for_index(self.display_offset(range.end))
1135 - self.scroll_offset,
1136 bounds.bottom(),
1137 ),
1138 ))
1139 }
1140
1141 fn character_index_for_point(
1142 &mut self,
1143 point: Point<Pixels>,
1144 _window: &mut Window,
1145 cx: &mut Context<Self>,
1146 ) -> Option<usize> {
1147 let offset = self.index_for_position(point, cx.layout_direction().is_rtl());
1148 Some(self.offset_to_utf16(offset))
1149 }
1150}
1151
1152impl Render for TextInput {
1153 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1154 if self.disabled && self.focus_handle.is_focused(window) {
1155 window.blur();
1156 }
1157 let theme = cx.theme().clone();
1158 let metrics = theme.control.get(self.size);
1159 let focused = self.focus_handle.is_focused(window);
1160 let spec = self.semantics(window);
1161 let shell = if self.bare {
1162 div().w_full().flex().flex_row().items_center()
1163 } else {
1164 field_shell(
1165 &theme,
1166 self.size,
1167 FieldState::default()
1168 .focused(focused)
1169 .invalid(self.invalid)
1170 .disabled(self.disabled),
1171 )
1172 };
1173
1174 let content = self.content.clone();
1175 let (anchor, focus) = if self.selection_reversed {
1176 (self.selected_range.end, self.selected_range.start)
1177 } else {
1178 (self.selected_range.start, self.selected_range.end)
1179 };
1180 let accessible_snapshot = self.accessible_snapshot.clone();
1181 let selection_representable = text_edit::accessible_text_is_representable(&content);
1182 let accessible_rows = std::iter::once(0..content.len()).collect::<Vec<_>>();
1183 let accessibility_revision = self.accessibility_revision;
1184 let entity = cx.entity().clone();
1185 let accessible_direction = if cx.layout_direction().is_rtl() {
1186 gpui::accesskit::TextDirection::RightToLeft
1187 } else {
1188 gpui::accesskit::TextDirection::LeftToRight
1189 };
1190
1191 shell
1192 .id(self.ident.element_id())
1193 .key_context(KEY_CONTEXT)
1194 .when(!self.disabled, |element| {
1195 element.track_focus(&self.focus_handle)
1196 })
1197 .when(!self.disabled, |element| {
1198 element
1199 .on_action(cx.listener(Self::left))
1200 .on_action(cx.listener(Self::right))
1201 .on_action(cx.listener(Self::word_left))
1202 .on_action(cx.listener(Self::word_right))
1203 .on_action(cx.listener(Self::select_left))
1204 .on_action(cx.listener(Self::select_right))
1205 .on_action(cx.listener(Self::select_word_left))
1206 .on_action(cx.listener(Self::select_word_right))
1207 .on_action(cx.listener(Self::select_to_line_start))
1208 .on_action(cx.listener(Self::select_to_line_end))
1209 .on_action(cx.listener(Self::select_all))
1210 .on_action(cx.listener(Self::line_start))
1211 .on_action(cx.listener(Self::line_end))
1212 .on_action(cx.listener(Self::copy))
1213 .on_action(cx.listener(Self::submit))
1214 .on_action(cx.listener(Self::cancel))
1215 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1216 .on_mouse_move(cx.listener(Self::on_mouse_move))
1217 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
1218 .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
1219 .cursor(CursorStyle::IBeam)
1220 })
1221 .when(!self.disabled && !self.read_only, |element| {
1222 element
1223 .on_action(cx.listener(Self::backspace))
1224 .on_action(cx.listener(Self::delete))
1225 .on_action(cx.listener(Self::delete_word_left))
1226 .on_action(cx.listener(Self::delete_word_right))
1227 .on_action(cx.listener(Self::delete_to_line_start))
1228 .on_action(cx.listener(Self::cut))
1229 .on_action(cx.listener(Self::paste))
1230 .on_action(cx.listener(Self::show_character_palette))
1231 })
1232 .when(!self.secret, |element| {
1233 element
1234 .a11y_synthetic_children(move |builder| {
1235 let ids = text_edit::publish_accessible_text(
1236 builder,
1237 &content,
1238 anchor,
1239 focus,
1240 accessible_direction,
1241 &accessible_rows,
1242 accessibility_revision,
1243 );
1244 *accessible_snapshot
1245 .lock()
1246 .unwrap_or_else(|poisoned| poisoned.into_inner()) = ids;
1247 })
1248 .when(!self.disabled && selection_representable, |element| {
1249 let selection_entity = entity.clone();
1250 let selection_snapshot = self.accessible_snapshot.clone();
1251 element.on_a11y_action(
1252 AccessibleAction::SetTextSelection,
1253 move |data, _, cx| {
1254 let Some(ActionData::SetTextSelection(selection)) = data else {
1255 return;
1256 };
1257 let published = selection_snapshot
1258 .lock()
1259 .unwrap_or_else(|poisoned| poisoned.into_inner())
1260 .clone();
1261 selection_entity.update(cx, |input, cx| {
1262 if input.disabled {
1263 return;
1264 }
1265 let Some(published) = published.as_ref() else {
1266 return;
1267 };
1268 let Some(anchor) =
1269 text_edit::byte_offset_for_published_position(
1270 &input.content,
1271 input.accessibility_revision,
1272 published,
1273 selection.anchor,
1274 )
1275 else {
1276 return;
1277 };
1278 let Some(focus) = text_edit::byte_offset_for_published_position(
1279 &input.content,
1280 input.accessibility_revision,
1281 published,
1282 selection.focus,
1283 ) else {
1284 return;
1285 };
1286 input.selected_range = anchor.min(focus)..anchor.max(focus);
1287 input.selection_reversed = focus < anchor;
1288 input.marked_range = None;
1289 cx.notify();
1290 });
1291 },
1292 )
1293 })
1294 })
1295 .when(!self.disabled && !self.read_only, |element| {
1296 element.on_a11y_action(AccessibleAction::SetValue, move |data, window, cx| {
1297 let Some(ActionData::Value(value)) = data else {
1298 return;
1299 };
1300 entity.update(cx, |input, cx| {
1301 if input.disabled || input.read_only {
1302 return;
1303 }
1304 let end = text_edit::offset_to_utf16(&input.content, input.content.len());
1305 input.replace_text_in_range(Some(0..end), value, window, cx);
1306 });
1307 })
1308 })
1309 .h(px(metrics.height))
1310 .child(TextElement::new(cx.entity()))
1311 .semantic_in(cx, spec)
1312 }
1313}