Skip to main content

gpui_kit/controls/input/
mod.rs

1//! A single-line editable text control.
2//!
3//! `TextInput` is a view rather than a `RenderOnce` builder, because editing
4//! carries state that outlives a frame: the caret, the selection, the
5//! in-progress input method composition, and the horizontal scroll position.
6//! Callers own the entity and read [`TextInput::value`] from it.
7//!
8//! ```no_run
9//! # use gpui::{App, AppContext as _, Context, Window};
10//! # use gpui_kit::controls::input::{TextInput, TextInputEvent};
11//! # struct Host;
12//! # fn example(window: &mut Window, cx: &mut Context<Host>) {
13//! let input = cx.new(|cx| {
14//!     TextInput::new("settings.token", window, cx)
15//!         .placeholder("sk-...")
16//!         .secret(true)
17//! });
18//! cx.subscribe(&input, |_host, input, event, cx| {
19//!     if let TextInputEvent::Submit = event {
20//!         let _typed = input.read(cx).value().to_string();
21//!     }
22//! })
23//! .detach();
24//! # }
25//! ```
26
27mod 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
78/// The key context every input publishes, so a host can layer its own
79/// bindings on top without re-declaring these.
80pub const KEY_CONTEXT: &str = "TextInput";
81
82/// Installs the editing key bindings.
83///
84/// Called by [`crate::install`]. Bindings are scoped to the input key context,
85/// so they never shadow a host's global shortcuts.
86pub(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/// What an input reports to its owner.
167#[derive(Clone, PartialEq, Eq)]
168pub enum TextInputEvent {
169    /// The text changed, by typing, deletion, paste, or a programmatic set.
170    Change(SharedString),
171    /// The primary key was pressed while the input had focus.
172    Submit,
173    /// Editing was abandoned with the cancel key.
174    Cancel,
175    /// Backspace was pressed with nothing before the caret to delete.
176    ///
177    /// A bound key never reaches an ancestor listener, so a control that
178    /// composes an input — a tag field, where backspace reaches past the
179    /// start of the text — is told here instead.
180    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            // TextInput is also the editor under sensitive controls. Its
189            // payload remains available to the subscriber, but formatting an
190            // event must never turn the payload into an action log.
191            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
206/// One line of editable text.
207///
208/// The field owns the caret, the selection, and any composition in flight; the
209/// committed text belongs to the caller, which is why a host that refuses a
210/// change simply does not apply it and the field keeps showing what is true.
211/// A secret field publishes its shape and never its content.
212pub struct TextInput {
213    ident: Ident,
214    focus_handle: FocusHandle,
215    content: SharedString,
216    placeholder: SharedString,
217    /// What to call the field where nothing on screen already does. A control
218    /// that wraps a bare field owns the visible label, so the field it types
219    /// into has to be told its own name or it reaches a reader unnamed.
220    name: SharedString,
221    /// A caret is an empty selection, so one range describes both.
222    selected_range: Range<usize>,
223    selection_reversed: bool,
224    /// The range the input method is currently composing, which is underlined
225    /// and replaced wholesale as composition continues.
226    marked_range: Option<Range<usize>>,
227    size: ControlSize,
228    disabled: bool,
229    invalid: bool,
230    required: bool,
231    read_only: bool,
232    /// Sensitivity controls every export boundary and never changes while a
233    /// credential is visually revealed.
234    secret: bool,
235    /// Visual masking is deliberately separate from sensitivity. A password
236    /// reveal changes this bit and leaves semantic, accessibility, clipboard,
237    /// and Debug redaction untouched.
238    visually_masked: bool,
239    /// Set when a composing control supplies the frame itself.
240    bare: bool,
241    max_length: Option<usize>,
242    /// Used by segmented sensitive inputs, where one slot means one Unicode
243    /// grapheme rather than one UTF-8 byte.
244    max_graphemes: Option<usize>,
245    /// A custom visual may segment the one editor into this many slots. The
246    /// editor still owns hit testing and IME geometry for the full surface.
247    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    /// Held so the focus listeners live as long as the input does.
255    _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    /// Names the field for a reader without drawing anything. Use it when a
305    /// surrounding control carries the visible label.
306    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
307        self.name = name.into();
308        self
309    }
310
311    /// Names the field after it has been built.
312    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    /// Seeds the initial text, with the caret at the end.
318    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    /// Keeps the value focusable and selectable while refusing editing, IME,
336    /// and accessibility value changes.
337    pub fn read_only(mut self, read_only: bool) -> Self {
338        self.read_only = read_only;
339        self
340    }
341
342    /// Renders as dots and keeps the text out of every snapshot and export.
343    pub fn secret(mut self, secret: bool) -> Self {
344        self.secret = secret;
345        self.visually_masked = secret;
346        self
347    }
348
349    /// Changes only what is painted for a sensitive field.
350    ///
351    /// This is crate-private because a caller should choose a public
352    /// sensitive control rather than assemble an export policy from toggles.
353    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    /// Gives a sensitive editor a segmented visual contract while preserving
359    /// one input handler, focus handle, selection, and composition.
360    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    /// Drops the input's own border and background.
370    ///
371    /// For a control that composes the input with something else — a step
372    /// button, a token list — and draws one [`crate::controls::field::field_shell`]
373    /// around the lot, so a composed field is not two nested frames.
374    pub fn bare(mut self, bare: bool) -> Self {
375        self.bare = bare;
376        self
377    }
378
379    /// Refuses input past a length in bytes of UTF-8.
380    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    /// Replaces the text from the host side, for example when a form resets.
394    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    /// Replaces the text without reporting a change.
416    ///
417    /// For a composing control that is putting its owner's value on screen:
418    /// nobody asked for that text, so reporting it as an edit would send the
419    /// host a change it made itself.
420    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    /// What the element shapes, which is dots for a secret.
510    ///
511    /// The mask is one dot per grapheme so the caret can still be placed
512    /// between characters the typist entered.
513    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    /// Maps a content offset onto the masked text, which has its own byte
521    /// widths, so the caret lands between dots rather than inside one.
522    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    /// The inverse of [`Self::display_offset`], for a hit test on masked text.
611    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        // A secret is never handed to the clipboard, where nothing in this
807        // library controls where it goes next.
808        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        // A single-line control accepts pasted lines as spaces rather than
829        // silently dropping everything after the first newline.
830        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        // A secret publishes its shape, never its text, so a snapshot can
919        // assert that something was typed without carrying the credential.
920        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        // The content is deliberately absent: an input may hold a credential,
942        // and a debug log is not a place for one.
943        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            // GPUI reports this range relative to the composing replacement,
1086            // not the full document. Convert against exactly that replacement
1087            // before offsetting it into the document; converting against the
1088            // already-mutated document can land inside an astral UTF-8 scalar.
1089            .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}