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