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