gpui_component/input/
text_input.rs

1use gpui::prelude::FluentBuilder as _;
2use gpui::{
3    div, px, relative, AnyElement, App, DefiniteLength, Edges, EdgesRefinement, Entity,
4    InteractiveElement as _, IntoElement, IsZero, MouseButton, ParentElement as _, Pixels, Rems,
5    RenderOnce, StyleRefinement, Styled, Window,
6};
7
8use crate::button::{Button, ButtonVariants as _};
9use crate::indicator::Indicator;
10use crate::input::clear_button;
11use crate::input::element::{LINE_NUMBER_RIGHT_MARGIN, RIGHT_MARGIN};
12use crate::scroll::Scrollbar;
13use crate::{h_flex, StyledExt};
14use crate::{v_flex, ActiveTheme};
15use crate::{IconName, Size};
16use crate::{Sizable, StyleSized};
17
18use super::InputState;
19
20#[derive(IntoElement)]
21pub struct TextInput {
22    state: Entity<InputState>,
23    style: StyleRefinement,
24    size: Size,
25    prefix: Option<AnyElement>,
26    suffix: Option<AnyElement>,
27    height: Option<DefiniteLength>,
28    appearance: bool,
29    cleanable: bool,
30    mask_toggle: bool,
31    disabled: bool,
32    bordered: bool,
33    focus_bordered: bool,
34    tab_index: isize,
35}
36
37impl Sizable for TextInput {
38    fn with_size(mut self, size: impl Into<Size>) -> Self {
39        self.size = size.into();
40        self
41    }
42}
43
44impl TextInput {
45    /// Create a new [`TextInput`] element bind to the [`InputState`].
46    pub fn new(state: &Entity<InputState>) -> Self {
47        Self {
48            state: state.clone(),
49            size: Size::default(),
50            style: StyleRefinement::default(),
51            prefix: None,
52            suffix: None,
53            height: None,
54            appearance: true,
55            cleanable: false,
56            mask_toggle: false,
57            disabled: false,
58            bordered: true,
59            focus_bordered: true,
60            tab_index: 0,
61        }
62    }
63
64    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
65        self.prefix = Some(prefix.into_any_element());
66        self
67    }
68
69    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
70        self.suffix = Some(suffix.into_any_element());
71        self
72    }
73
74    /// Set full height of the input (Multi-line only).
75    pub fn h_full(mut self) -> Self {
76        self.height = Some(relative(1.));
77        self
78    }
79
80    /// Set height of the input (Multi-line only).
81    pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
82        self.height = Some(height.into());
83        self
84    }
85
86    /// Set the appearance of the input field, if false the input field will no border, background.
87    pub fn appearance(mut self, appearance: bool) -> Self {
88        self.appearance = appearance;
89        self
90    }
91
92    /// Set the bordered for the input, default: true
93    pub fn bordered(mut self, bordered: bool) -> Self {
94        self.bordered = bordered;
95        self
96    }
97
98    /// Set focus border for the input, default is true.
99    pub fn focus_bordered(mut self, bordered: bool) -> Self {
100        self.focus_bordered = bordered;
101        self
102    }
103
104    /// Set true to show the clear button when the input field is not empty.
105    pub fn cleanable(mut self) -> Self {
106        self.cleanable = true;
107        self
108    }
109
110    /// Set to enable toggle button for password mask state.
111    pub fn mask_toggle(mut self) -> Self {
112        self.mask_toggle = true;
113        self
114    }
115
116    /// Set to disable the input field.
117    pub fn disabled(mut self, disabled: bool) -> Self {
118        self.disabled = disabled;
119        self
120    }
121
122    /// Set the tab index for the input, default is 0.
123    pub fn tab_index(mut self, index: isize) -> Self {
124        self.tab_index = index;
125        self
126    }
127
128    fn render_toggle_mask_button(state: Entity<InputState>) -> impl IntoElement {
129        Button::new("toggle-mask")
130            .icon(IconName::Eye)
131            .xsmall()
132            .ghost()
133            .tab_stop(false)
134            .on_mouse_down(MouseButton::Left, {
135                let state = state.clone();
136                move |_, window, cx| {
137                    state.update(cx, |state, cx| {
138                        state.set_masked(false, window, cx);
139                    })
140                }
141            })
142            .on_mouse_up(MouseButton::Left, {
143                let state = state.clone();
144                move |_, window, cx| {
145                    state.update(cx, |state, cx| {
146                        state.set_masked(true, window, cx);
147                    })
148                }
149            })
150    }
151
152    /// This method must after the refine_style.
153    fn render_editor(
154        paddings: EdgesRefinement<DefiniteLength>,
155        input_state: &Entity<InputState>,
156        state: &InputState,
157        window: &Window,
158        _cx: &App,
159    ) -> impl IntoElement {
160        let base_size = window.text_style().font_size;
161        let rem_size = window.rem_size();
162
163        let paddings = Edges {
164            left: paddings
165                .left
166                .map(|v| v.to_pixels(base_size, rem_size))
167                .unwrap_or(px(0.)),
168            right: paddings
169                .right
170                .map(|v| v.to_pixels(base_size, rem_size))
171                .unwrap_or(px(0.)),
172            top: paddings
173                .top
174                .map(|v| v.to_pixels(base_size, rem_size))
175                .unwrap_or(px(0.)),
176            bottom: paddings
177                .bottom
178                .map(|v| v.to_pixels(base_size, rem_size))
179                .unwrap_or(px(0.)),
180        };
181
182        const MIN_SCROLL_PADDING: Pixels = px(2.0);
183
184        v_flex()
185            .size_full()
186            .children(state.search_panel.clone())
187            .child(div().flex_1().child(input_state.clone()).map(|this| {
188                if let Some(last_layout) = state.last_layout.as_ref() {
189                    let left = if last_layout.line_number_width.is_zero() {
190                        px(0.)
191                    } else {
192                        // Align left edge to the Line number.
193                        paddings.left + last_layout.line_number_width - LINE_NUMBER_RIGHT_MARGIN
194                    };
195
196                    let scroll_size = gpui::Size {
197                        width: state.scroll_size.width - left + paddings.right + RIGHT_MARGIN,
198                        height: state.scroll_size.height,
199                    };
200
201                    let scrollbar = if !state.soft_wrap {
202                        Scrollbar::both(&state.scroll_state, &state.scroll_handle)
203                    } else {
204                        Scrollbar::vertical(&state.scroll_state, &state.scroll_handle)
205                    };
206
207                    this.relative().child(
208                        div()
209                            .absolute()
210                            .top(-paddings.top + MIN_SCROLL_PADDING)
211                            .left(left)
212                            .right(-paddings.right + MIN_SCROLL_PADDING)
213                            .bottom(-paddings.bottom + MIN_SCROLL_PADDING)
214                            .child(scrollbar.scroll_size(scroll_size)),
215                    )
216                } else {
217                    this
218                }
219            }))
220    }
221}
222
223impl Styled for TextInput {
224    fn style(&mut self) -> &mut StyleRefinement {
225        &mut self.style
226    }
227}
228
229impl RenderOnce for TextInput {
230    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
231        const LINE_HEIGHT: Rems = Rems(1.25);
232        let font = window.text_style().font();
233        let font_size = window.text_style().font_size.to_pixels(window.rem_size());
234
235        self.state.update(cx, |state, cx| {
236            state.text_wrapper.set_font(font, font_size, cx);
237            state.disabled = self.disabled;
238        });
239
240        let state = self.state.read(cx);
241        let focused = state.focus_handle.is_focused(window);
242        let gap_x = match self.size {
243            Size::Small => px(4.),
244            Size::Large => px(8.),
245            _ => px(4.),
246        };
247
248        let bg = if state.disabled {
249            cx.theme().muted
250        } else {
251            if state.mode.is_code_editor() {
252                cx.theme().editor_background()
253            } else {
254                cx.theme().background
255            }
256        };
257
258        let prefix = self.prefix;
259        let suffix = self.suffix;
260        let show_clear_button =
261            self.cleanable && !state.loading && state.text.len() > 0 && state.mode.is_single_line();
262        let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
263
264        div()
265            .id(("input", self.state.entity_id()))
266            .flex()
267            .key_context(crate::input::CONTEXT)
268            .track_focus(&state.focus_handle.clone())
269            .tab_index(self.tab_index)
270            .when(!state.disabled, |this| {
271                this.on_action(window.listener_for(&self.state, InputState::backspace))
272                    .on_action(window.listener_for(&self.state, InputState::delete))
273                    .on_action(
274                        window.listener_for(&self.state, InputState::delete_to_beginning_of_line),
275                    )
276                    .on_action(window.listener_for(&self.state, InputState::delete_to_end_of_line))
277                    .on_action(window.listener_for(&self.state, InputState::delete_previous_word))
278                    .on_action(window.listener_for(&self.state, InputState::delete_next_word))
279                    .on_action(window.listener_for(&self.state, InputState::enter))
280                    .on_action(window.listener_for(&self.state, InputState::escape))
281                    .on_action(window.listener_for(&self.state, InputState::paste))
282                    .on_action(window.listener_for(&self.state, InputState::cut))
283                    .on_action(window.listener_for(&self.state, InputState::undo))
284                    .on_action(window.listener_for(&self.state, InputState::redo))
285                    .when(state.mode.is_multi_line(), |this| {
286                        this.on_action(window.listener_for(&self.state, InputState::indent_inline))
287                            .on_action(window.listener_for(&self.state, InputState::outdent_inline))
288                            .on_action(window.listener_for(&self.state, InputState::indent_block))
289                            .on_action(window.listener_for(&self.state, InputState::outdent_block))
290                    })
291                    .on_action(
292                        window.listener_for(&self.state, InputState::on_action_toggle_code_actions),
293                    )
294            })
295            .on_action(window.listener_for(&self.state, InputState::left))
296            .on_action(window.listener_for(&self.state, InputState::right))
297            .on_action(window.listener_for(&self.state, InputState::select_left))
298            .on_action(window.listener_for(&self.state, InputState::select_right))
299            .when(state.mode.is_multi_line(), |this| {
300                this.on_action(window.listener_for(&self.state, InputState::up))
301                    .on_action(window.listener_for(&self.state, InputState::down))
302                    .on_action(window.listener_for(&self.state, InputState::select_up))
303                    .on_action(window.listener_for(&self.state, InputState::select_down))
304                    .on_action(window.listener_for(&self.state, InputState::page_up))
305                    .on_action(window.listener_for(&self.state, InputState::page_down))
306                    .on_action(
307                        window.listener_for(&self.state, InputState::on_action_go_to_definition),
308                    )
309            })
310            .on_action(window.listener_for(&self.state, InputState::select_all))
311            .on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))
312            .on_action(window.listener_for(&self.state, InputState::select_to_end_of_line))
313            .on_action(window.listener_for(&self.state, InputState::select_to_previous_word))
314            .on_action(window.listener_for(&self.state, InputState::select_to_next_word))
315            .on_action(window.listener_for(&self.state, InputState::home))
316            .on_action(window.listener_for(&self.state, InputState::end))
317            .on_action(window.listener_for(&self.state, InputState::move_to_start))
318            .on_action(window.listener_for(&self.state, InputState::move_to_end))
319            .on_action(window.listener_for(&self.state, InputState::move_to_previous_word))
320            .on_action(window.listener_for(&self.state, InputState::move_to_next_word))
321            .on_action(window.listener_for(&self.state, InputState::select_to_start))
322            .on_action(window.listener_for(&self.state, InputState::select_to_end))
323            .on_action(window.listener_for(&self.state, InputState::show_character_palette))
324            .on_action(window.listener_for(&self.state, InputState::copy))
325            .on_action(window.listener_for(&self.state, InputState::on_action_search))
326            .on_key_down(window.listener_for(&self.state, InputState::on_key_down))
327            .on_mouse_down(
328                MouseButton::Left,
329                window.listener_for(&self.state, InputState::on_mouse_down),
330            )
331            .on_mouse_down(
332                MouseButton::Right,
333                window.listener_for(&self.state, InputState::on_mouse_down),
334            )
335            .on_mouse_up(
336                MouseButton::Left,
337                window.listener_for(&self.state, InputState::on_mouse_up),
338            )
339            .on_mouse_up(
340                MouseButton::Right,
341                window.listener_for(&self.state, InputState::on_mouse_up),
342            )
343            .on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move))
344            .on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel))
345            .size_full()
346            .line_height(LINE_HEIGHT)
347            .input_px(self.size)
348            .input_py(self.size)
349            .input_h(self.size)
350            .cursor_text()
351            .text_size(font_size)
352            .items_center()
353            .when(state.mode.is_multi_line(), |this| {
354                this.h_auto()
355                    .when_some(self.height, |this, height| this.h(height))
356            })
357            .when(self.appearance, |this| {
358                this.bg(bg)
359                    .rounded(cx.theme().radius)
360                    .when(self.bordered, |this| {
361                        this.border_color(cx.theme().input)
362                            .border_1()
363                            .when(cx.theme().shadow, |this| this.shadow_xs())
364                            .when(focused && self.focus_bordered, |this| {
365                                this.focused_border(cx)
366                            })
367                    })
368            })
369            .items_center()
370            .gap(gap_x)
371            .refine_style(&self.style)
372            .children(prefix)
373            .when(state.mode.is_multi_line(), |mut this| {
374                let paddings = this.style().padding.clone();
375                this.child(Self::render_editor(
376                    paddings,
377                    &self.state,
378                    &state,
379                    window,
380                    cx,
381                ))
382            })
383            .when(!state.mode.is_multi_line(), |this| {
384                this.child(self.state.clone())
385            })
386            .when(has_suffix, |this| {
387                this.pr(self.size.input_px() / 2.).child(
388                    h_flex()
389                        .id("suffix")
390                        .gap(gap_x)
391                        .when(self.appearance, |this| this.bg(bg))
392                        .items_center()
393                        .when(state.loading, |this| {
394                            this.child(Indicator::new().color(cx.theme().muted_foreground))
395                        })
396                        .when(self.mask_toggle, |this| {
397                            this.child(Self::render_toggle_mask_button(self.state.clone()))
398                        })
399                        .when(show_clear_button, |this| {
400                            this.child(clear_button(cx).on_click({
401                                let state = self.state.clone();
402                                move |_, window, cx| {
403                                    state.update(cx, |state, cx| {
404                                        state.clean(window, cx);
405                                        state.focus(window, cx);
406                                    })
407                                }
408                            }))
409                        })
410                        .children(suffix),
411                )
412            })
413    }
414}