1use std::borrow::Cow;
32use std::time::Duration;
33
34use gpui::prelude::*;
35use gpui::*;
36
37use crate::theme::{get_theme_or, Theme};
38use super::cursor_blink::CursorBlink;
39use super::editing_core::EditingCore;
40use super::focus_navigation::{FocusNext, FocusPrev};
41
42actions!(
44 ccf_text_input,
45 [
46 MoveLeft,
47 MoveRight,
48 MoveWordLeft,
49 MoveWordRight,
50 MoveToStart,
51 MoveToEnd,
52 SelectLeft,
53 SelectRight,
54 SelectWordLeft,
55 SelectWordRight,
56 SelectToStart,
57 SelectToEnd,
58 SelectAll,
59 DeleteBackward,
60 DeleteForward,
61 DeleteWordBackward,
62 DeleteWordForward,
63 Cut,
64 Copy,
65 Paste,
66 Enter,
67 Escape,
68 ]
69);
70
71pub fn register_keybindings(cx: &mut App) {
78 cx.bind_keys([
79 KeyBinding::new("left", MoveLeft, Some("CcfTextInput")),
81 KeyBinding::new("right", MoveRight, Some("CcfTextInput")),
82 KeyBinding::new("home", MoveToStart, Some("CcfTextInput")),
83 KeyBinding::new("end", MoveToEnd, Some("CcfTextInput")),
84 KeyBinding::new("shift-left", SelectLeft, Some("CcfTextInput")),
86 KeyBinding::new("shift-right", SelectRight, Some("CcfTextInput")),
87 KeyBinding::new("shift-home", SelectToStart, Some("CcfTextInput")),
88 KeyBinding::new("shift-end", SelectToEnd, Some("CcfTextInput")),
89 KeyBinding::new("backspace", DeleteBackward, Some("CcfTextInput")),
91 KeyBinding::new("delete", DeleteForward, Some("CcfTextInput")),
92 KeyBinding::new("enter", Enter, Some("CcfTextInput")),
94 KeyBinding::new("escape", Escape, Some("CcfTextInput")),
95 ]);
97
98 #[cfg(target_os = "macos")]
100 cx.bind_keys([
101 KeyBinding::new("cmd-a", SelectAll, Some("CcfTextInput")),
103 KeyBinding::new("cmd-c", Copy, Some("CcfTextInput")),
104 KeyBinding::new("cmd-x", Cut, Some("CcfTextInput")),
105 KeyBinding::new("cmd-v", Paste, Some("CcfTextInput")),
106 KeyBinding::new("alt-left", MoveWordLeft, Some("CcfTextInput")),
108 KeyBinding::new("alt-right", MoveWordRight, Some("CcfTextInput")),
109 KeyBinding::new("shift-alt-left", SelectWordLeft, Some("CcfTextInput")),
110 KeyBinding::new("shift-alt-right", SelectWordRight, Some("CcfTextInput")),
111 KeyBinding::new("alt-backspace", DeleteWordBackward, Some("CcfTextInput")),
113 KeyBinding::new("alt-delete", DeleteWordForward, Some("CcfTextInput")),
114 ]);
115
116 #[cfg(not(target_os = "macos"))]
117 cx.bind_keys([
118 KeyBinding::new("ctrl-a", SelectAll, Some("CcfTextInput")),
120 KeyBinding::new("ctrl-c", Copy, Some("CcfTextInput")),
121 KeyBinding::new("ctrl-x", Cut, Some("CcfTextInput")),
122 KeyBinding::new("ctrl-v", Paste, Some("CcfTextInput")),
123 KeyBinding::new("ctrl-left", MoveWordLeft, Some("CcfTextInput")),
125 KeyBinding::new("ctrl-right", MoveWordRight, Some("CcfTextInput")),
126 KeyBinding::new("shift-ctrl-left", SelectWordLeft, Some("CcfTextInput")),
127 KeyBinding::new("shift-ctrl-right", SelectWordRight, Some("CcfTextInput")),
128 KeyBinding::new("ctrl-backspace", DeleteWordBackward, Some("CcfTextInput")),
130 KeyBinding::new("ctrl-delete", DeleteWordForward, Some("CcfTextInput")),
131 ]);
132}
133
134#[derive(Clone, Debug)]
136pub enum TextInputEvent {
137 Change,
139 Enter,
141 Escape,
143 Blur,
145 Focus,
147 Tab,
149 ShiftTab,
151}
152
153const MASK_CHAR: &str = "\u{25CF}"; pub struct TextInput {
158 core: EditingCore<String>,
160 placeholder: Option<SharedString>,
162 focus_handle: FocusHandle,
164 pub select_on_focus: bool,
166 scroll_offset: f32,
168 visible_width: f32,
170 content_origin_x: f32,
172 was_focused: bool,
174 focus_out_subscribed: bool,
176 cursor_blink: CursorBlink,
178 blink_timer_active: bool,
180 custom_theme: Option<Theme>,
182 is_dragging: bool,
184 auto_scroll_active: bool,
186 auto_scroll_speed: f32,
188 borderless: bool,
190 input_filter: Option<Box<dyn Fn(char) -> bool>>,
192 emit_tab_events: bool,
194 enabled: bool,
196}
197
198impl EventEmitter<TextInputEvent> for TextInput {}
199
200impl Focusable for TextInput {
201 fn focus_handle(&self, _cx: &App) -> FocusHandle {
202 self.focus_handle.clone()
203 }
204}
205
206impl TextInput {
207 pub fn new(cx: &mut Context<Self>) -> Self {
209 Self {
210 core: EditingCore::new(),
211 placeholder: None,
212 focus_handle: cx.focus_handle().tab_stop(true),
213 select_on_focus: false,
214 scroll_offset: 0.0,
215 visible_width: 200.0,
216 content_origin_x: 0.0,
217 was_focused: false,
218 focus_out_subscribed: false,
219 cursor_blink: CursorBlink::new(),
220 blink_timer_active: false,
221 custom_theme: None,
222 is_dragging: false,
223 auto_scroll_active: false,
224 auto_scroll_speed: 0.0,
225 borderless: false,
226 input_filter: None,
227 emit_tab_events: false,
228 enabled: true,
229 }
230 }
231
232 #[must_use]
234 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
235 self.placeholder = Some(text.into());
236 self
237 }
238
239 #[must_use]
241 pub fn with_value(mut self, text: impl Into<String>) -> Self {
242 let text = text.into();
243 self.core.set_content(&text);
244 self
245 }
246
247 #[must_use]
249 pub fn theme(mut self, theme: Theme) -> Self {
250 self.custom_theme = Some(theme);
251 self
252 }
253
254 #[must_use]
256 pub fn select_on_focus(mut self, select: bool) -> Self {
257 self.select_on_focus = select;
258 self
259 }
260
261 #[must_use]
266 pub fn masked(mut self, masked: bool) -> Self {
267 self.core.set_masked(masked);
268 self
269 }
270
271 pub fn is_masked(&self) -> bool {
273 self.core.is_masked()
274 }
275
276 pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
278 self.core.set_masked(masked);
279 cx.notify();
280 }
281
282 #[must_use]
287 pub fn borderless(mut self, borderless: bool) -> Self {
288 self.borderless = borderless;
289 self
290 }
291
292 #[must_use]
303 pub fn input_filter(mut self, filter: impl Fn(char) -> bool + 'static) -> Self {
304 self.input_filter = Some(Box::new(filter));
305 self
306 }
307
308 #[must_use]
318 pub fn emit_tab_events(mut self, emit: bool) -> Self {
319 self.emit_tab_events = emit;
320 self
321 }
322
323 #[must_use]
328 pub fn with_enabled(mut self, enabled: bool) -> Self {
329 self.enabled = enabled;
330 self
331 }
332
333 pub fn is_enabled(&self) -> bool {
335 self.enabled
336 }
337
338 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
340 if self.enabled != enabled {
341 self.enabled = enabled;
342 cx.notify();
343 }
344 }
345
346 fn display_content(&self) -> Cow<'_, str> {
349 if self.core.is_masked() {
350 Cow::Owned(MASK_CHAR.repeat(self.core.content().chars().count()))
351 } else {
352 Cow::Borrowed(self.core.content())
353 }
354 }
355
356 fn content_byte_to_display_byte(&self, content_pos: usize) -> usize {
358 if !self.core.is_masked() || self.core.content().is_empty() {
359 return content_pos;
360 }
361 let char_count = self.core.content()[..content_pos].chars().count();
362 char_count * MASK_CHAR.len()
363 }
364
365 fn display_byte_to_content_byte(&self, display_pos: usize) -> usize {
367 if !self.core.is_masked() || self.core.content().is_empty() {
368 return display_pos;
369 }
370 let mask_char_len = MASK_CHAR.len();
371 let char_index = display_pos / mask_char_len;
372 self.core.content()
374 .char_indices()
375 .nth(char_index)
376 .map(|(i, _)| i)
377 .unwrap_or(self.core.content().len())
378 }
379
380 fn reset_cursor_blink(&mut self) {
382 self.cursor_blink.reset();
383 }
384
385 pub fn content(&self) -> &str {
387 self.core.content()
388 }
389
390 pub fn set_value(&mut self, value: &str, cx: &mut Context<Self>) {
392 self.core.set_content(value);
393 self.scroll_offset = 0.0;
394 cx.emit(TextInputEvent::Change);
395 cx.notify();
396 }
397
398 pub fn set_placeholder(&mut self, text: impl Into<SharedString>, cx: &mut Context<Self>) {
400 self.placeholder = Some(text.into());
401 cx.notify();
402 }
403
404 pub fn focus_handle(&self) -> &FocusHandle {
406 &self.focus_handle
407 }
408
409 fn copy(&self, cx: &mut Context<Self>) {
411 if self.core.is_masked() {
412 return;
414 }
415 if let Some(text) = self.core.selected_text() {
416 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
417 }
418 }
419
420 fn cut(&mut self, cx: &mut Context<Self>) {
422 if !self.core.is_masked() {
423 self.copy(cx);
424 }
425 if self.core.delete_selection() {
426 self.reset_cursor_blink();
427 cx.emit(TextInputEvent::Change);
428 cx.notify();
429 }
430 }
431
432 fn paste(&mut self, cx: &mut Context<Self>) {
434 if let Some(clipboard) = cx.read_from_clipboard() {
435 if let Some(text) = clipboard.text() {
436 let clean_text = text.replace(['\n', '\r'], "");
437
438 let filtered_text: String = if let Some(ref filter) = self.input_filter {
440 clean_text.chars().filter(|c| filter(*c)).collect()
441 } else {
442 clean_text
443 };
444
445 if filtered_text.is_empty() {
446 return;
447 }
448
449 self.core.insert_text(&filtered_text);
450 self.reset_cursor_blink();
451 cx.emit(TextInputEvent::Change);
452 cx.notify();
453 }
454 }
455 }
456
457 fn on_focus(&mut self, cx: &mut Context<Self>) {
459 if self.select_on_focus && !self.core.content().is_empty() {
460 self.core.select_all();
461 }
462 self.reset_cursor_blink();
463 cx.emit(TextInputEvent::Focus);
464 cx.notify();
465 }
466
467 fn on_blur(&mut self, cx: &mut Context<Self>) {
469 self.stop_drag();
471 cx.emit(TextInputEvent::Blur);
474 cx.notify();
475 }
476
477 fn shape_line(&self, window: &Window) -> Option<ShapedLine> {
479 let display = self.display_content().into_owned();
480 if display.is_empty() {
481 return None;
482 }
483
484 let style = window.text_style();
485 let font_size = window.rem_size() * 0.875;
486
487 let run = TextRun {
488 len: display.len(),
489 font: style.font(),
490 color: style.color,
491 background_color: None,
492 underline: None,
493 strikethrough: None,
494 };
495
496 Some(window.text_system().shape_line(
497 SharedString::from(display),
498 font_size,
499 &[run],
500 None,
501 ))
502 }
503
504 fn cursor_at_x(&self, x: f32, window: &Window) -> usize {
506 let adjusted_x = x + self.scroll_offset;
507
508 if adjusted_x <= 0.0 || self.core.content().is_empty() {
509 return 0;
510 }
511
512 if let Some(line) = self.shape_line(window) {
513 let display_pos = line.closest_index_for_x(px(adjusted_x));
514 self.display_byte_to_content_byte(display_pos)
515 } else {
516 0
517 }
518 }
519
520 fn x_for_cursor(&self, cursor: usize, window: &Window) -> f32 {
522 if self.core.content().is_empty() || cursor == 0 {
523 return 0.0;
524 }
525
526 if let Some(line) = self.shape_line(window) {
527 let display_cursor = self.content_byte_to_display_byte(cursor);
528 let pixels = line.x_for_index(display_cursor);
529 pixels.into()
530 } else {
531 0.0
532 }
533 }
534
535 fn ensure_cursor_visible(&mut self, window: &Window) {
537 let cursor_x = self.x_for_cursor(self.core.cursor(), window);
538 let content_width = self.x_for_cursor(self.core.content().len(), window);
539 let margin = 2.0;
540 let cursor_width = 1.0;
541 let padding = 2.0;
542
543 let actual_visible = self.visible_width - padding;
544
545 if content_width <= actual_visible {
546 self.scroll_offset = 0.0;
547 return;
548 }
549
550 let visual_cursor_x = cursor_x - self.scroll_offset;
551
552 if visual_cursor_x + cursor_width > actual_visible - margin {
553 self.scroll_offset = cursor_x + cursor_width - actual_visible + margin;
554 }
555
556 if visual_cursor_x < margin {
557 self.scroll_offset = (cursor_x - margin).max(0.0);
558 }
559
560 let max_scroll = (content_width + cursor_width - actual_visible + margin).max(0.0);
561 self.scroll_offset = self.scroll_offset.clamp(0.0, max_scroll);
562 }
563
564 fn handle_drag_move(&mut self, mouse_x: f32, window: &Window) -> f32 {
567 if !self.is_dragging {
568 return 0.0;
569 }
570
571 let relative_x = mouse_x - self.content_origin_x;
572 let padding = 2.0;
573 let actual_visible = self.visible_width - padding;
574
575 let scroll_speed = if relative_x < 0.0 {
577 -self.calculate_scroll_speed(-relative_x)
579 } else if relative_x > actual_visible {
580 self.calculate_scroll_speed(relative_x - actual_visible)
582 } else {
583 0.0
584 };
585
586 let clamped_x = relative_x.clamp(0.0, actual_visible);
588 let new_cursor = self.cursor_at_x(clamped_x, window);
589 self.core.extend_selection_to(new_cursor);
590 self.reset_cursor_blink();
591
592 scroll_speed
593 }
594
595 fn calculate_scroll_speed(&self, distance: f32) -> f32 {
598 let base_speed = 0.5; let max_speed = 20.0; let max_distance = 100.0; let normalized = (distance / max_distance).min(1.0);
603 let eased = 1.0 - (1.0 - normalized).powi(2);
605 base_speed + eased * (max_speed - base_speed)
606 }
607
608 fn apply_auto_scroll(&mut self, window: &Window) {
610 if self.auto_scroll_speed == 0.0 {
611 return;
612 }
613
614 let content_width = self.x_for_cursor(self.core.content().len(), window);
615 let padding = 2.0;
616 let actual_visible = self.visible_width - padding;
617 let max_scroll = (content_width - actual_visible).max(0.0);
618
619 self.scroll_offset = (self.scroll_offset + self.auto_scroll_speed).clamp(0.0, max_scroll);
621
622 if self.auto_scroll_speed < 0.0 {
624 let new_cursor = self.cursor_at_x(0.0, window);
626 self.core.extend_selection_to(new_cursor);
627 } else {
628 let new_cursor = self.cursor_at_x(actual_visible, window);
630 self.core.extend_selection_to(new_cursor);
631 }
632 }
633
634 fn stop_drag(&mut self) {
636 self.is_dragging = false;
637 self.auto_scroll_active = false;
638 self.auto_scroll_speed = 0.0;
639 }
640
641 fn spawn_auto_scroll_timer_if_needed(&mut self, scroll_speed: f32, window: &mut Window, cx: &mut Context<Self>) {
643 self.auto_scroll_speed = scroll_speed;
644 if scroll_speed != 0.0 && !self.auto_scroll_active {
645 self.auto_scroll_active = true;
646 let entity = cx.entity();
647 window.spawn(cx, async move |async_cx| {
648 loop {
649 smol::Timer::after(Duration::from_millis(32)).await; let should_continue = async_cx
651 .update_entity(&entity, |this, cx| {
652 if !this.auto_scroll_active || !this.is_dragging {
653 this.auto_scroll_active = false;
654 return false;
655 }
656 cx.notify();
657 true
658 })
659 .unwrap_or(false);
660 if !should_continue {
661 break;
662 }
663 }
664 }).detach();
665 }
666 }
667
668 fn handle_move_left(&mut self, cx: &mut Context<Self>) {
671 self.core.move_left();
672 self.reset_cursor_blink();
673 cx.notify();
674 }
675
676 fn handle_move_right(&mut self, cx: &mut Context<Self>) {
677 self.core.move_right();
678 self.reset_cursor_blink();
679 cx.notify();
680 }
681
682 fn handle_move_word_left(&mut self, cx: &mut Context<Self>) {
683 self.core.move_word_left();
684 self.reset_cursor_blink();
685 cx.notify();
686 }
687
688 fn handle_move_word_right(&mut self, cx: &mut Context<Self>) {
689 self.core.move_word_right();
690 self.reset_cursor_blink();
691 cx.notify();
692 }
693
694 fn handle_move_to_start(&mut self, cx: &mut Context<Self>) {
695 self.core.move_to_start();
696 self.reset_cursor_blink();
697 cx.notify();
698 }
699
700 fn handle_move_to_end(&mut self, cx: &mut Context<Self>) {
701 self.core.move_to_end();
702 self.reset_cursor_blink();
703 cx.notify();
704 }
705
706 fn handle_select_left(&mut self, cx: &mut Context<Self>) {
707 self.core.select_left();
708 self.reset_cursor_blink();
709 cx.notify();
710 }
711
712 fn handle_select_right(&mut self, cx: &mut Context<Self>) {
713 self.core.select_right();
714 self.reset_cursor_blink();
715 cx.notify();
716 }
717
718 fn handle_select_word_left(&mut self, cx: &mut Context<Self>) {
719 self.core.select_word_left();
720 self.reset_cursor_blink();
721 cx.notify();
722 }
723
724 fn handle_select_word_right(&mut self, cx: &mut Context<Self>) {
725 self.core.select_word_right();
726 self.reset_cursor_blink();
727 cx.notify();
728 }
729
730 fn handle_select_to_start(&mut self, cx: &mut Context<Self>) {
731 self.core.select_to_start();
732 self.reset_cursor_blink();
733 cx.notify();
734 }
735
736 fn handle_select_to_end(&mut self, cx: &mut Context<Self>) {
737 self.core.select_to_end();
738 self.reset_cursor_blink();
739 cx.notify();
740 }
741
742 fn handle_select_all(&mut self, cx: &mut Context<Self>) {
743 self.core.select_all();
744 self.reset_cursor_blink();
745 cx.notify();
746 }
747
748 fn handle_delete_backward(&mut self, cx: &mut Context<Self>) {
749 if self.core.delete_backward() {
750 self.reset_cursor_blink();
751 cx.emit(TextInputEvent::Change);
752 cx.notify();
753 }
754 }
755
756 fn handle_delete_forward(&mut self, cx: &mut Context<Self>) {
757 if self.core.delete_forward() {
758 self.reset_cursor_blink();
759 cx.emit(TextInputEvent::Change);
760 cx.notify();
761 }
762 }
763
764 fn handle_delete_word_backward(&mut self, cx: &mut Context<Self>) {
765 if self.core.delete_word_backward() {
766 self.reset_cursor_blink();
767 cx.emit(TextInputEvent::Change);
768 cx.notify();
769 }
770 }
771
772 fn handle_delete_word_forward(&mut self, cx: &mut Context<Self>) {
773 if self.core.delete_word_forward() {
774 self.reset_cursor_blink();
775 cx.emit(TextInputEvent::Change);
776 cx.notify();
777 }
778 }
779
780 fn handle_insert_text(&mut self, text: &str, cx: &mut Context<Self>) {
781 let filtered_text: String = if let Some(ref filter) = self.input_filter {
783 text.chars().filter(|c| filter(*c)).collect()
784 } else {
785 text.to_string()
786 };
787
788 if filtered_text.is_empty() {
789 return;
790 }
791
792 self.core.insert_text(&filtered_text);
793 self.reset_cursor_blink();
794 cx.emit(TextInputEvent::Change);
795 cx.notify();
796 }
797}
798
799impl Render for TextInput {
800 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
801 let theme = get_theme_or(cx, self.custom_theme.as_ref());
802 let focus_handle = self.focus_handle.clone();
803 let is_focused = self.focus_handle.is_focused(window);
804 let display_content = self.display_content().into_owned();
806 let placeholder = self.placeholder.clone();
807 let has_content = !self.core.content().is_empty();
808
809 if !self.focus_out_subscribed {
811 self.focus_out_subscribed = true;
812 let focus_handle = self.focus_handle.clone();
813 cx.on_focus_out(&focus_handle, window, |this: &mut Self, _event, _window, cx| {
814 this.on_blur(cx);
815 }).detach();
816 }
817
818 if is_focused && !self.was_focused {
820 self.on_focus(cx);
821 }
822 self.was_focused = is_focused;
823
824 let render_scroll_offset = if is_focused { self.scroll_offset } else { 0.0 };
826
827 if is_focused && !self.blink_timer_active {
829 self.blink_timer_active = true;
830 let entity = cx.entity();
831 let blink_period = CursorBlink::blink_period();
832 window.spawn(cx, async move |async_cx| {
833 loop {
834 smol::Timer::after(blink_period).await;
835 let should_continue = async_cx
836 .update_entity(&entity, |this, cx| {
837 if !this.blink_timer_active {
838 return false;
839 }
840 cx.notify();
841 true
842 })
843 .unwrap_or(false);
844 if !should_continue {
845 break;
846 }
847 }
848 }).detach();
849 }
850
851 if !is_focused {
852 self.blink_timer_active = false;
853 }
854
855 if self.auto_scroll_active && self.is_dragging {
857 self.apply_auto_scroll(window);
858 }
859
860 if is_focused {
861 self.ensure_cursor_visible(window);
862 }
863
864 let cursor = self.core.cursor();
866 let selection = self.core.selection();
867
868 let cursor_x = self.x_for_cursor(cursor, window) - render_scroll_offset;
869 let cursor_visible = is_focused && self.cursor_blink.is_visible();
870
871 let selection_bounds: Option<(f32, f32)> = selection
873 .filter(|_| is_focused)
874 .filter(|(start, end)| start != end)
875 .map(|(start, end)| {
876 let start_x = self.x_for_cursor(start, window) - render_scroll_offset;
877 let end_x = self.x_for_cursor(end, window) - render_scroll_offset;
878 (start_x, end_x - start_x)
879 });
880
881 let scroll_offset = render_scroll_offset;
882 let enabled = self.enabled;
883 let selection_color = theme.selection;
884 let text_color = if enabled { theme.text_primary } else { theme.disabled_text };
885 let text_placeholder = theme.text_placeholder;
886 let border_focus = theme.border_focus;
887 let border_input = theme.border_input;
888 let bg_input = if enabled { theme.bg_input } else { theme.disabled_bg };
889 let disabled_bg = theme.disabled_bg;
890
891 div()
892 .id("ccf_text_input")
893 .key_context("CcfTextInput")
894 .track_focus(&focus_handle)
895 .tab_stop(enabled)
896 .on_action(cx.listener(|this, _: &MoveLeft, _window, cx| {
898 if !this.enabled { return; }
899 this.handle_move_left(cx);
900 }))
901 .on_action(cx.listener(|this, _: &MoveRight, _window, cx| {
902 if !this.enabled { return; }
903 this.handle_move_right(cx);
904 }))
905 .on_action(cx.listener(|this, _: &MoveWordLeft, _window, cx| {
906 if !this.enabled { return; }
907 this.handle_move_word_left(cx);
908 }))
909 .on_action(cx.listener(|this, _: &MoveWordRight, _window, cx| {
910 if !this.enabled { return; }
911 this.handle_move_word_right(cx);
912 }))
913 .on_action(cx.listener(|this, _: &MoveToStart, _window, cx| {
914 if !this.enabled { return; }
915 this.handle_move_to_start(cx);
916 }))
917 .on_action(cx.listener(|this, _: &MoveToEnd, _window, cx| {
918 if !this.enabled { return; }
919 this.handle_move_to_end(cx);
920 }))
921 .on_action(cx.listener(|this, _: &SelectLeft, _window, cx| {
923 if !this.enabled { return; }
924 this.handle_select_left(cx);
925 }))
926 .on_action(cx.listener(|this, _: &SelectRight, _window, cx| {
927 if !this.enabled { return; }
928 this.handle_select_right(cx);
929 }))
930 .on_action(cx.listener(|this, _: &SelectWordLeft, _window, cx| {
931 if !this.enabled { return; }
932 this.handle_select_word_left(cx);
933 }))
934 .on_action(cx.listener(|this, _: &SelectWordRight, _window, cx| {
935 if !this.enabled { return; }
936 this.handle_select_word_right(cx);
937 }))
938 .on_action(cx.listener(|this, _: &SelectToStart, _window, cx| {
939 if !this.enabled { return; }
940 this.handle_select_to_start(cx);
941 }))
942 .on_action(cx.listener(|this, _: &SelectToEnd, _window, cx| {
943 if !this.enabled { return; }
944 this.handle_select_to_end(cx);
945 }))
946 .on_action(cx.listener(|this, _: &SelectAll, _window, cx| {
947 if !this.enabled { return; }
948 this.handle_select_all(cx);
949 }))
950 .on_action(cx.listener(|this, _: &DeleteBackward, _window, cx| {
952 if !this.enabled { return; }
953 this.handle_delete_backward(cx);
954 }))
955 .on_action(cx.listener(|this, _: &DeleteForward, _window, cx| {
956 if !this.enabled { return; }
957 this.handle_delete_forward(cx);
958 }))
959 .on_action(cx.listener(|this, _: &DeleteWordBackward, _window, cx| {
960 if !this.enabled { return; }
961 this.handle_delete_word_backward(cx);
962 }))
963 .on_action(cx.listener(|this, _: &DeleteWordForward, _window, cx| {
964 if !this.enabled { return; }
965 this.handle_delete_word_forward(cx);
966 }))
967 .on_action(cx.listener(|this, _: &Cut, _window, cx| {
969 if !this.enabled { return; }
970 this.cut(cx);
971 }))
972 .on_action(cx.listener(|this, _: &Copy, _window, cx| {
973 if !this.enabled { return; }
974 this.copy(cx);
975 }))
976 .on_action(cx.listener(|this, _: &Paste, _window, cx| {
977 if !this.enabled { return; }
978 this.paste(cx);
979 }))
980 .on_action(cx.listener(|this, _: &Enter, _window, cx| {
982 if !this.enabled { return; }
983 cx.emit(TextInputEvent::Enter);
984 }))
985 .on_action(cx.listener(|this, _: &Escape, _window, cx| {
986 if !this.enabled { return; }
987 cx.emit(TextInputEvent::Escape);
988 }))
989 .on_action(cx.listener(|this, _: &FocusNext, window, _cx| {
991 if !this.enabled { return; }
992 window.focus_next();
993 }))
994 .on_action(cx.listener(|this, _: &FocusPrev, window, _cx| {
995 if !this.enabled { return; }
996 window.focus_prev();
997 }))
998 .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
1000 if !this.enabled { return; }
1001 if event.keystroke.key == "tab" {
1003 if this.emit_tab_events {
1004 if event.keystroke.modifiers.shift {
1006 cx.emit(TextInputEvent::ShiftTab);
1007 } else {
1008 cx.emit(TextInputEvent::Tab);
1009 }
1010 } else {
1011 if event.keystroke.modifiers.shift {
1013 window.focus_prev();
1014 } else {
1015 window.focus_next();
1016 }
1017 }
1018 return;
1019 }
1020 if !event.keystroke.modifiers.alt
1021 && !event.keystroke.modifiers.control
1022 && !event.keystroke.modifiers.platform
1023 {
1024 if let Some(ref ch) = event.keystroke.key_char {
1025 this.handle_insert_text(ch, cx);
1026 }
1027 }
1028 }))
1029 .when(enabled, |d| {
1031 d.on_mouse_down(MouseButton::Left, cx.listener(|this, event: &MouseDownEvent, window, cx| {
1032 let was_focused = this.focus_handle.is_focused(window);
1033 this.focus_handle.focus(window);
1034
1035 if !was_focused && this.core.selection().is_some() {
1038 this.reset_cursor_blink();
1039 cx.notify();
1040 return;
1041 }
1042
1043 let click_x: f32 = event.position.x.into();
1044 let relative_x = (click_x - this.content_origin_x).max(0.0);
1045 let new_cursor = this.cursor_at_x(relative_x, window);
1046
1047 if event.modifiers.shift {
1048 this.core.start_selection_from_cursor();
1050 this.core.extend_selection_to(new_cursor);
1051 } else {
1052 this.core.clear_selection();
1054 this.core.set_cursor(new_cursor);
1055 this.core.start_selection_from_cursor();
1057 }
1058
1059 this.is_dragging = true;
1061 this.reset_cursor_blink();
1062 cx.notify();
1063 }))
1064 .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, window, cx| {
1066 if !this.is_dragging {
1067 return;
1068 }
1069
1070 let mouse_x: f32 = event.position.x.into();
1071 let scroll_speed = this.handle_drag_move(mouse_x, window);
1072 this.spawn_auto_scroll_timer_if_needed(scroll_speed, window, cx);
1073 cx.notify();
1074 }))
1075 .on_mouse_up(MouseButton::Left, cx.listener(|this, _event: &MouseUpEvent, _window, cx| {
1077 this.stop_drag();
1078 cx.notify();
1079 }))
1080 })
1081 .w_full()
1083 .h(px(28.))
1084 .px_2()
1085 .when(!self.borderless, |d| {
1087 d.border_1()
1088 .border_color(if is_focused { rgb(border_focus) } else { rgb(border_input) })
1089 .rounded_md()
1090 .bg(rgb(bg_input))
1091 })
1092 .when(self.borderless && !enabled, |d| {
1094 d.bg(rgb(disabled_bg))
1095 })
1096 .when(enabled, |d| d.cursor_text())
1097 .when(!enabled, |d| d.cursor_default())
1098 .relative()
1099 .overflow_hidden()
1100 .child({
1101 let entity = cx.entity();
1102 let entity_paint = entity.clone();
1103 let is_dragging = self.is_dragging;
1104
1105 div()
1106 .size_full()
1107 .flex()
1108 .items_center()
1109 .relative()
1110 .child(
1112 canvas(
1113 move |bounds, _window, cx| {
1114 let width: f32 = bounds.size.width.into();
1115 let origin_x: f32 = bounds.origin.x.into();
1116 entity.update(cx, |this: &mut TextInput, _cx| {
1119 this.visible_width = width;
1120 this.content_origin_x = origin_x;
1121 });
1122 },
1123 {
1124 let entity = entity_paint;
1125 move |_bounds, _, window, _cx| {
1126 if is_dragging {
1129 let entity_move = entity.clone();
1131 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
1132 if phase != DispatchPhase::Capture {
1133 return;
1134 }
1135 let mouse_x: f32 = event.position.x.into();
1136 entity_move.update(cx, |this: &mut TextInput, cx| {
1137 let scroll_speed = this.handle_drag_move(mouse_x, window);
1138 this.spawn_auto_scroll_timer_if_needed(scroll_speed, window, cx);
1139 cx.notify();
1140 });
1141 });
1142
1143 let entity_up = entity.clone();
1145 window.on_mouse_event(move |_event: &MouseUpEvent, phase, _window, cx| {
1146 if phase != DispatchPhase::Capture {
1147 return;
1148 }
1149 entity_up.update(cx, |this: &mut TextInput, cx| {
1150 this.stop_drag();
1151 cx.notify();
1152 });
1153 });
1154 }
1155 }
1156 },
1157 )
1158 .size_full()
1159 .absolute()
1160 )
1161 .child(
1163 div()
1164 .relative()
1165 .h_full()
1166 .flex()
1167 .items_center()
1168 .min_w_0()
1169 .when_some(selection_bounds, |d, (start_x, width)| {
1171 d.child(
1172 div()
1173 .absolute()
1174 .top_0()
1175 .bottom_0()
1176 .left(px(start_x))
1177 .w(px(width))
1178 .bg(rgb(selection_color))
1179 )
1180 })
1181 .child(
1183 div()
1184 .absolute()
1185 .left(px(-scroll_offset))
1186 .text_sm()
1187 .text_color(rgb(text_color))
1188 .whitespace_nowrap()
1189 .child(display_content.clone())
1190 )
1191 .when(cursor_visible, |d| {
1193 d.child(
1194 div()
1195 .absolute()
1196 .top(px(4.))
1197 .bottom(px(4.))
1198 .left(px(cursor_x))
1199 .w(px(1.))
1200 .bg(rgb(text_color))
1201 )
1202 })
1203 )
1204 .when(!has_content, |d| {
1206 if let Some(ph) = placeholder {
1207 d.child(
1208 div()
1209 .absolute()
1210 .left_0()
1211 .text_sm()
1212 .text_color(rgb(text_placeholder))
1213 .child(ph)
1214 )
1215 } else {
1216 d
1217 }
1218 })
1219 })
1220 }
1221}