Skip to main content

gpui_component/input/
input.rs

1use std::rc::Rc;
2
3use gpui::prelude::FluentBuilder as _;
4use gpui::{
5    AnyElement, App, DefiniteLength, Edges, ElementId, Entity, Global, Hsla,
6    InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Rems, RenderOnce, Role,
7    SharedString, StyleRefinement, Styled, TextAlign, TouchPhase, Window, div, px, relative,
8};
9
10use crate::button::{Button, ButtonRounded, ButtonVariants as _};
11use crate::input::clear_button;
12use crate::native_menu::NativeMenu;
13use crate::spinner::Spinner;
14use crate::touch_selection::{EditMenuItem, TouchSelectionOverlay};
15use crate::{ActiveTheme, Colorize, v_flex};
16use crate::{IconName, Size};
17use crate::{RoleOverride, Selectable, StyledExt, h_flex};
18use crate::{Sizable, StyleSized};
19use gpui_base::InputBase as BaseInput;
20use rust_i18n::t;
21
22use super::state::{TextInputState, sync_focused_input_registry};
23use super::{InputContentType, InputState, sync_native_content_type};
24use crate::ThemeStyled as _;
25
26/// Set when the platform draws the touch selection UI of a focused input.
27#[derive(Default)]
28struct NativeTouchSelection(bool);
29impl Global for NativeTouchSelection {}
30
31/// Let the platform draw the touch selection UI of a focused [`Input`]: the
32/// selection handles and the edit menu a long press leaves behind.
33///
34/// Use it on a host that already shows its own text UI over the window, such
35/// as Android's floating toolbar and drag handles. The long press still
36/// selects a word; the platform follows that selection and drives it back
37/// through the window's input handler. Selections in plain text, outside an
38/// input, keep the handles and menu drawn here.
39pub fn set_native_touch_selection(enabled: bool, cx: &mut App) {
40    cx.set_global(NativeTouchSelection(enabled));
41    cx.refresh_windows();
42}
43
44fn is_native_touch_selection(cx: &App) -> bool {
45    cx.try_global::<NativeTouchSelection>()
46        .is_some_and(|native| native.0)
47}
48
49fn accessibility_role(
50    is_multi_line: bool,
51    content_type: Option<InputContentType>,
52    role: RoleOverride,
53) -> Option<Role> {
54    role.resolve(|| {
55        if is_multi_line {
56            return Role::MultilineTextInput;
57        }
58
59        match content_type {
60            None => Role::TextInput,
61            Some(InputContentType::TelephoneNumber) => Role::PhoneNumberInput,
62            Some(InputContentType::EmailAddress) => Role::EmailInput,
63            Some(InputContentType::Url) => Role::UrlInput,
64            Some(InputContentType::Password | InputContentType::NewPassword) => Role::PasswordInput,
65            Some(InputContentType::DateTime) => Role::DateTimeInput,
66            Some(InputContentType::Birthdate) => Role::DateInput,
67            Some(
68                InputContentType::Name
69                | InputContentType::NamePrefix
70                | InputContentType::GivenName
71                | InputContentType::MiddleName
72                | InputContentType::FamilyName
73                | InputContentType::NameSuffix
74                | InputContentType::Nickname
75                | InputContentType::JobTitle
76                | InputContentType::OrganizationName
77                | InputContentType::Location
78                | InputContentType::FullStreetAddress
79                | InputContentType::StreetAddressLine1
80                | InputContentType::StreetAddressLine2
81                | InputContentType::AddressCity
82                | InputContentType::AddressState
83                | InputContentType::AddressCityAndState
84                | InputContentType::Sublocality
85                | InputContentType::CountryName
86                | InputContentType::PostalCode
87                | InputContentType::CreditCardNumber
88                | InputContentType::CreditCardName
89                | InputContentType::CreditCardGivenName
90                | InputContentType::CreditCardMiddleName
91                | InputContentType::CreditCardFamilyName
92                | InputContentType::CreditCardSecurityCode
93                | InputContentType::CreditCardExpiration
94                | InputContentType::CreditCardExpirationMonth
95                | InputContentType::CreditCardExpirationYear
96                | InputContentType::CreditCardType
97                | InputContentType::Username
98                | InputContentType::OneTimeCode
99                | InputContentType::ShipmentTrackingNumber
100                | InputContentType::FlightNumber
101                | InputContentType::BirthdateDay
102                | InputContentType::BirthdateMonth
103                | InputContentType::BirthdateYear
104                | InputContentType::CellularEid
105                | InputContentType::CellularImei,
106            ) => Role::TextInput,
107        }
108    })
109}
110
111fn exposes_accessibility_value(content_type: Option<InputContentType>) -> bool {
112    !matches!(
113        content_type,
114        Some(InputContentType::Password | InputContentType::NewPassword)
115    )
116}
117
118/// Returns `(background, foreground)` colors for input-like components.
119pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) {
120    if disabled {
121        (
122            cx.theme().input.mix_oklab(cx.theme().transparent, 0.8),
123            cx.theme().muted_foreground,
124        )
125    } else {
126        (cx.theme().input_background(), cx.theme().foreground)
127    }
128}
129
130/// A text input element bind to an [`InputState`].
131#[derive(IntoElement)]
132pub struct Input {
133    token_renderer: Option<gpui_base::input::InlineTokenRenderer>,
134    token_click_listener: Option<gpui_base::input::InlineTokenClickListener>,
135    token_hover_listener: Option<gpui_base::input::InlineTokenHoverListener>,
136    id: Option<ElementId>,
137    state: TextInputState,
138    style: StyleRefinement,
139    size: Size,
140    prefix: Option<AnyElement>,
141    suffix: Option<AnyElement>,
142    height: Option<DefiniteLength>,
143    appearance: bool,
144    cleanable: bool,
145    mask_toggle: bool,
146    disabled: bool,
147    readonly: bool,
148    bordered: bool,
149    focus_bordered: bool,
150    tab_index: isize,
151    selected: bool,
152    content_type: Option<InputContentType>,
153    role: RoleOverride,
154    accessibility_id: Option<SharedString>,
155    aria_label: Option<SharedString>,
156
157    /// An optional context menu builder to allow a custom context menu on the input.
158    ///
159    /// If set, this overrides the built-in context menu.
160    context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
161
162    /// An optional paste handler. If set, it is invoked with the clipboard item
163    /// before the default text insertion. Return `true` if handled.
164    paste_handler: Option<Rc<dyn Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool>>,
165}
166
167impl Sizable for Input {
168    fn with_size(mut self, size: impl Into<Size>) -> Self {
169        self.size = size.into();
170        self
171    }
172}
173
174impl Selectable for Input {
175    fn selected(mut self, selected: bool) -> Self {
176        self.selected = selected;
177        self
178    }
179
180    fn is_selected(&self) -> bool {
181        self.selected
182    }
183}
184
185impl crate::FocusableExt for Input {
186    fn focus_ring(mut self, enabled: bool) -> Self {
187        self.focus_bordered = enabled;
188        self
189    }
190
191    fn is_focus_ring_enabled(&self) -> bool {
192        self.focus_bordered
193    }
194}
195
196impl Input {
197    /// The element each atomic inline token renders as, in place of the default
198    /// [`InputToken`](super::InputToken); editing and history stay
199    /// with the input.
200    pub fn token<R: IntoElement>(
201        mut self,
202        render: impl Fn(&super::InlineTokenContext, &mut Window, &mut App) -> R + 'static,
203    ) -> Self {
204        self.token_renderer = Some(Rc::new(move |token, window, cx| {
205            render(token, window, cx).into_any_element()
206        }));
207        self
208    }
209    /// Open a reference after a completed, unconsumed token click.
210    pub fn on_token_click(
211        mut self,
212        listener: impl Fn(&super::InlineTokenClickEvent, &mut Window, &mut App) + 'static,
213    ) -> Self {
214        self.token_click_listener = Some(Rc::new(listener));
215        self
216    }
217    /// Report pointer presence over a token so the application can show a
218    /// tooltip or run custom logic. Hover never selects or edits.
219    pub fn on_token_hover(
220        mut self,
221        listener: impl Fn(&super::InlineTokenHoverEvent, &mut Window, &mut App) + 'static,
222    ) -> Self {
223        self.token_hover_listener = Some(Rc::new(listener));
224        self
225    }
226
227    /// Sets the GPUI identity of the input frame. By default it uses the state entity ID.
228    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
229        self.id = Some(id.into());
230        self
231    }
232
233    /// Create a new [`Input`] element bind to the [`InputState`].
234    pub fn new(state: &Entity<InputState>) -> Self {
235        Self::with_state(state.clone().into())
236    }
237
238    /// Builds an input renderer around a state of any kind.
239    ///
240    /// `Textarea` and `Editor` render through this. Application code uses
241    /// [`Input::new`], [`super::Textarea`], or [`super::Editor`].
242    pub(crate) fn from_state(state: impl Into<TextInputState>) -> Self {
243        Self::with_state(state.into())
244    }
245
246    fn with_state(state: TextInputState) -> Self {
247        Self {
248            id: None,
249            state,
250            size: Size::default(),
251            style: StyleRefinement::default(),
252            prefix: None,
253            suffix: None,
254            height: None,
255            appearance: true,
256            cleanable: false,
257            mask_toggle: false,
258            disabled: false,
259            readonly: false,
260            bordered: true,
261            focus_bordered: true,
262            tab_index: 0,
263            selected: false,
264            content_type: None,
265            role: RoleOverride::default(),
266            accessibility_id: None,
267            aria_label: None,
268            context_menu_builder: None,
269            paste_handler: None,
270            token_renderer: None,
271            token_click_listener: None,
272            token_hover_listener: None,
273        }
274    }
275
276    /// Set the developer-assigned identifier exposed to accessibility clients.
277    /// The state this input renders; a compound control reads focus and
278    /// presentation from it.
279    pub(crate) fn state(&self) -> &TextInputState {
280        &self.state
281    }
282
283    pub(crate) fn is_disabled(&self) -> bool {
284        self.disabled
285    }
286
287    pub fn accessibility_id(mut self, id: impl Into<SharedString>) -> Self {
288        self.accessibility_id = Some(id.into());
289        self
290    }
291
292    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
293        self.aria_label = Some(label.into());
294        self
295    }
296
297    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
298        self.prefix = Some(prefix.into_any_element());
299        self
300    }
301
302    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
303        self.suffix = Some(suffix.into_any_element());
304        self
305    }
306
307    /// Set full height of the input (Multi-line only).
308    pub fn h_full(mut self) -> Self {
309        self.height = Some(relative(1.));
310        self
311    }
312
313    /// Set height of the input (Multi-line only).
314    pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
315        self.height = Some(height.into());
316        self
317    }
318
319    /// Set the appearance of the input field, if false the input field will no border, background.
320    pub fn appearance(mut self, appearance: bool) -> Self {
321        self.appearance = appearance;
322        self
323    }
324
325    /// Set the bordered for the input, default: true
326    pub fn bordered(mut self, bordered: bool) -> Self {
327        self.bordered = bordered;
328        self
329    }
330
331    /// Set focus border for the input, default is true.
332    pub fn focus_bordered(mut self, bordered: bool) -> Self {
333        self.focus_bordered = bordered;
334        self
335    }
336
337    /// Set whether to show the clear button when the input field is not empty, default is false.
338    pub fn cleanable(mut self, cleanable: bool) -> Self {
339        self.cleanable = cleanable;
340        self
341    }
342
343    /// Set to enable toggle button for password mask state.
344    pub fn mask_toggle(mut self) -> Self {
345        self.mask_toggle = true;
346        self
347    }
348
349    /// Set the semantic content type for password managers and autofill.
350    ///
351    /// This is a component-level semantic hint. It does not change the text
352    /// value or masked rendering state.
353    pub fn content_type(mut self, content_type: InputContentType) -> Self {
354        self.content_type = Some(content_type);
355        self
356    }
357
358    /// Override the accessible role for the input.
359    ///
360    /// If unset, the role is inferred from multi-line mode and content type.
361    pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
362        self.role = role.into();
363        self
364    }
365
366    /// Set to disable the input field.
367    pub fn disabled(mut self, disabled: bool) -> Self {
368        self.disabled = disabled;
369        self
370    }
371
372    /// Set the input field to read-only, default is `false`.
373    ///
374    /// Unlike [`Self::disabled`], a read-only input keeps the normal appearance
375    /// and still can be focused, selected and copied, it only rejects the changes
376    /// made by the user.
377    pub fn readonly(mut self, readonly: bool) -> Self {
378        self.readonly = readonly;
379        self
380    }
381
382    /// Set the tab index for the input, default is 0.
383    pub fn tab_index(mut self, index: isize) -> Self {
384        self.tab_index = index;
385        self
386    }
387
388    /// Sets a custom context menu builder for the input, shown as a native OS menu.
389    ///
390    /// If set, this overrides the built-in right-click context menu. It shows
391    /// only while the state's context menu is enabled, which is the default.
392    pub fn context_menu(
393        mut self,
394        f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
395    ) -> Self {
396        self.context_menu_builder = Some(Rc::new(f));
397        self
398    }
399
400    /// Intercept paste payloads (images, files) before the default text insertion.
401    ///
402    /// The handler receives the clipboard item and returns whether it took the
403    /// paste: `true` stops the `input::Paste` action so the input inserts
404    /// nothing, `false` lets it reach the engine, which inserts
405    /// `clipboard.text()` as today. Text stays in the `Rope`; images and
406    /// copied files (`ClipboardEntry::Image`, `ClipboardEntry::ExternalPaths`)
407    /// belong in app-owned state beside the input (e.g. `Attachment`s), never
408    /// inside it.
409    ///
410    /// Known limit: on web `read_from_clipboard()` is `None`, so the handler
411    /// is skipped there and the input inserts the plain text itself (a
412    /// keyboard paste arrives through the platform input handler, a menu
413    /// paste through the asynchronous clipboard read); image paste there
414    /// needs `read_from_clipboard_async` and permission, out of scope here.
415    pub fn on_paste(
416        mut self,
417        handler: impl Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool + 'static,
418    ) -> Self {
419        self.paste_handler = Some(Rc::new(handler));
420        self
421    }
422
423    /// The handles and the edit menu of the selection a long press made, unless
424    /// the platform draws them; see [`set_native_touch_selection`].
425    ///
426    /// The menu offers what the native context menu would: Cut, Copy, Paste
427    /// and Select All, leaving out what cannot apply right now rather than
428    /// disabling it. Cut, Copy and Paste go through the input's actions, so a
429    /// custom key binding or an open completion menu sees them the same way.
430    fn render_touch_selection(
431        state: &TextInputState,
432        window: &Window,
433        cx: &App,
434    ) -> Vec<AnyElement> {
435        if is_native_touch_selection(cx) || state.touch_selection(cx).is_none() {
436            return Vec::new();
437        }
438        let capabilities = state.context_menu_capabilities(cx);
439        let editable = capabilities.is_editable();
440        let copyable = capabilities.is_copyable();
441        // Offered whenever the text can change, without peeking at the
442        // clipboard: on iOS every read of it shows the system's paste banner,
443        // and an empty clipboard pastes nothing.
444        let pasteable = editable;
445        let selectable = state.text(cx).len() > 0 && !state.is_all_selected(cx);
446        let focus_handle = state.presentation(cx).focus_handle().clone();
447
448        let dispatch = {
449            let focus_handle = focus_handle.clone();
450            move |action: &dyn gpui::Action, window: &mut Window, cx: &mut App| {
451                focus_handle.dispatch_action(action, window, cx);
452            }
453        };
454        let mut items = Vec::with_capacity(4);
455        if editable && copyable {
456            let dispatch = dispatch.clone();
457            items.push(EditMenuItem::new(t!("Input.Cut"), move |window, cx| {
458                dispatch(&gpui_base::input::Cut, window, cx);
459            }));
460        }
461        if copyable {
462            let dispatch = dispatch.clone();
463            let state = state.clone();
464            items.push(EditMenuItem::new(t!("Input.Copy"), move |window, cx| {
465                dispatch(&gpui_base::input::Copy, window, cx);
466                state.close_edit_menu(cx);
467            }));
468        }
469        if pasteable {
470            let dispatch = dispatch.clone();
471            items.push(EditMenuItem::new(t!("Input.Paste"), move |window, cx| {
472                dispatch(&gpui_base::input::Paste, window, cx);
473            }));
474        }
475        if selectable {
476            let state = state.clone();
477            items.push(EditMenuItem::new(
478                t!("Input.Select All"),
479                move |window, cx| state.select_all_from_edit_menu(window, cx),
480            ));
481        }
482
483        let drag_state = state.clone();
484        let source_state = state.clone();
485        TouchSelectionOverlay::new(
486            ("input-touch-selection", state.entity_id()),
487            move |_, cx| source_state.touch_selection(cx),
488        )
489        .handles(move |edge, phase, position, _, cx| match phase {
490            TouchPhase::Started => drag_state.begin_edge_drag(edge, position, cx),
491            TouchPhase::Moved => drag_state.update_edge_drag(position, cx),
492            TouchPhase::Ended | TouchPhase::Cancelled => drag_state.end_edge_drag(cx),
493        })
494        .items(items)
495        .into_elements(window, cx)
496    }
497
498    fn render_toggle_mask_button(state: &TextInputState, cx: &App) -> impl IntoElement {
499        let masked = state.presentation(cx).is_masked();
500        Button::new("toggle-mask")
501            .icon(if masked {
502                IconName::Eye
503            } else {
504                IconName::EyeOff
505            })
506            .accessibility_label(if masked {
507                t!("Input.Show Password")
508            } else {
509                t!("Input.Hide Password")
510            })
511            .xsmall()
512            .text()
513            .tab_stop(false)
514            .on_click({
515                let state = state.clone();
516                move |_, window, cx| state.toggle_masked(window, cx)
517            })
518    }
519
520    /// This method must after the refine_style.
521    fn render_editor(
522        input_state: TextInputState,
523        search_panel: Option<AnyElement>,
524        _: &Window,
525    ) -> impl IntoElement {
526        v_flex().size_full().children(search_panel).child(
527            div()
528                .relative()
529                .flex_1()
530                .child(input_state.into_any_element()),
531        )
532    }
533}
534
535impl Styled for Input {
536    fn style(&mut self) -> &mut StyleRefinement {
537        &mut self.style
538    }
539}
540
541impl RenderOnce for Input {
542    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
543        const LINE_HEIGHT: Rems = Rems(1.25);
544        let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
545        let state = self.state.clone();
546        state.install_token_presentation(
547            Some(self.token_renderer.unwrap_or_else(|| {
548                Rc::new(|token, _, _| super::InputToken::new(token).into_any_element())
549            })),
550            self.token_click_listener,
551            matches!(
552                self.content_type,
553                Some(InputContentType::Password | InputContentType::NewPassword)
554            ),
555            cx,
556        );
557        state.install_token_hover_presentation(self.token_hover_listener, cx);
558        // Which kind of input this registers as follows from the state itself.
559        sync_focused_input_registry(&state, window, cx);
560
561        state.ensure_highlighter_factory(crate::highlighter::input_highlighter_factory(), cx);
562        state.set_editor_style(
563            gpui_base::input::InputEditorStyle {
564                foreground: cx.theme().foreground,
565                muted_foreground: cx.theme().muted_foreground,
566                background: cx.theme().editor_background(),
567                border: cx.theme().border,
568                selection: cx.theme().selection,
569                caret: cx.theme().caret,
570                diagnostics: gpui_base::input::DiagnosticColors {
571                    error: cx.theme().highlight_theme.style.status.error(cx),
572                    warning: cx.theme().highlight_theme.style.status.warning(cx),
573                    info: cx.theme().highlight_theme.style.status.info(cx),
574                    hint: cx.theme().highlight_theme.style.status.hint(cx),
575                },
576                highlight_styles: cx.theme().highlight_theme.clone(),
577                editor_invisible: cx.theme().highlight_theme.style.editor_invisible,
578                editor_active_line: cx.theme().highlight_theme.style.editor_active_line,
579                editor_gutter_background: cx.theme().highlight_theme.style.editor_gutter_background,
580                fold_icon_renderer: Some(Rc::new(|ix, is_folded| {
581                    Button::new(("fold-icon", ix))
582                        .ghost()
583                        .icon(if is_folded {
584                            IconName::ChevronRight
585                        } else {
586                            IconName::ChevronDown
587                        })
588                        .xsmall()
589                        .rounded(ButtonRounded::Small)
590                        .size(px(14.))
591                        .selected(is_folded)
592                        .into_any_element()
593                })),
594            },
595            cx,
596        );
597        state.set_editor_paddings(
598            if state.presentation(cx).is_multi_line() {
599                Edges {
600                    top: self.size.input_py(),
601                    right: self.size.input_px(),
602                    bottom: self.size.input_py(),
603                    left: if state.presentation(cx).is_code_editor() {
604                        self.size.input_px().min(px(6.))
605                    } else {
606                        self.size.input_px()
607                    },
608                }
609            } else {
610                Edges::default()
611            },
612            cx,
613        );
614        state.set_disabled(self.disabled, cx);
615        state.set_readonly(self.readonly, cx);
616        state.set_text_align(text_align, cx);
617        let custom = self.context_menu_builder.clone();
618        state.on_context_menu(
619            Rc::new(move |_, capabilities, position, window, cx| {
620                let menu = if let Some(custom) = custom.as_ref() {
621                    custom(NativeMenu::new(), window, cx)
622                } else {
623                    let enabled = !capabilities.is_disabled();
624                    // A read-only input can still navigate the code, it only
625                    // rejects the items that would change the text.
626                    let editable = enabled && !capabilities.is_readonly();
627                    let mut menu = NativeMenu::new();
628                    if capabilities.is_code_editor() {
629                        menu = menu
630                            .menu_with_disabled(
631                                t!("Input.Go to Definition"),
632                                !(enabled && capabilities.has_definition()),
633                                Box::new(gpui_base::input::GoToDefinition),
634                            )
635                            .menu_with_disabled(
636                                t!("Input.Show Code Actions"),
637                                !(editable && capabilities.has_code_actions()),
638                                Box::new(gpui_base::input::ToggleCodeActions),
639                            )
640                            .separator();
641                    }
642                    menu.menu_with_disabled(
643                        t!("Input.Cut"),
644                        !(editable && capabilities.is_copyable()),
645                        Box::new(gpui_base::input::Cut),
646                    )
647                    .menu_with_disabled(
648                        t!("Input.Copy"),
649                        !capabilities.is_copyable(),
650                        Box::new(gpui_base::input::Copy),
651                    )
652                    // Offered whenever the text can change, without peeking
653                    // at the clipboard: the synchronous read is always empty
654                    // on the web, and an empty clipboard pastes nothing.
655                    .menu_with_disabled(
656                        t!("Input.Paste"),
657                        !editable,
658                        Box::new(gpui_base::input::Paste),
659                    )
660                    .separator()
661                    .menu(
662                        t!("Input.Select All"),
663                        Box::new(gpui_base::input::SelectAll),
664                    )
665                };
666                menu.show(position, window, cx);
667            }),
668            cx,
669        );
670        // The engine ignores `Paste` on a read-only or disabled input; the hook
671        // must not see a paste the input itself would refuse.
672        let paste_handler = self
673            .paste_handler
674            .clone()
675            .filter(|_| state.presentation(cx).is_editable());
676        let mut overlays = state.render_overlays(window, cx);
677        overlays
678            .floating
679            .extend(Self::render_touch_selection(&state, window, cx));
680
681        let presentation = state.presentation(cx);
682        let content_type = self.content_type;
683        let disabled = self.disabled;
684        let is_multi_line = presentation.is_multi_line();
685        // Without a content type, a masked input is a password field to screen
686        // readers, so they announce it as one.
687        let accessibility_role = accessibility_role(
688            is_multi_line,
689            content_type.or(presentation
690                .is_masked()
691                .then_some(InputContentType::Password)),
692            self.role,
693        );
694        let input_focused =
695            presentation.focus_handle().is_focused(window) && !presentation.is_disabled();
696        if input_focused {
697            sync_native_content_type(window, content_type, presentation.is_editable());
698        }
699        let frame_focus_handle = window
700            .use_keyed_state(("input-frame-focus", state.entity_id()), cx, |_, cx| {
701                cx.focus_handle()
702            })
703            .read(cx)
704            .clone();
705        let focused = !presentation.is_disabled()
706            && (state.has_selection_focus(window, cx)
707                || frame_focus_handle.contains_focused(window, cx));
708
709        let gap_x = match self.size {
710            Size::Small => px(4.),
711            Size::Large => px(8.),
712            _ => px(6.),
713        };
714
715        let (bg, _) = input_style(presentation.is_disabled(), cx);
716        let bg = if presentation.is_code_editor() {
717            cx.theme().editor_background()
718        } else {
719            bg
720        };
721        let bg = if presentation.is_disabled() {
722            bg.opacity(0.5)
723        } else {
724            bg
725        };
726        let prefix = self.prefix;
727        let suffix = self.suffix;
728        let show_clear_button = self.cleanable
729            && presentation.is_editable()
730            && !presentation.is_loading()
731            && state.text(cx).len() > 0
732            && !presentation.is_multi_line();
733        let has_suffix =
734            suffix.is_some() || presentation.is_loading() || self.mask_toggle || show_clear_button;
735
736        let placeholder = Some(presentation.placeholder().clone()).filter(|p| !p.is_empty());
737
738        // Don't use a mask-derived placeholder ("(___)___-___") as an aria_label fallback.
739        let placeholder_is_mask = presentation.mask_placeholder() == placeholder.as_deref();
740
741        let aria_label = match self.aria_label {
742            Some(label) => Some(label),
743            None if placeholder_is_mask => None,
744            None => placeholder.clone(),
745        };
746        state.set_accessibility(
747            gpui_base::input::InputAccessibility::new(accessibility_role)
748                .with_label(aria_label)
749                .with_placeholder(placeholder)
750                .with_accessibility_id(self.accessibility_id)
751                .with_value_exposed(exposes_accessibility_value(content_type)),
752            cx,
753        );
754        let id = self
755            .id
756            .unwrap_or_else(|| ("input", state.entity_id()).into());
757        BaseInput::new(id)
758            .semantic_owner(state.entity_id())
759            .focused(focused)
760            .disabled(disabled)
761            .track_focus(&frame_focus_handle)
762            .on_mouse_down(MouseButton::Right, {
763                let state = state.clone();
764                move |_, window, cx| {
765                    // The text element has already taken focus. Publish its state
766                    // before an outer ContextMenu resolves the selection owner;
767                    // render-time registration still names the previous input.
768                    sync_focused_input_registry(&state, window, cx);
769                }
770            })
771            .when(disabled, |this| {
772                this.capture_any_mouse_down(|_, _, cx| cx.stop_propagation())
773            })
774            .styles(|styles| {
775                styles.focused(|style| {
776                    style.when(
777                        self.appearance && self.bordered && self.focus_bordered,
778                        |style| style.border_1().border_color(cx.theme().ring),
779                    )
780                })
781            })
782            // The editor owns text semantics. The frame still scopes addon
783            // controls, custom IDs and focus styling without registering the
784            // editor's FocusHandle a second time.
785            .role(RoleOverride::Presentational)
786            .flex()
787            .size_full()
788            .line_height(LINE_HEIGHT)
789            .when(!is_multi_line, |this| {
790                this.input_px(self.size).input_py(self.size)
791            })
792            .input_h(self.size)
793            .input_text_size(self.size)
794            .items_center()
795            .when(presentation.is_multi_line(), |this| {
796                this.h_auto()
797                    .when_some(self.height, |this, height| this.h(height))
798            })
799            .when(self.appearance, |this| {
800                this.bg(bg)
801                    .rounded(cx.theme().radius)
802                    .when(self.bordered, |this| {
803                        this.border_1().border_color(cx.theme().input)
804                    })
805            })
806            .items_center()
807            .gap(gap_x)
808            .refine_style(&self.style)
809            .when(
810                focused && self.appearance && self.bordered && self.focus_bordered,
811                |this| this.focus_ring_style(window, cx),
812            )
813            .children(prefix.map(|p| {
814                div()
815                    .when(presentation.is_disabled(), |this| this.opacity(0.5))
816                    .child(p)
817            }))
818            .when(presentation.is_multi_line(), |this| {
819                this.child(Self::render_editor(state.clone(), overlays.search, window))
820            })
821            .when(!presentation.is_multi_line(), |this| {
822                this.child(state.clone().into_any_element())
823            })
824            .when(has_suffix, |this| {
825                this.pr(self.size.input_px()).child(
826                    h_flex()
827                        .id("suffix")
828                        .gap(gap_x)
829                        .items_center()
830                        .cursor_default()
831                        .when(presentation.is_disabled(), |this| this.opacity(0.5))
832                        .when(presentation.is_loading(), |this| {
833                            this.child(Spinner::new().color(cx.theme().muted_foreground))
834                        })
835                        .when(self.mask_toggle, |this| {
836                            this.child(Self::render_toggle_mask_button(&state, cx))
837                        })
838                        .when(show_clear_button, |this| {
839                            this.child(clear_button(cx).on_click({
840                                let state = state.clone();
841                                move |_, window, cx| {
842                                    state.clean(window, cx);
843                                    state.focus(window, cx);
844                                }
845                            }))
846                        })
847                        .children(suffix),
848                )
849            })
850            .relative()
851            .children(overlays.floating)
852            .when_some(paste_handler, |this, handler| {
853                this.capture_action(move |_: &gpui_base::input::Paste, window, cx| {
854                    if let Some(clipboard) = cx.read_from_clipboard() {
855                        if handler(&clipboard, window, cx) {
856                            cx.stop_propagation();
857                        }
858                    }
859                })
860            })
861            .render(window, cx)
862    }
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868    use crate::ElementExt as _;
869    use crate::input::AnyInputState;
870
871    #[test]
872    fn content_types_map_to_accessibility_roles() {
873        let cases = [
874            (None, Role::TextInput),
875            (Some(InputContentType::Name), Role::TextInput),
876            (Some(InputContentType::NamePrefix), Role::TextInput),
877            (Some(InputContentType::GivenName), Role::TextInput),
878            (Some(InputContentType::MiddleName), Role::TextInput),
879            (Some(InputContentType::FamilyName), Role::TextInput),
880            (Some(InputContentType::NameSuffix), Role::TextInput),
881            (Some(InputContentType::Nickname), Role::TextInput),
882            (Some(InputContentType::JobTitle), Role::TextInput),
883            (Some(InputContentType::OrganizationName), Role::TextInput),
884            (Some(InputContentType::Location), Role::TextInput),
885            (Some(InputContentType::FullStreetAddress), Role::TextInput),
886            (Some(InputContentType::StreetAddressLine1), Role::TextInput),
887            (Some(InputContentType::StreetAddressLine2), Role::TextInput),
888            (Some(InputContentType::AddressCity), Role::TextInput),
889            (Some(InputContentType::AddressState), Role::TextInput),
890            (Some(InputContentType::AddressCityAndState), Role::TextInput),
891            (Some(InputContentType::Sublocality), Role::TextInput),
892            (Some(InputContentType::CountryName), Role::TextInput),
893            (Some(InputContentType::PostalCode), Role::TextInput),
894            (
895                Some(InputContentType::TelephoneNumber),
896                Role::PhoneNumberInput,
897            ),
898            (Some(InputContentType::EmailAddress), Role::EmailInput),
899            (Some(InputContentType::Url), Role::UrlInput),
900            (Some(InputContentType::CreditCardNumber), Role::TextInput),
901            (Some(InputContentType::CreditCardName), Role::TextInput),
902            (Some(InputContentType::CreditCardGivenName), Role::TextInput),
903            (
904                Some(InputContentType::CreditCardMiddleName),
905                Role::TextInput,
906            ),
907            (
908                Some(InputContentType::CreditCardFamilyName),
909                Role::TextInput,
910            ),
911            (
912                Some(InputContentType::CreditCardSecurityCode),
913                Role::TextInput,
914            ),
915            (
916                Some(InputContentType::CreditCardExpiration),
917                Role::TextInput,
918            ),
919            (
920                Some(InputContentType::CreditCardExpirationMonth),
921                Role::TextInput,
922            ),
923            (
924                Some(InputContentType::CreditCardExpirationYear),
925                Role::TextInput,
926            ),
927            (Some(InputContentType::CreditCardType), Role::TextInput),
928            (Some(InputContentType::Username), Role::TextInput),
929            (Some(InputContentType::Password), Role::PasswordInput),
930            (Some(InputContentType::NewPassword), Role::PasswordInput),
931            (Some(InputContentType::OneTimeCode), Role::TextInput),
932            (
933                Some(InputContentType::ShipmentTrackingNumber),
934                Role::TextInput,
935            ),
936            (Some(InputContentType::FlightNumber), Role::TextInput),
937            (Some(InputContentType::DateTime), Role::DateTimeInput),
938            (Some(InputContentType::Birthdate), Role::DateInput),
939            (Some(InputContentType::BirthdateDay), Role::TextInput),
940            (Some(InputContentType::BirthdateMonth), Role::TextInput),
941            (Some(InputContentType::BirthdateYear), Role::TextInput),
942            (Some(InputContentType::CellularEid), Role::TextInput),
943            (Some(InputContentType::CellularImei), Role::TextInput),
944        ];
945
946        for (content_type, role) in cases {
947            assert_eq!(
948                accessibility_role(false, content_type, RoleOverride::Implicit),
949                Some(role)
950            );
951        }
952    }
953
954    #[test]
955    fn multiline_inputs_keep_multiline_accessibility_role() {
956        assert_eq!(
957            accessibility_role(
958                true,
959                Some(InputContentType::Password),
960                RoleOverride::Implicit
961            ),
962            Some(Role::MultilineTextInput)
963        );
964    }
965
966    #[test]
967    fn explicit_accessibility_role_overrides_defaults() {
968        assert_eq!(
969            accessibility_role(
970                false,
971                Some(InputContentType::Password),
972                Role::TextInput.into()
973            ),
974            Some(Role::TextInput)
975        );
976        assert_eq!(
977            accessibility_role(
978                true,
979                Some(InputContentType::Password),
980                Role::TextInput.into()
981            ),
982            Some(Role::TextInput)
983        );
984    }
985
986    #[test]
987    fn presentational_role_emits_no_accessibility_node() {
988        assert_eq!(
989            accessibility_role(
990                false,
991                Some(InputContentType::Password),
992                RoleOverride::Presentational
993            ),
994            None
995        );
996        assert_eq!(
997            accessibility_role(true, None, RoleOverride::Presentational),
998            None
999        );
1000    }
1001
1002    #[test]
1003    fn role_option_converts_to_the_matching_override() {
1004        assert_eq!(
1005            RoleOverride::from(Some(Role::Button)),
1006            RoleOverride::Role(Role::Button)
1007        );
1008        assert_eq!(RoleOverride::from(None), RoleOverride::Presentational);
1009    }
1010
1011    #[gpui::test]
1012    fn test_on_paste_builder(cx: &mut gpui::TestAppContext) {
1013        use gpui::{AppContext as _, Render};
1014
1015        struct Probe;
1016        impl Render for Probe {
1017            fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1018                div()
1019            }
1020        }
1021
1022        cx.update(crate::init);
1023        let _ = cx.add_window_view(|window, cx| {
1024            let state = cx.new(|cx| InputState::new(window, cx));
1025
1026            assert!(Input::new(&state).paste_handler.is_none());
1027            let input = Input::new(&state).on_paste(|_, _, _| true);
1028            assert!(input.paste_handler.is_some());
1029            Probe
1030        });
1031    }
1032
1033    #[gpui::test]
1034    fn test_on_token_hover_builder(cx: &mut gpui::TestAppContext) {
1035        use gpui::{AppContext as _, Render};
1036
1037        struct Probe;
1038        impl Render for Probe {
1039            fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1040                div()
1041            }
1042        }
1043
1044        cx.update(crate::init);
1045        let _ = cx.add_window_view(|window, cx| {
1046            let state = cx.new(|cx| InputState::new(window, cx));
1047
1048            assert!(Input::new(&state).token_hover_listener.is_none());
1049            let input = Input::new(&state).on_token_hover(|_, _, _| {});
1050            assert!(input.token_hover_listener.is_some());
1051            Probe
1052        });
1053    }
1054
1055    #[gpui::test]
1056    fn editable_input_offers_accessibility_write_action(cx: &mut gpui::TestAppContext) {
1057        use gpui::{AccessibleAction, AppContext as _, Element as _, IntoElement as _, Render};
1058        use std::cell::RefCell;
1059
1060        type EmittedState = Option<(Option<String>, bool)>;
1061
1062        struct InputA11yProbe {
1063            state: Entity<InputState>,
1064            emitted: Rc<RefCell<EmittedState>>,
1065            disabled: bool,
1066            readonly: bool,
1067        }
1068
1069        impl Render for InputA11yProbe {
1070            fn render(
1071                &mut self,
1072                _window: &mut Window,
1073                _cx: &mut gpui::Context<Self>,
1074            ) -> impl IntoElement {
1075                let state = self.state.clone();
1076                let emitted = self.emitted.clone();
1077                div()
1078                    .child(
1079                        Input::new(&state)
1080                            .disabled(self.disabled)
1081                            .readonly(self.readonly),
1082                    )
1083                    .on_prepaint(move |_, window, cx| {
1084                        state.update(cx, |state, cx| {
1085                            let input = state.render(window, cx).into_element();
1086                            let mut node = gpui::accesskit::Node::new(Role::Unknown);
1087                            input.write_a11y_info(&mut node);
1088                            *emitted.borrow_mut() = Some((
1089                                node.value().map(ToOwned::to_owned),
1090                                node.supports_action(AccessibleAction::SetValue),
1091                            ));
1092                        });
1093                    })
1094            }
1095        }
1096
1097        cx.update(crate::init);
1098        let emitted = Rc::new(RefCell::new(None));
1099        let captured = emitted.clone();
1100        let (probe, cx) = cx.add_window_view(move |window, cx| InputA11yProbe {
1101            state: cx.new(|cx| InputState::new(window, cx).default_value("initial")),
1102            emitted,
1103            disabled: false,
1104            readonly: false,
1105        });
1106        cx.update(|window, cx| {
1107            let _ = window.draw(cx);
1108        });
1109        // Normal builds leave the value lazy without an accessibility client;
1110        // test-support reads that same production value path eagerly.
1111        let expected_value = cfg!(feature = "test-support").then(|| "initial".to_owned());
1112        assert_eq!(*captured.borrow(), Some((expected_value.clone(), true)));
1113
1114        let state = probe.read_with(cx, |probe, _| probe.state.clone());
1115        for disabled in [false, true] {
1116            probe.update(cx, |probe, cx| {
1117                probe.disabled = disabled;
1118                probe.readonly = !disabled;
1119                cx.notify();
1120            });
1121            cx.update(|window, cx| window.draw(cx).clear(cx));
1122            assert_eq!(*captured.borrow(), Some((expected_value.clone(), false)));
1123        }
1124        probe.update(cx, |probe, cx| {
1125            probe.disabled = false;
1126            probe.readonly = false;
1127            cx.notify();
1128        });
1129        cx.update(|window, cx| {
1130            state.update(cx, |state, cx| state.set_masked(true, window, cx));
1131            window.draw(cx).clear(cx);
1132        });
1133        assert_eq!(state.read_with(cx, |state, _| state.value()), "initial");
1134        assert_eq!(*captured.borrow(), Some((None, true)));
1135    }
1136
1137    #[gpui::test]
1138    fn input_emits_accessibility_id(cx: &mut gpui::TestAppContext) {
1139        use gpui::{AppContext as _, Element as _, IntoElement as _, Render};
1140        use std::cell::RefCell;
1141
1142        type EmittedIds = Vec<Option<String>>;
1143
1144        struct InputA11yProbe {
1145            states: [Entity<InputState>; 2],
1146            emitted: Rc<RefCell<EmittedIds>>,
1147        }
1148
1149        impl Render for InputA11yProbe {
1150            fn render(
1151                &mut self,
1152                _window: &mut Window,
1153                _cx: &mut gpui::Context<Self>,
1154            ) -> impl IntoElement {
1155                let states = self.states.clone();
1156                let emitted = self.emitted.clone();
1157                div()
1158                    .child(Input::new(&states[0]))
1159                    .child(Input::new(&states[1]).accessibility_id("search.query"))
1160                    .on_prepaint(move |_, window, cx| {
1161                        *emitted.borrow_mut() = states
1162                            .iter()
1163                            .map(|state| {
1164                                state.update(cx, |state, cx| {
1165                                    let input = state.render(window, cx).into_element();
1166                                    let mut node = gpui::accesskit::Node::new(Role::Unknown);
1167                                    input.write_a11y_info(&mut node);
1168                                    node.author_id().map(ToOwned::to_owned)
1169                                })
1170                            })
1171                            .collect();
1172                    })
1173            }
1174        }
1175
1176        cx.update(crate::init);
1177        let emitted = Rc::new(RefCell::new(Vec::new()));
1178        let captured = emitted.clone();
1179        let (_, cx) = cx.add_window_view(move |window, cx| InputA11yProbe {
1180            states: std::array::from_fn(|_| cx.new(|cx| InputState::new(window, cx))),
1181            emitted,
1182        });
1183        cx.update(|window, cx| {
1184            let _ = window.draw(cx);
1185        });
1186
1187        assert_eq!(*captured.borrow(), vec![None, Some("search.query".into())]);
1188    }
1189
1190    #[test]
1191    fn password_content_types_keep_accessibility_values_private() {
1192        assert!(exposes_accessibility_value(None));
1193        assert!(!exposes_accessibility_value(Some(
1194            InputContentType::Password
1195        )));
1196        assert!(!exposes_accessibility_value(Some(
1197            InputContentType::NewPassword
1198        )));
1199    }
1200
1201    #[gpui::test]
1202    fn focused_input_registry_tracks_focus_and_blur(cx: &mut gpui::TestAppContext) {
1203        use crate::{Root, WindowExt as _};
1204        use gpui::{AppContext as _, Render};
1205
1206        struct Probe {
1207            input: Entity<InputState>,
1208            textarea: Entity<crate::input::TextareaState>,
1209            editor: Entity<crate::input::EditorState>,
1210            otp: Entity<gpui_base::OtpState>,
1211            other: gpui::FocusHandle,
1212        }
1213        impl Render for Probe {
1214            fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1215                div()
1216                    .child(div().track_focus(&self.other))
1217                    .child(Input::new(&self.input))
1218                    .child(crate::input::Textarea::new(&self.textarea))
1219                    .child(crate::input::Editor::new(&self.editor))
1220                    .child(crate::input::OtpInput::new(&self.otp))
1221            }
1222        }
1223
1224        cx.update(crate::init);
1225        let mut input = None;
1226        let mut textarea = None;
1227        let mut editor = None;
1228        let mut other_focus = None;
1229        let mut otp = None;
1230        let window = cx.update(|cx| {
1231            cx.open_window(Default::default(), |window, cx| {
1232                let state = cx.new(|cx| InputState::new(window, cx));
1233                let textarea_state = cx.new(|cx| crate::input::TextareaState::new(window, cx));
1234                let editor_state =
1235                    cx.new(|cx| crate::input::EditorState::new(window, cx).language("rust"));
1236                let otp_state = cx.new(|cx| gpui_base::OtpState::new(6, window, cx));
1237                input = Some(state.clone());
1238                textarea = Some(textarea_state.clone());
1239                editor = Some(editor_state.clone());
1240                otp = Some(otp_state.clone());
1241                let other = cx.focus_handle();
1242                other_focus = Some(other.clone());
1243                let probe = cx.new(|_| Probe {
1244                    input: state,
1245                    textarea: textarea_state,
1246                    editor: editor_state,
1247                    otp: otp_state,
1248                    other,
1249                });
1250                cx.new(|cx| Root::new(probe, window, cx))
1251            })
1252            .unwrap()
1253        });
1254        let input = input.unwrap();
1255        let textarea = textarea.unwrap();
1256        let editor = editor.unwrap();
1257        let otp = otp.unwrap();
1258        let other_focus = other_focus.unwrap();
1259        let mut cx = gpui::VisualTestContext::from_window(window.into(), cx);
1260
1261        // Focusing each kind of input registers it, and blurring clears it.
1262        let cases: Vec<AnyInputState> = vec![
1263            input.clone().into(),
1264            textarea.clone().into(),
1265            editor.clone().into(),
1266            otp.clone().into(),
1267        ];
1268        for expected in cases {
1269            cx.update(|window, cx| {
1270                let _ = window.draw(cx);
1271            });
1272            cx.update(|window, cx| expected.focus_handle(cx).focus(window, cx));
1273            cx.run_until_parked();
1274            cx.update(|window, cx| {
1275                let _ = window.draw(cx);
1276            });
1277            assert_eq!(
1278                cx.update(|window, cx| window.focused_input(cx)),
1279                Some(expected)
1280            );
1281
1282            cx.update(|window, cx| other_focus.clone().focus(window, cx));
1283            cx.run_until_parked();
1284            cx.update(|window, cx| {
1285                let _ = window.draw(cx);
1286            });
1287            assert_eq!(cx.update(|window, cx| window.focused_input(cx)), None);
1288        }
1289    }
1290
1291    #[gpui::test]
1292    fn focused_input_registry_ignores_input_removed_while_focused(cx: &mut gpui::TestAppContext) {
1293        use crate::{Root, WindowExt as _};
1294        use gpui::{AppContext as _, Render};
1295
1296        struct Probe {
1297            input: Entity<InputState>,
1298            show_input: bool,
1299            other: gpui::FocusHandle,
1300        }
1301        impl Render for Probe {
1302            fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1303                let base = div().child(div().track_focus(&self.other));
1304                if self.show_input {
1305                    base.child(Input::new(&self.input))
1306                } else {
1307                    base
1308                }
1309            }
1310        }
1311
1312        cx.update(crate::init);
1313        let mut input = None;
1314        let mut other_focus = None;
1315        let mut probe_entity = None;
1316        let window = cx.update(|cx| {
1317            cx.open_window(Default::default(), |window, cx| {
1318                let state = cx.new(|cx| InputState::new(window, cx));
1319                input = Some(state.clone());
1320                let other = cx.focus_handle();
1321                other_focus = Some(other.clone());
1322                let probe = cx.new(|_| Probe {
1323                    input: state,
1324                    show_input: true,
1325                    other,
1326                });
1327                probe_entity = Some(probe.clone());
1328                cx.new(|cx| Root::new(probe, window, cx))
1329            })
1330            .unwrap()
1331        });
1332        let input: AnyInputState = input.unwrap().into();
1333        let other_focus = other_focus.unwrap();
1334        let probe = probe_entity.unwrap();
1335        let mut cx = gpui::VisualTestContext::from_window(window.into(), cx);
1336
1337        cx.update(|window, cx| {
1338            let _ = window.draw(cx);
1339        });
1340        cx.update(|window, cx| input.focus_handle(cx).focus(window, cx));
1341        cx.run_until_parked();
1342        cx.update(|window, cx| {
1343            let _ = window.draw(cx);
1344        });
1345        assert_eq!(
1346            cx.update(|window, cx| window.focused_input(cx)),
1347            Some(input.clone())
1348        );
1349
1350        // Remove the input from the tree while it holds focus and move focus
1351        // elsewhere in the same update (e.g. closing a sidebar containing the
1352        // input) — the input never re-renders to unregister itself.
1353        cx.update(|window, cx| {
1354            probe.update(cx, |probe, cx| {
1355                probe.show_input = false;
1356                cx.notify();
1357            });
1358            other_focus.focus(window, cx);
1359        });
1360        cx.run_until_parked();
1361        cx.update(|window, cx| {
1362            let _ = window.draw(cx);
1363        });
1364        assert!(!cx.update(|window, cx| window.has_focused_input(cx)));
1365        assert_eq!(cx.update(|window, cx| window.focused_input(cx)), None);
1366    }
1367
1368    #[gpui::test]
1369    fn native_touch_selection_leaves_the_handles_and_menu_to_the_platform(
1370        cx: &mut gpui::TestAppContext,
1371    ) {
1372        use gpui::{AppContext as _, LongPressEvent, Render, point};
1373
1374        struct Probe(Entity<InputState>);
1375        impl Render for Probe {
1376            fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1377                div().w(px(300.)).child(Input::new(&self.0))
1378            }
1379        }
1380
1381        cx.update(crate::init);
1382        let (probe, cx) = cx.add_window_view(|window, cx| {
1383            Probe(cx.new(|cx| InputState::new(window, cx).default_value("quick select value")))
1384        });
1385        cx.update(|window, cx| {
1386            let _ = window.draw(cx);
1387        });
1388        let position = point(px(40.), px(16.));
1389        for phase in [TouchPhase::Started, TouchPhase::Ended] {
1390            cx.simulate_event(LongPressEvent {
1391                phase,
1392                start_position: position,
1393                position,
1394            });
1395        }
1396        let state = TextInputState::from(probe.read_with(cx, |probe, _| probe.0.clone()));
1397        let overlay = |cx: &mut gpui::VisualTestContext| {
1398            cx.update(|window, cx| Input::render_touch_selection(&state, window, cx).len())
1399        };
1400        assert!(cx.update(|_, cx| state.touch_selection(cx).is_some()));
1401        assert!(overlay(cx) > 0);
1402
1403        cx.update(|_, cx| set_native_touch_selection(true, cx));
1404        assert!(cx.update(|_, cx| state.touch_selection(cx).is_some()));
1405        assert_eq!(overlay(cx), 0);
1406
1407        cx.update(|_, cx| set_native_touch_selection(false, cx));
1408        assert!(overlay(cx) > 0);
1409    }
1410}