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 entity_id(&self) -> gpui::EntityId {
56        dispatch!(self, |state| state.entity_id())
57    }
58
59    pub(crate) fn presentation(&self, cx: &App) -> gpui_base::input::InputPresentation {
60        dispatch!(self, |state| state.read(cx).presentation())
61    }
62
63    /// The text of the input, borrowed from the state that owns it.
64    ///
65    /// The state holds the only copy; nothing on the render path snapshots it.
66    pub(crate) fn text<'a>(&self, cx: &'a App) -> &'a Rope {
67        dispatch!(self, |state| state.read(cx).text())
68    }
69
70    pub(crate) fn focus(&self, window: &mut Window, cx: &mut App) {
71        dispatch!(self, |state| state
72            .update(cx, |state, cx| state.focus(window, cx)))
73    }
74
75    pub(crate) fn clean(&self, window: &mut Window, cx: &mut App) {
76        dispatch!(self, |state| state
77            .update(cx, |state, cx| state.clean(window, cx)))
78    }
79
80    pub(crate) fn toggle_masked(&self, window: &mut Window, cx: &mut App) {
81        dispatch!(self, |state| state
82            .update(cx, |state, cx| state.toggle_masked(window, cx)))
83    }
84
85    pub(crate) fn set_text_align(&self, align: gpui::TextAlign, cx: &mut App) {
86        dispatch!(self, |state| state
87            .update(cx, |state, cx| state.set_text_align(align, cx)))
88    }
89
90    pub(crate) fn set_disabled(&self, disabled: bool, cx: &mut App) {
91        dispatch!(self, |state| state
92            .update(cx, |state, cx| state.set_disabled(disabled, cx)))
93    }
94
95    pub(crate) fn set_readonly(&self, readonly: bool, cx: &mut App) {
96        dispatch!(self, |state| state
97            .update(cx, |state, cx| state.set_readonly(readonly, cx)))
98    }
99
100    pub(crate) fn set_editor_style(&self, style: gpui_base::input::InputEditorStyle, cx: &mut App) {
101        dispatch!(self, |state| state
102            .update(cx, |state, _| state.set_editor_style(style)))
103    }
104
105    pub(crate) fn set_editor_paddings(&self, paddings: gpui::Edges<gpui::Pixels>, cx: &mut App) {
106        dispatch!(self, |state| state
107            .update(cx, |state, _| state.set_editor_paddings(paddings)))
108    }
109
110    pub(crate) fn ensure_highlighter_factory(
111        &self,
112        factory: gpui_base::input::InputHighlighterFactory,
113        cx: &mut App,
114    ) {
115        dispatch!(self, |state| state
116            .update(cx, |state, _| state.ensure_highlighter_factory(factory)))
117    }
118
119    pub(crate) fn on_context_menu(
120        &self,
121        handler: std::rc::Rc<
122            dyn Fn(
123                gpui_base::input::NativeMenu,
124                gpui_base::input::InputContextMenuCapabilities,
125                gpui::Point<gpui::Pixels>,
126                &mut Window,
127                &mut App,
128            ),
129        >,
130        cx: &mut App,
131    ) {
132        dispatch!(self, |state| state
133            .update(cx, |state, _| state.on_context_menu(handler)))
134    }
135
136    /// Builds and syncs this input's overlays. See [`super::overlay`].
137    pub(super) fn render_overlays(
138        &self,
139        window: &mut Window,
140        cx: &mut App,
141    ) -> super::overlay::InputOverlays {
142        dispatch!(self, |state| super::overlay::render_overlays(
143            state, window, cx
144        ))
145    }
146
147    pub(crate) fn replace_all(&self, value: String, window: &mut Window, cx: &mut App) {
148        dispatch!(self, |state| state
149            .update(cx, |state, cx| state.replace_all(value, window, cx)))
150    }
151
152    /// The text element itself, as a child to place in the frame.
153    pub(crate) fn into_any_element(self) -> gpui::AnyElement {
154        use gpui::IntoElement as _;
155        dispatch!(self, |state| state.into_any_element())
156    }
157}
158
159impl From<&TextInputState> for AnyInputState {
160    fn from(state: &TextInputState) -> Self {
161        match state {
162            TextInputState::Input(state) => Self::Input(state.clone()),
163            TextInputState::Textarea(state) => Self::Textarea(state.clone()),
164            TextInputState::Editor(state) => Self::Editor(state.clone()),
165        }
166    }
167}
168
169impl From<Entity<InputState>> for TextInputState {
170    fn from(state: Entity<InputState>) -> Self {
171        Self::Input(state)
172    }
173}
174
175impl From<Entity<TextareaState>> for TextInputState {
176    fn from(state: Entity<TextareaState>) -> Self {
177        Self::Textarea(state)
178    }
179}
180
181impl From<Entity<EditorState>> for TextInputState {
182    fn from(state: Entity<EditorState>) -> Self {
183        Self::Editor(state)
184    }
185}
186
187impl AnyInputState {
188    /// Returns the [`InputState`], if this is an `Input` state.
189    pub fn as_input(&self) -> Option<&Entity<InputState>> {
190        match self {
191            Self::Input(state) => Some(state),
192            _ => None,
193        }
194    }
195
196    /// Returns the [`TextareaState`], if this is a `Textarea` state.
197    pub fn as_textarea(&self) -> Option<&Entity<TextareaState>> {
198        match self {
199            Self::Textarea(state) => Some(state),
200            _ => None,
201        }
202    }
203
204    /// Returns the [`EditorState`], if this is an `Editor` state.
205    pub fn as_editor(&self) -> Option<&Entity<EditorState>> {
206        match self {
207            Self::Editor(state) => Some(state),
208            _ => None,
209        }
210    }
211
212    /// Returns the [`OtpState`], if this is an `OtpInput` state.
213    pub fn as_otp(&self) -> Option<&Entity<OtpState>> {
214        match self {
215            Self::Otp(state) => Some(state),
216            _ => None,
217        }
218    }
219
220    /// Returns the value of the input.
221    ///
222    /// A masked input returns its masked value, the same as what is rendered.
223    pub fn value(&self, cx: &App) -> SharedString {
224        match self {
225            Self::Input(state) => state.read(cx).value(),
226            Self::Textarea(state) => state.read(cx).value(),
227            Self::Editor(state) => state.read(cx).value(),
228            Self::Otp(state) => state.read(cx).value().clone(),
229        }
230    }
231
232    /// Returns the focus handle of the input.
233    pub fn focus_handle(&self, cx: &App) -> FocusHandle {
234        match self {
235            Self::Input(state) => state.focus_handle(cx),
236            Self::Textarea(state) => state.focus_handle(cx),
237            Self::Editor(state) => state.focus_handle(cx),
238            Self::Otp(state) => state.focus_handle(cx),
239        }
240    }
241}
242
243impl From<Entity<InputState>> for AnyInputState {
244    fn from(state: Entity<InputState>) -> Self {
245        Self::Input(state)
246    }
247}
248
249impl From<Entity<TextareaState>> for AnyInputState {
250    fn from(state: Entity<TextareaState>) -> Self {
251        Self::Textarea(state)
252    }
253}
254
255impl From<Entity<EditorState>> for AnyInputState {
256    fn from(state: Entity<EditorState>) -> Self {
257        Self::Editor(state)
258    }
259}
260
261impl From<Entity<OtpState>> for AnyInputState {
262    fn from(state: Entity<OtpState>) -> Self {
263        Self::Otp(state)
264    }
265}
266
267/// Registers `state` as the window's focused input while it holds focus, and
268/// unregisters it once focus moves elsewhere.
269pub(super) fn sync_focused_input_registry(
270    state: impl Into<AnyInputState>,
271    window: &mut Window,
272    cx: &mut App,
273) {
274    let state = state.into();
275    let focused = state.focus_handle(cx).is_focused(window);
276    Root::try_update(window, cx, |root, _, cx| {
277        if focused {
278            root.focused_input = Some(state.clone());
279        } else if root.focused_input.as_ref() == Some(&state) {
280            root.focused_input = None;
281        }
282        cx.notify();
283    });
284}