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 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 pub fn h_full(mut self) -> Self {
89 self.height = Some(relative(1.));
90 self
91 }
92
93 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
95 self.height = Some(height.into());
96 self
97 }
98
99 pub fn appearance(mut self, appearance: bool) -> Self {
101 self.appearance = appearance;
102 self
103 }
104
105 pub fn bordered(mut self, bordered: bool) -> Self {
107 self.bordered = bordered;
108 self
109 }
110
111 pub fn focus_bordered(mut self, bordered: bool) -> Self {
113 self.focus_bordered = bordered;
114 self
115 }
116
117 pub fn cleanable(mut self) -> Self {
119 self.cleanable = true;
120 self
121 }
122
123 pub fn mask_toggle(mut self) -> Self {
125 self.mask_toggle = true;
126 self
127 }
128
129 pub fn disabled(mut self, disabled: bool) -> Self {
131 self.disabled = disabled;
132 self
133 }
134
135 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 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 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}