Skip to main content

gpui_component/input/
state.rs

1use gpui::{App, Entity, FocusHandle, Focusable as _, SharedString, Window};
2use gpui_base::OtpState;
3use ropey::Rope;
4
5use super::{EditorState, InputState, TextareaState};
6use crate::Root;
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 install_token_presentation(
56        &self,
57        renderer: Option<gpui_base::input::InlineTokenRenderer>,
58        listener: Option<gpui_base::input::InlineTokenClickListener>,
59        secret: bool,
60        cx: &mut App,
61    ) {
62        dispatch!(self, |state| state.update(cx, |state, _| state
63            .install_token_presentation(renderer, listener, secret)))
64    }
65
66    pub(crate) fn entity_id(&self) -> gpui::EntityId {
67        dispatch!(self, |state| state.entity_id())
68    }
69
70    pub(crate) fn presentation(&self, cx: &App) -> gpui_base::input::InputPresentation {
71        dispatch!(self, |state| state.read(cx).presentation())
72    }
73
74    /// The text of the input, borrowed from the state that owns it.
75    ///
76    /// The state holds the only copy; nothing on the render path snapshots it.
77    pub(crate) fn text<'a>(&self, cx: &'a App) -> &'a Rope {
78        dispatch!(self, |state| state.read(cx).text())
79    }
80
81    pub(crate) fn focus(&self, window: &mut Window, cx: &mut App) {
82        dispatch!(self, |state| state
83            .update(cx, |state, cx| state.focus(window, cx)))
84    }
85
86    pub(crate) fn clean(&self, window: &mut Window, cx: &mut App) {
87        dispatch!(self, |state| state
88            .update(cx, |state, cx| state.clean(window, cx)))
89    }
90
91    pub(crate) fn toggle_masked(&self, window: &mut Window, cx: &mut App) {
92        dispatch!(self, |state| state
93            .update(cx, |state, cx| state.toggle_masked(window, cx)))
94    }
95
96    pub(crate) fn set_text_align(&self, align: gpui::TextAlign, cx: &mut App) {
97        dispatch!(self, |state| state
98            .update(cx, |state, cx| state.set_text_align(align, cx)))
99    }
100
101    pub(crate) fn set_disabled(&self, disabled: bool, cx: &mut App) {
102        dispatch!(self, |state| state
103            .update(cx, |state, cx| state.set_disabled(disabled, cx)))
104    }
105
106    pub(crate) fn set_readonly(&self, readonly: bool, cx: &mut App) {
107        dispatch!(self, |state| state
108            .update(cx, |state, cx| state.set_readonly(readonly, cx)))
109    }
110
111    pub(crate) fn set_editor_style(&self, style: gpui_base::input::InputEditorStyle, cx: &mut App) {
112        dispatch!(self, |state| state
113            .update(cx, |state, _| state.set_editor_style(style)))
114    }
115
116    pub(crate) fn set_editor_paddings(&self, paddings: gpui::Edges<gpui::Pixels>, cx: &mut App) {
117        dispatch!(self, |state| state
118            .update(cx, |state, _| state.set_editor_paddings(paddings)))
119    }
120
121    pub(crate) fn ensure_highlighter_factory(
122        &self,
123        factory: gpui_base::input::InputHighlighterFactory,
124        cx: &mut App,
125    ) {
126        dispatch!(self, |state| state
127            .update(cx, |state, _| state.ensure_highlighter_factory(factory)))
128    }
129
130    pub(crate) fn on_context_menu(
131        &self,
132        handler: std::rc::Rc<
133            dyn Fn(
134                gpui_base::input::NativeMenu,
135                gpui_base::input::InputContextMenuCapabilities,
136                gpui::Point<gpui::Pixels>,
137                &mut Window,
138                &mut App,
139            ),
140        >,
141        cx: &mut App,
142    ) {
143        dispatch!(self, |state| state
144            .update(cx, |state, _| state.on_context_menu(handler)))
145    }
146
147    /// The selection a long press made, laid out for its handles and menu.
148    pub(crate) fn touch_selection(&self, cx: &App) -> Option<gpui_base::TouchSelectionSnapshot> {
149        dispatch!(self, |state| state.read(cx).touch_selection())
150    }
151
152    pub(crate) fn context_menu_capabilities(
153        &self,
154        cx: &App,
155    ) -> gpui_base::input::InputContextMenuCapabilities {
156        dispatch!(self, |state| state.read(cx).context_menu_capabilities())
157    }
158
159    /// Whether every character is already selected, so Select All has nothing
160    /// left to offer.
161    pub(crate) fn is_all_selected(&self, cx: &App) -> bool {
162        dispatch!(self, |state| {
163            let state = state.read(cx);
164            state.selected_range() == (0..state.text().len())
165        })
166    }
167
168    pub(crate) fn begin_edge_drag(
169        &self,
170        edge: gpui_base::SelectionEdge,
171        finger: gpui::Point<gpui::Pixels>,
172        cx: &mut App,
173    ) {
174        dispatch!(self, |state| state
175            .update(cx, |state, cx| state.begin_edge_drag(edge, finger, cx)))
176    }
177
178    pub(crate) fn update_edge_drag(&self, finger: gpui::Point<gpui::Pixels>, cx: &mut App) {
179        dispatch!(self, |state| state
180            .update(cx, |state, cx| state.update_edge_drag(finger, cx)))
181    }
182
183    pub(crate) fn end_edge_drag(&self, cx: &mut App) {
184        dispatch!(self, |state| state
185            .update(cx, |state, cx| state.end_edge_drag(cx)))
186    }
187
188    pub(crate) fn close_edit_menu(&self, cx: &mut App) {
189        dispatch!(self, |state| state
190            .update(cx, |state, cx| state.close_edit_menu(cx)))
191    }
192
193    pub(crate) fn select_all_from_edit_menu(&self, window: &mut Window, cx: &mut App) {
194        dispatch!(self, |state| state.update(cx, |state, cx| state
195            .select_all_from_edit_menu(window, cx)))
196    }
197
198    /// Builds and syncs this input's overlays. See [`super::overlay`].
199    pub(super) fn render_overlays(
200        &self,
201        window: &mut Window,
202        cx: &mut App,
203    ) -> super::overlay::InputOverlays {
204        dispatch!(self, |state| super::overlay::render_overlays(
205            state, window, cx
206        ))
207    }
208
209    pub(crate) fn replace_all(&self, value: String, window: &mut Window, cx: &mut App) {
210        dispatch!(self, |state| state
211            .update(cx, |state, cx| state.replace_all(value, window, cx)))
212    }
213
214    /// The text element itself, as a child to place in the frame.
215    pub(crate) fn into_any_element(self) -> gpui::AnyElement {
216        use gpui::IntoElement as _;
217        dispatch!(self, |state| state.into_any_element())
218    }
219}
220
221impl From<&TextInputState> for AnyInputState {
222    fn from(state: &TextInputState) -> Self {
223        match state {
224            TextInputState::Input(state) => Self::Input(state.clone()),
225            TextInputState::Textarea(state) => Self::Textarea(state.clone()),
226            TextInputState::Editor(state) => Self::Editor(state.clone()),
227        }
228    }
229}
230
231impl From<Entity<InputState>> for TextInputState {
232    fn from(state: Entity<InputState>) -> Self {
233        Self::Input(state)
234    }
235}
236
237impl From<Entity<TextareaState>> for TextInputState {
238    fn from(state: Entity<TextareaState>) -> Self {
239        Self::Textarea(state)
240    }
241}
242
243impl From<Entity<EditorState>> for TextInputState {
244    fn from(state: Entity<EditorState>) -> Self {
245        Self::Editor(state)
246    }
247}
248
249impl AnyInputState {
250    /// Returns the [`InputState`], if this is an `Input` state.
251    pub fn as_input(&self) -> Option<&Entity<InputState>> {
252        match self {
253            Self::Input(state) => Some(state),
254            _ => None,
255        }
256    }
257
258    /// Returns the [`TextareaState`], if this is a `Textarea` state.
259    pub fn as_textarea(&self) -> Option<&Entity<TextareaState>> {
260        match self {
261            Self::Textarea(state) => Some(state),
262            _ => None,
263        }
264    }
265
266    /// Returns the [`EditorState`], if this is an `Editor` state.
267    pub fn as_editor(&self) -> Option<&Entity<EditorState>> {
268        match self {
269            Self::Editor(state) => Some(state),
270            _ => None,
271        }
272    }
273
274    /// Returns the [`OtpState`], if this is an `OtpInput` state.
275    pub fn as_otp(&self) -> Option<&Entity<OtpState>> {
276        match self {
277            Self::Otp(state) => Some(state),
278            _ => None,
279        }
280    }
281
282    /// Returns the value of the input.
283    ///
284    /// A masked input returns its masked value, the same as what is rendered.
285    pub fn value(&self, cx: &App) -> SharedString {
286        match self {
287            Self::Input(state) => state.read(cx).value(),
288            Self::Textarea(state) => state.read(cx).value(),
289            Self::Editor(state) => state.read(cx).value(),
290            Self::Otp(state) => state.read(cx).value().clone(),
291        }
292    }
293
294    /// Returns the focus handle of the input.
295    pub fn focus_handle(&self, cx: &App) -> FocusHandle {
296        match self {
297            Self::Input(state) => state.focus_handle(cx),
298            Self::Textarea(state) => state.focus_handle(cx),
299            Self::Editor(state) => state.focus_handle(cx),
300            Self::Otp(state) => state.focus_handle(cx),
301        }
302    }
303}
304
305impl From<Entity<InputState>> for AnyInputState {
306    fn from(state: Entity<InputState>) -> Self {
307        Self::Input(state)
308    }
309}
310
311impl From<Entity<TextareaState>> for AnyInputState {
312    fn from(state: Entity<TextareaState>) -> Self {
313        Self::Textarea(state)
314    }
315}
316
317impl From<Entity<EditorState>> for AnyInputState {
318    fn from(state: Entity<EditorState>) -> Self {
319        Self::Editor(state)
320    }
321}
322
323impl From<Entity<OtpState>> for AnyInputState {
324    fn from(state: Entity<OtpState>) -> Self {
325        Self::Otp(state)
326    }
327}
328
329/// Registers `state` as the window's focused input while it holds focus, and
330/// unregisters it once focus moves elsewhere.
331pub(super) fn sync_focused_input_registry(
332    state: impl Into<AnyInputState>,
333    window: &mut Window,
334    cx: &mut App,
335) {
336    let state = state.into();
337    let focused = state.focus_handle(cx).is_focused(window);
338    Root::try_update(window, cx, |root, _, cx| {
339        if focused {
340            root.focused_input = Some(state.clone());
341        } else if root.focused_input.as_ref() == Some(&state) {
342            root.focused_input = None;
343        }
344        cx.notify();
345    });
346}