Skip to main content

gpui_component/input/
input.rs

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