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 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 pub fn h_full(mut self) -> Self {
76 self.height = Some(relative(1.));
77 self
78 }
79
80 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
82 self.height = Some(height.into());
83 self
84 }
85
86 pub fn appearance(mut self, appearance: bool) -> Self {
88 self.appearance = appearance;
89 self
90 }
91
92 pub fn bordered(mut self, bordered: bool) -> Self {
94 self.bordered = bordered;
95 self
96 }
97
98 pub fn focus_bordered(mut self, bordered: bool) -> Self {
100 self.focus_bordered = bordered;
101 self
102 }
103
104 pub fn cleanable(mut self) -> Self {
106 self.cleanable = true;
107 self
108 }
109
110 pub fn mask_toggle(mut self) -> Self {
112 self.mask_toggle = true;
113 self
114 }
115
116 pub fn disabled(mut self, disabled: bool) -> Self {
118 self.disabled = disabled;
119 self
120 }
121
122 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 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 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}