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