Skip to main content

gpui_component/input/
state.rs

1use crate::root::WindowState;
2use gpui::{App, Bounds, Entity, FocusHandle, Focusable as _, Pixels, SharedString, Window};
3use gpui_base::OtpState;
4use ropey::Rope;
5
6use super::{EditorState, InputState, TextareaState};
7
8/// Any input-like state, regardless of which input element renders it.
9///
10/// [`InputState`], [`TextareaState`], [`EditorState`] and [`OtpState`] are
11/// separate types, so an API that refers to “whatever input is here” takes this
12/// enum instead of one of them. [`crate::WindowExt::focused_input`] returns it.
13///
14/// Use [`Self::as_input`] and friends to get the concrete state back, or
15/// [`Self::value`] and [`Self::focus_handle`] when the kind does not matter.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AnyInputState {
18    /// A single-line [`crate::input::Input`] state.
19    Input(Entity<InputState>),
20    /// A multi-line [`crate::input::Textarea`] state.
21    Textarea(Entity<TextareaState>),
22    /// A source-code [`crate::input::Editor`] state.
23    Editor(Entity<EditorState>),
24    /// A one-time-code [`crate::input::OtpInput`] state.
25    Otp(Entity<OtpState>),
26}
27
28/// The three states the [`crate::input::Input`] element can render.
29///
30/// [`AnyInputState`] also covers `OtpState`, which is a different engine with
31/// none of these methods, so the element takes this narrower enum instead.
32///
33/// Everything here forwards to a method the shared engine offers for every
34/// mode, which is why the element does not need to be generic over the mode to
35/// call them.
36#[derive(Debug, Clone)]
37pub(crate) enum TextInputState {
38    Input(Entity<InputState>),
39    Textarea(Entity<TextareaState>),
40    Editor(Entity<EditorState>),
41}
42
43/// Runs `$body` against whichever concrete state this is.
44macro_rules! dispatch {
45    ($self:expr, |$state:ident| $body:expr) => {
46        match $self {
47            TextInputState::Input($state) => $body,
48            TextInputState::Textarea($state) => $body,
49            TextInputState::Editor($state) => $body,
50        }
51    };
52}
53
54impl TextInputState {
55    pub(crate) fn has_selection_focus(&self, window: &Window, cx: &App) -> bool {
56        dispatch!(self, |state| state.read(cx).has_selection_focus(window, cx))
57    }
58
59    pub(crate) fn install_token_presentation(
60        &self,
61        renderer: Option<gpui_base::input::InlineTokenRenderer>,
62        listener: Option<gpui_base::input::InlineTokenClickListener>,
63        secret: bool,
64        cx: &mut App,
65    ) {
66        dispatch!(self, |state| state.update(cx, |state, _| state
67            .install_token_presentation(renderer, listener, secret)))
68    }
69
70    pub(crate) fn install_token_hover_presentation(
71        &self,
72        hover_listener: Option<gpui_base::input::InlineTokenHoverListener>,
73        cx: &mut App,
74    ) {
75        dispatch!(self, |state| state.update(cx, |state, _| state
76            .install_token_hover_presentation(hover_listener)))
77    }
78
79    pub(crate) fn entity_id(&self) -> gpui::EntityId {
80        dispatch!(self, |state| state.entity_id())
81    }
82
83    pub(crate) fn presentation(&self, cx: &App) -> gpui_base::input::InputPresentation {
84        dispatch!(self, |state| state.read(cx).presentation())
85    }
86
87    /// The text of the input, borrowed from the state that owns it.
88    ///
89    /// The state holds the only copy; nothing on the render path snapshots it.
90    pub(crate) fn text<'a>(&self, cx: &'a App) -> &'a Rope {
91        dispatch!(self, |state| state.read(cx).text())
92    }
93
94    pub(crate) fn focus(&self, window: &mut Window, cx: &mut App) {
95        dispatch!(self, |state| state
96            .update(cx, |state, cx| state.focus(window, cx)))
97    }
98
99    pub(crate) fn clean(&self, window: &mut Window, cx: &mut App) {
100        dispatch!(self, |state| state
101            .update(cx, |state, cx| state.clean(window, cx)))
102    }
103
104    pub(crate) fn toggle_masked(&self, window: &mut Window, cx: &mut App) {
105        dispatch!(self, |state| state
106            .update(cx, |state, cx| state.toggle_masked(window, cx)))
107    }
108
109    pub(crate) fn set_text_align(&self, align: gpui::TextAlign, cx: &mut App) {
110        dispatch!(self, |state| state
111            .update(cx, |state, cx| state.set_text_align(align, cx)))
112    }
113
114    pub(crate) fn set_disabled(&self, disabled: bool, cx: &mut App) {
115        dispatch!(self, |state| state
116            .update(cx, |state, cx| state.set_disabled(disabled, cx)))
117    }
118
119    pub(crate) fn set_readonly(&self, readonly: bool, cx: &mut App) {
120        dispatch!(self, |state| state
121            .update(cx, |state, cx| state.set_readonly(readonly, cx)))
122    }
123
124    pub(crate) fn set_editor_style(&self, style: gpui_base::input::InputEditorStyle, cx: &mut App) {
125        dispatch!(self, |state| state
126            .update(cx, |state, _| state.set_editor_style(style)))
127    }
128
129    pub(crate) fn set_accessibility(
130        &self,
131        accessibility: gpui_base::input::InputAccessibility,
132        cx: &mut App,
133    ) {
134        dispatch!(self, |state| state
135            .update(cx, |state, _| state.set_accessibility(accessibility)))
136    }
137    pub(crate) fn set_editor_paddings(&self, paddings: gpui::Edges<gpui::Pixels>, cx: &mut App) {
138        dispatch!(self, |state| state
139            .update(cx, |state, _| state.set_editor_paddings(paddings)))
140    }
141
142    pub(crate) fn ensure_highlighter_factory(
143        &self,
144        factory: gpui_base::input::InputHighlighterFactory,
145        cx: &mut App,
146    ) {
147        dispatch!(self, |state| state
148            .update(cx, |state, _| state.ensure_highlighter_factory(factory)))
149    }
150
151    pub(crate) fn on_context_menu(
152        &self,
153        handler: std::rc::Rc<
154            dyn Fn(
155                gpui_base::input::NativeMenu,
156                gpui_base::input::InputContextMenuCapabilities,
157                gpui::Point<gpui::Pixels>,
158                &mut Window,
159                &mut App,
160            ),
161        >,
162        cx: &mut App,
163    ) {
164        dispatch!(self, |state| state
165            .update(cx, |state, _| state.on_context_menu(handler)))
166    }
167
168    /// The selection a long press made, laid out for its handles and menu.
169    pub(crate) fn touch_selection(&self, cx: &App) -> Option<gpui_base::TouchSelectionSnapshot> {
170        dispatch!(self, |state| state.read(cx).touch_selection())
171    }
172
173    pub(crate) fn context_menu_capabilities(
174        &self,
175        cx: &App,
176    ) -> gpui_base::input::InputContextMenuCapabilities {
177        dispatch!(self, |state| state.read(cx).context_menu_capabilities())
178    }
179
180    /// Whether every character is already selected, so Select All has nothing
181    /// left to offer.
182    pub(crate) fn is_all_selected(&self, cx: &App) -> bool {
183        dispatch!(self, |state| {
184            let state = state.read(cx);
185            state.selected_range() == (0..state.text().len())
186        })
187    }
188
189    pub(crate) fn begin_edge_drag(
190        &self,
191        edge: gpui_base::SelectionEdge,
192        finger: gpui::Point<gpui::Pixels>,
193        cx: &mut App,
194    ) {
195        dispatch!(self, |state| state
196            .update(cx, |state, cx| state.begin_edge_drag(edge, finger, cx)))
197    }
198
199    pub(crate) fn update_edge_drag(&self, finger: gpui::Point<gpui::Pixels>, cx: &mut App) {
200        dispatch!(self, |state| state
201            .update(cx, |state, cx| state.update_edge_drag(finger, cx)))
202    }
203
204    pub(crate) fn end_edge_drag(&self, cx: &mut App) {
205        dispatch!(self, |state| state
206            .update(cx, |state, cx| state.end_edge_drag(cx)))
207    }
208
209    pub(crate) fn close_edit_menu(&self, cx: &mut App) {
210        dispatch!(self, |state| state
211            .update(cx, |state, cx| state.close_edit_menu(cx)))
212    }
213
214    pub(crate) fn select_all_from_edit_menu(&self, window: &mut Window, cx: &mut App) {
215        dispatch!(self, |state| state.update(cx, |state, cx| state
216            .select_all_from_edit_menu(window, cx)))
217    }
218
219    /// Builds and syncs this input's overlays. See [`super::overlay`].
220    pub(super) fn render_overlays(
221        &self,
222        window: &mut Window,
223        cx: &mut App,
224    ) -> super::overlay::InputOverlays {
225        dispatch!(self, |state| super::overlay::render_overlays(
226            state, window, cx
227        ))
228    }
229
230    /// The text element itself, as a child to place in the frame.
231    pub(crate) fn into_any_element(self) -> gpui::AnyElement {
232        use gpui::IntoElement as _;
233        dispatch!(self, |state| state.into_any_element())
234    }
235}
236
237impl From<&TextInputState> for AnyInputState {
238    fn from(state: &TextInputState) -> Self {
239        match state {
240            TextInputState::Input(state) => Self::Input(state.clone()),
241            TextInputState::Textarea(state) => Self::Textarea(state.clone()),
242            TextInputState::Editor(state) => Self::Editor(state.clone()),
243        }
244    }
245}
246
247impl From<Entity<InputState>> for TextInputState {
248    fn from(state: Entity<InputState>) -> Self {
249        Self::Input(state)
250    }
251}
252
253impl From<Entity<TextareaState>> for TextInputState {
254    fn from(state: Entity<TextareaState>) -> Self {
255        Self::Textarea(state)
256    }
257}
258
259impl From<Entity<EditorState>> for TextInputState {
260    fn from(state: Entity<EditorState>) -> Self {
261        Self::Editor(state)
262    }
263}
264
265impl AnyInputState {
266    pub(crate) fn input_bounds(&self, cx: &App) -> Option<Bounds<Pixels>> {
267        match self {
268            Self::Input(state) => Some(state.read(cx).input_bounds()),
269            Self::Textarea(state) => Some(state.read(cx).input_bounds()),
270            Self::Editor(state) => Some(state.read(cx).input_bounds()),
271            Self::Otp(_) => None,
272        }
273    }
274
275    pub(crate) fn set_selection_focus(&self, focus: Option<FocusHandle>, cx: &mut App) {
276        match self {
277            Self::Input(state) => {
278                state.update(cx, |state, cx| state.set_selection_focus(focus, cx))
279            }
280            Self::Textarea(state) => {
281                state.update(cx, |state, cx| state.set_selection_focus(focus, cx))
282            }
283            Self::Editor(state) => {
284                state.update(cx, |state, cx| state.set_selection_focus(focus, cx))
285            }
286            Self::Otp(_) => {}
287        }
288    }
289
290    /// Returns the [`InputState`], if this is an `Input` state.
291    pub fn as_input(&self) -> Option<&Entity<InputState>> {
292        match self {
293            Self::Input(state) => Some(state),
294            _ => None,
295        }
296    }
297
298    /// Returns the [`TextareaState`], if this is a `Textarea` state.
299    pub fn as_textarea(&self) -> Option<&Entity<TextareaState>> {
300        match self {
301            Self::Textarea(state) => Some(state),
302            _ => None,
303        }
304    }
305
306    /// Returns the [`EditorState`], if this is an `Editor` state.
307    pub fn as_editor(&self) -> Option<&Entity<EditorState>> {
308        match self {
309            Self::Editor(state) => Some(state),
310            _ => None,
311        }
312    }
313
314    /// Returns the [`OtpState`], if this is an `OtpInput` state.
315    pub fn as_otp(&self) -> Option<&Entity<OtpState>> {
316        match self {
317            Self::Otp(state) => Some(state),
318            _ => None,
319        }
320    }
321
322    /// Returns the value of the input.
323    ///
324    /// A masked input returns its masked value, the same as what is rendered.
325    pub fn value(&self, cx: &App) -> SharedString {
326        match self {
327            Self::Input(state) => state.read(cx).value(),
328            Self::Textarea(state) => state.read(cx).value(),
329            Self::Editor(state) => state.read(cx).value(),
330            Self::Otp(state) => state.read(cx).value().clone(),
331        }
332    }
333
334    /// Returns the focus handle of the input.
335    pub fn focus_handle(&self, cx: &App) -> FocusHandle {
336        match self {
337            Self::Input(state) => state.focus_handle(cx),
338            Self::Textarea(state) => state.focus_handle(cx),
339            Self::Editor(state) => state.focus_handle(cx),
340            Self::Otp(state) => state.focus_handle(cx),
341        }
342    }
343}
344
345impl From<Entity<InputState>> for AnyInputState {
346    fn from(state: Entity<InputState>) -> Self {
347        Self::Input(state)
348    }
349}
350
351impl From<Entity<TextareaState>> for AnyInputState {
352    fn from(state: Entity<TextareaState>) -> Self {
353        Self::Textarea(state)
354    }
355}
356
357impl From<Entity<EditorState>> for AnyInputState {
358    fn from(state: Entity<EditorState>) -> Self {
359        Self::Editor(state)
360    }
361}
362
363impl From<Entity<OtpState>> for AnyInputState {
364    fn from(state: Entity<OtpState>) -> Self {
365        Self::Otp(state)
366    }
367}
368
369/// Registers `state` as the window's focused input while it holds focus, and
370/// unregisters it once focus moves elsewhere.
371pub(super) fn sync_focused_input_registry(
372    state: impl Into<AnyInputState>,
373    window: &mut Window,
374    cx: &mut App,
375) {
376    let state = state.into();
377    let focused = state.focus_handle(cx).is_focused(window);
378    // Every input syncs as it renders; only one that gains or loses focus
379    // changes the registry, and notifying the window state otherwise would
380    // draw the whole window again on every frame an input renders in.
381    let unchanged = WindowState::try_read(window, cx)
382        .is_some_and(|root| (root.focused_input.as_ref() == Some(&state)) == focused);
383    if unchanged {
384        return;
385    }
386    WindowState::try_update(window, cx, |root, _, cx| {
387        if focused {
388            root.focused_input = Some(state.clone());
389        } else if root.focused_input.as_ref() == Some(&state) {
390            root.focused_input = None;
391        }
392        cx.notify();
393    });
394}