Skip to main content

liora_components/
input.rs

1use gpui::{
2    AnyElement, App, Bounds, Context, Element, ElementId, ElementInputHandler, Entity,
3    EntityInputHandler, FocusHandle, Focusable, GlobalElementId, InspectorElementId, IntoElement,
4    KeyBinding, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, Pixels, Point, Render,
5    ShapedLine, SharedString, Style, TextRun, UTF16Selection, Window, actions, fill, point,
6    prelude::*, px, size,
7};
8use liora_core::Config;
9use liora_icons::Icon;
10use liora_icons_lucide::IconName;
11use std::ops::{Add, Range};
12
13actions!(
14    input,
15    [
16        Backspace,
17        Delete,
18        Left,
19        Right,
20        Home,
21        End,
22        SelectAll,
23        Enter,
24        InputUp,
25        InputDown,
26        Copy,
27        Paste,
28        Cut,
29        SelectLeft,
30        SelectRight,
31        SelectUp,
32        SelectDown,
33        SelectHome,
34        SelectEnd,
35        TogglePassword
36    ]
37);
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum InputType {
41    Text,
42    Password,
43}
44
45pub struct Input {
46    value: SharedString,
47    placeholder: SharedString,
48    disabled: bool,
49    clearable: bool,
50    icon_prefix: Option<IconName>,
51    icon_suffix: Option<IconName>,
52    focus_handle: FocusHandle,
53    selected_range: Range<usize>,
54    selection_reversed: bool,
55    marked_range: Option<Range<usize>>,
56    last_line_layouts: Vec<(ShapedLine, Pixels)>,
57    last_bounds: Option<Bounds<Pixels>>,
58    last_layout_is_masked: bool,
59    cursor_visible: bool,
60    blink_task: Option<gpui::Task<()>>,
61    filter: Option<Box<dyn Fn(&str) -> bool + 'static>>,
62    max_length: Option<usize>,
63    input_type: InputType,
64    password_visible: bool,
65    mask_char: char,
66    prepend: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
67    append: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
68    width: Option<Pixels>,
69    height: Option<Pixels>,
70    pub min_rows: usize,
71    text_align: gpui::TextAlign,
72    on_enter: Option<Box<dyn Fn(&mut Self, &str, &mut Window, &mut Context<Self>) + 'static>>,
73    on_change: Option<Box<dyn Fn(&str, &mut Context<Self>) + 'static>>,
74}
75
76impl Input {
77    pub fn new(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
78        Self {
79            value: value.into(),
80            placeholder: SharedString::default(),
81            disabled: false,
82            clearable: false,
83            icon_prefix: None,
84            icon_suffix: None,
85            focus_handle: cx.focus_handle(),
86            selected_range: 0..0,
87            selection_reversed: false,
88            marked_range: None,
89            last_line_layouts: Vec::new(),
90            last_bounds: None,
91            last_layout_is_masked: false,
92            cursor_visible: true,
93            blink_task: None,
94            filter: None,
95            max_length: None,
96            input_type: InputType::Text,
97            password_visible: false,
98            mask_char: '•',
99            prepend: None,
100            append: None,
101            width: None,
102            height: None,
103            min_rows: 1,
104            text_align: gpui::TextAlign::Left,
105            on_enter: None,
106            on_change: None,
107        }
108    }
109    pub fn placeholder(mut self, p: impl Into<SharedString>) -> Self {
110        self.placeholder = p.into();
111        self
112    }
113    pub fn disabled(mut self, d: bool) -> Self {
114        self.disabled = d;
115        self
116    }
117    pub fn clearable(mut self, c: bool) -> Self {
118        self.clearable = c;
119        self
120    }
121    pub fn icon_prefix(mut self, icon: IconName) -> Self {
122        self.icon_prefix = Some(icon);
123        self
124    }
125    pub fn icon_suffix(mut self, icon: IconName) -> Self {
126        self.icon_suffix = Some(icon);
127        self
128    }
129    pub fn set_icon_suffix(&mut self, icon: Option<IconName>, cx: &mut Context<Self>) {
130        if self.icon_suffix == icon {
131            return;
132        }
133        self.icon_suffix = icon;
134        cx.notify();
135    }
136    pub fn set_clearable(&mut self, clearable: bool, cx: &mut Context<Self>) {
137        if self.clearable == clearable {
138            return;
139        }
140        self.clearable = clearable;
141        cx.notify();
142    }
143    pub fn filter(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
144        self.filter = Some(Box::new(f));
145        self
146    }
147    pub fn max_length(mut self, max: usize) -> Self {
148        self.max_length = Some(max);
149        self
150    }
151    pub fn password(mut self) -> Self {
152        self.input_type = InputType::Password;
153        self
154    }
155    pub fn mask_char(mut self, c: char) -> Self {
156        self.mask_char = c;
157        self
158    }
159    pub fn min_rows(mut self, rows: usize) -> Self {
160        self.min_rows = rows;
161        self
162    }
163    pub fn height(mut self, h: impl Into<Pixels>) -> Self {
164        self.height = Some(h.into());
165        self
166    }
167    pub fn width(mut self, w: impl Into<Pixels>) -> Self {
168        self.width = Some(w.into());
169        self
170    }
171    pub fn width_sm(self) -> Self {
172        self.width(px(96.0))
173    }
174    pub fn set_width(&mut self, w: impl Into<Pixels>, cx: &mut Context<Self>) {
175        self.width = Some(w.into());
176        cx.notify();
177    }
178    pub fn set_width_sm(&mut self, cx: &mut Context<Self>) {
179        self.set_width(px(96.0), cx);
180    }
181    pub fn text_align(mut self, align: gpui::TextAlign) -> Self {
182        self.text_align = align;
183        self
184    }
185    pub fn on_enter(
186        mut self,
187        f: impl Fn(&mut Self, &str, &mut Window, &mut Context<Self>) + 'static,
188    ) -> Self {
189        self.on_enter = Some(Box::new(f));
190        self
191    }
192
193    pub fn set_on_enter(
194        &mut self,
195        f: impl Fn(&mut Self, &str, &mut Window, &mut Context<Self>) + 'static,
196        cx: &mut Context<Self>,
197    ) {
198        self.on_enter = Some(Box::new(f));
199        cx.notify();
200    }
201
202    pub fn on_change(mut self, f: impl Fn(&str, &mut Context<Self>) + 'static) -> Self {
203        self.on_change = Some(Box::new(f));
204        self
205    }
206
207    pub fn set_on_change(&mut self, f: impl Fn(&str, &mut Context<Self>) + 'static) {
208        self.on_change = Some(Box::new(f));
209    }
210
211    pub fn clear_on_change(&mut self) {
212        self.on_change = None;
213    }
214
215    fn emit_change(&mut self, cx: &mut Context<Self>) {
216        if let Some(on_change) = self.on_change.take() {
217            let value = self.value.to_string();
218            on_change(&value, cx);
219            self.on_change = Some(on_change);
220        }
221    }
222
223    pub fn set_placeholder(&mut self, p: impl Into<SharedString>, cx: &mut Context<Self>) {
224        let p = p.into();
225        if self.placeholder == p {
226            return;
227        }
228        self.placeholder = p;
229        cx.notify();
230    }
231
232    pub fn set_disabled(&mut self, d: bool, cx: &mut Context<Self>) {
233        if self.disabled == d {
234            return;
235        }
236        self.disabled = d;
237        cx.notify();
238    }
239
240    pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
241        let value = value.into();
242        if self.value == value {
243            return;
244        }
245        self.value = value;
246        self.selected_range = self.value.len()..self.value.len();
247        cx.notify();
248    }
249
250    pub fn set_min_rows(&mut self, rows: usize, cx: &mut Context<Self>) {
251        self.min_rows = rows;
252        cx.notify();
253    }
254
255    pub fn value(&self) -> SharedString {
256        self.value.clone()
257    }
258
259    pub fn selected_range(&self) -> Range<usize> {
260        self.selected_range.clone()
261    }
262
263    pub fn insert_text(&mut self, text: &str, cx: &mut Context<Self>) {
264        self.internal_replace(text, cx);
265    }
266
267    pub fn indent_selection(&mut self, indent: &str, cx: &mut Context<Self>) {
268        if indent.is_empty() {
269            return;
270        }
271        if self.selected_range.is_empty() {
272            self.internal_replace(indent, cx);
273            return;
274        }
275        self.reindent_selected_lines(indent, true, cx);
276    }
277
278    pub fn outdent_selection(&mut self, indent: &str, cx: &mut Context<Self>) {
279        self.reindent_selected_lines(indent, false, cx);
280    }
281
282    pub fn register_key_bindings(cx: &mut App) {
283        cx.bind_keys([
284            KeyBinding::new("backspace", Backspace, None),
285            KeyBinding::new("delete", Delete, None),
286            KeyBinding::new("left", Left, None),
287            KeyBinding::new("shift-left", SelectLeft, None),
288            KeyBinding::new("right", Right, None),
289            KeyBinding::new("shift-right", SelectRight, None),
290            KeyBinding::new("home", Home, None),
291            KeyBinding::new("shift-home", SelectHome, None),
292            KeyBinding::new("end", End, None),
293            KeyBinding::new("shift-end", SelectEnd, None),
294            KeyBinding::new("cmd-a", SelectAll, None),
295            KeyBinding::new("ctrl-a", SelectAll, None),
296            KeyBinding::new("cmd-c", Copy, None),
297            KeyBinding::new("ctrl-c", Copy, None),
298            KeyBinding::new("cmd-v", Paste, None),
299            KeyBinding::new("ctrl-v", Paste, None),
300            KeyBinding::new("cmd-x", Cut, None),
301            KeyBinding::new("ctrl-x", Cut, None),
302            KeyBinding::new("enter", Enter, None),
303            KeyBinding::new("up", InputUp, None),
304            KeyBinding::new("shift-up", SelectUp, None),
305            KeyBinding::new("down", InputDown, None),
306            KeyBinding::new("shift-down", SelectDown, None),
307        ]);
308    }
309
310    pub fn clear(&mut self, cx: &mut Context<Self>) {
311        self.value = SharedString::default();
312        self.selected_range = 0..0;
313        self.emit_change(cx);
314        cx.notify();
315    }
316
317    fn cursor_offset(&self) -> usize {
318        if self.selection_reversed {
319            self.selected_range.start
320        } else {
321            self.selected_range.end
322        }
323    }
324
325    fn prev_char(&self, offset: usize) -> usize {
326        if offset == 0 {
327            return 0;
328        }
329        let mut p = offset - 1;
330        while p > 0 && !self.value.is_char_boundary(p) {
331            p -= 1;
332        }
333        p
334    }
335    fn next_char(&self, offset: usize) -> usize {
336        if offset >= self.value.len() {
337            return self.value.len();
338        }
339        let mut n = offset + 1;
340        while n < self.value.len() && !self.value.is_char_boundary(n) {
341            n += 1;
342        }
343        n
344    }
345    fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
346        self.selected_range = offset..offset;
347        self.reset_blink(cx);
348    }
349    fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
350        if self.selection_reversed {
351            self.selected_range.start = offset
352        } else {
353            self.selected_range.end = offset
354        }
355        if self.selected_range.end < self.selected_range.start {
356            self.selection_reversed = !self.selection_reversed;
357            self.selected_range = self.selected_range.end..self.selected_range.start;
358        }
359        self.reset_blink(cx);
360    }
361
362    fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
363        if self.selected_range.is_empty() {
364            let p = self.prev_char(self.cursor_offset());
365            if p == self.cursor_offset() {
366                return;
367            }
368            self.select_to(p, cx);
369        }
370        self.internal_replace("", cx);
371    }
372    fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
373        if self.selected_range.is_empty() {
374            let n = self.next_char(self.cursor_offset());
375            if n == self.cursor_offset() {
376                return;
377            }
378            self.select_to(n, cx);
379        }
380        self.internal_replace("", cx);
381    }
382    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
383        self.move_to(self.prev_char(self.cursor_offset()), cx);
384    }
385    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
386        self.select_to(self.prev_char(self.cursor_offset()), cx);
387    }
388    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
389        self.move_to(self.next_char(self.cursor_offset()), cx);
390    }
391    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
392        self.select_to(self.next_char(self.cursor_offset()), cx);
393    }
394    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
395        self.move_to(0, cx);
396    }
397    fn select_home(&mut self, _: &SelectHome, _: &mut Window, cx: &mut Context<Self>) {
398        self.select_to(0, cx);
399    }
400    fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
401        self.move_to(self.value.len(), cx);
402    }
403    fn select_end(&mut self, _: &SelectEnd, _: &mut Window, cx: &mut Context<Self>) {
404        self.select_to(self.value.len(), cx);
405    }
406    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
407        self.selected_range = 0..self.value.len();
408        self.reset_blink(cx);
409    }
410
411    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
412        if !self.selected_range.is_empty()
413            && (self.input_type != InputType::Password || self.password_visible)
414        {
415            let selected_text = self.value[self.selected_range.clone()].to_string();
416            cx.write_to_clipboard(gpui::ClipboardItem::new_string(selected_text));
417        }
418    }
419
420    fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
421        if let Some(clipboard) = cx.read_from_clipboard() {
422            if let Some(text) = clipboard.text() {
423                self.internal_replace(&text, cx);
424            }
425        }
426    }
427
428    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
429        if !self.selected_range.is_empty() {
430            self.copy(&Copy, window, cx);
431            self.internal_replace("", cx);
432        }
433    }
434
435    fn enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Context<Self>) {
436        if let Some(on_enter) = self.on_enter.take() {
437            let value = self.value.to_string();
438            on_enter(self, &value, window, cx);
439            self.on_enter = Some(on_enter);
440        } else {
441            self.internal_replace("\n", cx);
442        }
443    }
444
445    fn up(&mut self, _: &InputUp, _: &mut Window, cx: &mut Context<Self>) {
446        self.move_vertical(-1, false, cx);
447    }
448    fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
449        self.move_vertical(-1, true, cx);
450    }
451    fn down(&mut self, _: &InputDown, _: &mut Window, cx: &mut Context<Self>) {
452        self.move_vertical(1, false, cx);
453    }
454    fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
455        self.move_vertical(1, true, cx);
456    }
457
458    fn toggle_password(&mut self, _: &TogglePassword, _: &mut Window, cx: &mut Context<Self>) {
459        self.password_visible = !self.password_visible;
460        cx.notify();
461    }
462
463    fn move_vertical(&mut self, delta: isize, select: bool, cx: &mut Context<Self>) {
464        let text = self.value.clone();
465        let offset = self.cursor_offset();
466        let lines: Vec<&str> = text.split('\n').collect();
467        let mut current_line = 0;
468        let mut line_start = 0;
469        for (i, line) in lines.iter().enumerate() {
470            if offset >= line_start && offset <= line_start + line.len() {
471                current_line = i;
472                break;
473            }
474            line_start += line.len() + 1;
475        }
476
477        let col = offset - line_start;
478        let target_line = (current_line as isize + delta).max(0) as usize;
479        if target_line >= lines.len() {
480            return;
481        }
482
483        let mut target_start = 0;
484        for i in 0..target_line {
485            target_start += lines[i].len() + 1;
486        }
487        let target_len = lines[target_line].len();
488        let new_col = col.min(target_len);
489        let new_offset = target_start + new_col;
490        if select {
491            self.select_to(new_offset, cx);
492        } else {
493            self.move_to(new_offset, cx);
494        }
495    }
496
497    fn index_for_point(&self, pt: Point<Pixels>, window: &Window) -> usize {
498        if let (Some(bounds), layouts) = (self.last_bounds.as_ref(), &self.last_line_layouts) {
499            if layouts.is_empty() {
500                return 0;
501            }
502            let line_height = window.line_height();
503            let mut best_line = 0;
504            let mut final_original_byte_offset = 0;
505            let mut current_original_byte_offset = 0;
506            for (i, (_layout, y_offset)) in layouts.iter().enumerate() {
507                if pt.y >= *y_offset && pt.y < *y_offset + line_height {
508                    best_line = i;
509                    final_original_byte_offset = current_original_byte_offset;
510                    break;
511                }
512                if pt.y >= *y_offset {
513                    best_line = i;
514                    final_original_byte_offset = current_original_byte_offset;
515                }
516                current_original_byte_offset += self
517                    .value
518                    .split('\n')
519                    .nth(i)
520                    .map(|l| l.len() + 1)
521                    .unwrap_or(0);
522            }
523            let x = pt.x - bounds.left();
524            let display_index = layouts[best_line]
525                .0
526                .index_for_x(x)
527                .unwrap_or(layouts[best_line].0.len);
528
529            if self.last_layout_is_masked {
530                let char_count = display_index / self.mask_char.len_utf8();
531                let original_line = self.value.split('\n').nth(best_line).unwrap_or("");
532                let mut byte_idx = 0;
533                for _ in 0..char_count {
534                    if byte_idx >= original_line.len() {
535                        break;
536                    }
537                    let Some(ch) = original_line[byte_idx..].chars().next() else {
538                        break;
539                    };
540                    byte_idx += ch.len_utf8();
541                }
542                final_original_byte_offset + byte_idx
543            } else {
544                final_original_byte_offset + display_index
545            }
546        } else {
547            self.value.len()
548        }
549    }
550
551    fn on_mouse_down(
552        &mut self,
553        event: &MouseDownEvent,
554        window: &mut Window,
555        cx: &mut Context<Self>,
556    ) {
557        window.focus(&self.focus_handle);
558        if self.value.is_empty() {
559            self.move_to(0, cx);
560            return;
561        }
562        let idx = self.index_for_point(event.position, window);
563        match event.click_count {
564            1 => {
565                if event.modifiers.shift {
566                    self.select_to(idx, cx);
567                } else {
568                    self.move_to(idx, cx);
569                }
570            }
571            2 => {
572                let range = self.word_range_at(idx);
573                self.selected_range = range;
574                self.selection_reversed = false;
575                self.reset_blink(cx);
576            }
577            3 => {
578                self.selected_range = 0..self.value.len();
579                self.selection_reversed = false;
580                self.reset_blink(cx);
581            }
582            _ => {}
583        }
584    }
585
586    fn word_range_at(&self, idx: usize) -> Range<usize> {
587        let text = self.value.as_ref();
588        if text.is_empty() {
589            return 0..0;
590        }
591        let idx = idx.min(text.len());
592        let mut start = idx;
593        while start > 0 {
594            let prev = self.prev_char(start);
595            let Some(c) = text[prev..start].chars().next() else {
596                break;
597            };
598            if !c.is_alphanumeric() && c != '_' {
599                break;
600            }
601            start = prev;
602        }
603        let mut end = idx;
604        while end < text.len() {
605            let next = self.next_char(end);
606            let Some(c) = text[end..next].chars().next() else {
607                break;
608            };
609            if !c.is_alphanumeric() && c != '_' {
610                break;
611            }
612            end = next;
613        }
614        start..end
615    }
616
617    fn on_mouse_move(
618        &mut self,
619        event: &MouseMoveEvent,
620        window: &mut Window,
621        cx: &mut Context<Self>,
622    ) {
623        if event.pressed_button == Some(MouseButton::Left) {
624            let idx = self.index_for_point(event.position, window);
625            self.select_to(idx, cx);
626        }
627    }
628
629    fn start_blink(&mut self, cx: &mut Context<Self>) {
630        self.cursor_visible = true;
631        let executor = cx.background_executor().clone();
632        self.blink_task = Some(cx.spawn(async move |this, cx| {
633            loop {
634                executor.timer(std::time::Duration::from_millis(500)).await;
635                let res = this.update(cx, |this, cx| {
636                    this.cursor_visible = !this.cursor_visible;
637                    cx.notify();
638                });
639                if res.is_err() {
640                    break;
641                }
642            }
643        }));
644    }
645
646    fn reset_blink(&mut self, cx: &mut Context<Self>) {
647        self.cursor_visible = true;
648        self.start_blink(cx);
649        cx.notify();
650    }
651
652    fn internal_replace(&mut self, new_text: &str, cx: &mut Context<Self>) {
653        let mut v = self.value.to_string();
654        let range = self.selected_range.clone();
655        let potential_v = {
656            let mut temp = v.clone();
657            temp.replace_range(range.clone(), new_text);
658            temp
659        };
660        if let Some(ref filter) = self.filter {
661            if !filter(&potential_v) {
662                return;
663            }
664        }
665        if let Some(max) = self.max_length {
666            if potential_v.chars().count() > max {
667                return;
668            }
669        }
670        v.replace_range(range, new_text);
671        self.value = SharedString::from(v);
672        let pos = self.selected_range.start + new_text.len();
673        self.selected_range = pos..pos;
674        self.emit_change(cx);
675        self.reset_blink(cx);
676    }
677
678    fn apply_value_with_selection(
679        &mut self,
680        next_value: String,
681        next_selection: Range<usize>,
682        cx: &mut Context<Self>,
683    ) {
684        if let Some(ref filter) = self.filter {
685            if !filter(&next_value) {
686                return;
687            }
688        }
689        if let Some(max) = self.max_length {
690            if next_value.chars().count() > max {
691                return;
692            }
693        }
694        self.value = SharedString::from(next_value);
695        self.selected_range =
696            next_selection.start.min(self.value.len())..next_selection.end.min(self.value.len());
697        self.selection_reversed = false;
698        self.emit_change(cx);
699        self.reset_blink(cx);
700    }
701
702    fn reindent_selected_lines(&mut self, indent: &str, indenting: bool, cx: &mut Context<Self>) {
703        let value = self.value.to_string();
704        let selection = self.selected_range.clone();
705        let line_start = line_start_at(&value, selection.start);
706        let line_end = selected_line_end(&value, selection.clone());
707        let mut changed = false;
708        let mut next = String::with_capacity(value.len() + indent.len() * 4);
709        next.push_str(&value[..line_start]);
710
711        let mut selection_start_delta = 0isize;
712        let mut selection_end_delta = 0isize;
713        let mut cursor = line_start;
714
715        for line in value[line_start..line_end].split_inclusive('\n') {
716            let line_abs_start = cursor;
717            let (line_body, line_ending) = line
718                .strip_suffix('\n')
719                .map_or((line, ""), |body| (body, "\n"));
720            if indenting {
721                next.push_str(indent);
722                next.push_str(line_body);
723                next.push_str(line_ending);
724                changed = true;
725                if line_abs_start <= selection.start {
726                    selection_start_delta += indent.len() as isize;
727                }
728                if line_abs_start < selection.end || selection.is_empty() {
729                    selection_end_delta += indent.len() as isize;
730                }
731            } else if let Some(remove_len) = removable_indent_len(line_body, indent) {
732                next.push_str(&line_body[remove_len..]);
733                next.push_str(line_ending);
734                changed = true;
735                if line_abs_start < selection.start {
736                    selection_start_delta -= remove_len as isize;
737                }
738                if line_abs_start < selection.end {
739                    selection_end_delta -= remove_len as isize;
740                }
741            } else {
742                next.push_str(line_body);
743                next.push_str(line_ending);
744            }
745            cursor += line.len();
746        }
747
748        if !changed {
749            return;
750        }
751
752        next.push_str(&value[line_end..]);
753        let start = apply_signed_delta(selection.start, selection_start_delta);
754        let end = apply_signed_delta(selection.end, selection_end_delta).max(start);
755        self.apply_value_with_selection(next, start..end, cx);
756    }
757
758    fn is_password(&self) -> bool {
759        self.input_type == InputType::Password && !self.password_visible
760    }
761
762    pub fn prepend(
763        mut self,
764        render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
765    ) -> Self {
766        self.prepend = Some(Box::new(render));
767        self
768    }
769
770    pub fn prepend_text(self, text: impl Into<SharedString>) -> Self {
771        let text = text.into();
772        self.prepend(move |_, _| {
773            gpui::div()
774                .flex()
775                .items_center()
776                .px_3()
777                .child(text.clone())
778                .into_any_element()
779        })
780    }
781
782    pub fn prepend_icon(self, icon: IconName) -> Self {
783        self.prepend(move |_, _| {
784            gpui::div()
785                .flex()
786                .items_center()
787                .justify_center()
788                .px_3()
789                .child(Icon::new(icon).size(px(14.0)))
790                .into_any_element()
791        })
792    }
793
794    pub fn append(
795        mut self,
796        render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
797    ) -> Self {
798        self.append = Some(Box::new(render));
799        self
800    }
801
802    pub fn append_text(self, text: impl Into<SharedString>) -> Self {
803        let text = text.into();
804        self.append(move |_, _| {
805            gpui::div()
806                .flex()
807                .items_center()
808                .px_3()
809                .child(text.clone())
810                .into_any_element()
811        })
812    }
813}
814
815impl Focusable for Input {
816    fn focus_handle(&self, _cx: &App) -> FocusHandle {
817        self.focus_handle.clone()
818    }
819}
820
821impl EntityInputHandler for Input {
822    fn text_for_range(
823        &mut self,
824        range_utf16: Range<usize>,
825        _: &mut Option<Range<usize>>,
826        _: &mut Window,
827        _: &mut Context<Self>,
828    ) -> Option<String> {
829        let start = self.offset_from_utf16(range_utf16.start);
830        let end = self.offset_from_utf16(range_utf16.end);
831        if start <= self.value.len() && end <= self.value.len() {
832            Some(self.value[start..end].to_string())
833        } else {
834            None
835        }
836    }
837
838    fn selected_text_range(
839        &mut self,
840        _: bool,
841        _: &mut Window,
842        _: &mut Context<Self>,
843    ) -> Option<UTF16Selection> {
844        Some(UTF16Selection {
845            range: self.offset_to_utf16(self.selected_range.start)
846                ..self.offset_to_utf16(self.selected_range.end),
847            reversed: self.selection_reversed,
848        })
849    }
850
851    fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
852        self.marked_range
853            .as_ref()
854            .map(|r| self.offset_to_utf16(r.start)..self.offset_to_utf16(r.end))
855    }
856
857    fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
858        self.marked_range = None;
859    }
860
861    fn replace_text_in_range(
862        &mut self,
863        range_utf16: Option<Range<usize>>,
864        new_text: &str,
865        _: &mut Window,
866        cx: &mut Context<Self>,
867    ) {
868        let range = range_utf16
869            .map(|r| self.offset_from_utf16(r.start)..self.offset_from_utf16(r.end))
870            .or_else(|| self.marked_range.clone())
871            .unwrap_or(self.selected_range.clone());
872        let potential_v = {
873            let mut temp = self.value.to_string();
874            temp.replace_range(range.clone(), new_text);
875            temp
876        };
877        if let Some(ref filter) = self.filter {
878            if !filter(&potential_v) {
879                return;
880            }
881        }
882        if let Some(max) = self.max_length {
883            if potential_v.chars().count() > max {
884                return;
885            }
886        }
887        let mut v = self.value.to_string();
888        v.replace_range(range.clone(), new_text);
889        self.value = SharedString::from(v);
890        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
891        self.marked_range = None;
892        self.emit_change(cx);
893        cx.notify();
894    }
895
896    fn replace_and_mark_text_in_range(
897        &mut self,
898        range_utf16: Option<Range<usize>>,
899        new_text: &str,
900        new_selected: Option<Range<usize>>,
901        _: &mut Window,
902        cx: &mut Context<Self>,
903    ) {
904        let range = range_utf16
905            .map(|r| self.offset_from_utf16(r.start)..self.offset_from_utf16(r.end))
906            .or(self.marked_range.clone())
907            .unwrap_or(self.selected_range.clone());
908        let potential_v = {
909            let mut temp = self.value.to_string();
910            temp.replace_range(range.clone(), new_text);
911            temp
912        };
913        if let Some(ref filter) = self.filter {
914            if !filter(&potential_v) {
915                return;
916            }
917        }
918        if let Some(max) = self.max_length {
919            if potential_v.chars().count() > max {
920                return;
921            }
922        }
923        let mut v = self.value.to_string();
924        v.replace_range(range.clone(), new_text);
925        self.value = SharedString::from(v);
926        if !new_text.is_empty() {
927            self.marked_range = Some(range.start..range.start + new_text.len());
928        } else {
929            self.marked_range = None;
930        }
931        if let Some(sel) = new_selected {
932            self.selected_range = range.start + sel.start..range.start + sel.end;
933        } else {
934            self.selected_range = range.start + new_text.len()..range.start + new_text.len();
935        }
936        self.emit_change(cx);
937        cx.notify();
938    }
939
940    fn bounds_for_range(
941        &mut self,
942        range_utf16: Range<usize>,
943        bounds: Bounds<Pixels>,
944        window: &mut Window,
945        _: &mut Context<Self>,
946    ) -> Option<Bounds<Pixels>> {
947        let layouts = &self.last_line_layouts;
948        if layouts.is_empty() {
949            return None;
950        }
951        let start = self.offset_from_utf16(range_utf16.start);
952        let end = self.offset_from_utf16(range_utf16.end);
953        let line_height = window.line_height();
954        let mut original_byte_offset = 0;
955        for (idx, (layout, y_offset)) in layouts.iter().enumerate() {
956            let line_text = self.value.split('\n').nth(idx).unwrap_or("");
957            let line_len = line_text.len();
958            if start >= original_byte_offset && start <= original_byte_offset + line_len {
959                let x_start = layout.x_for_index(
960                    self.safe_display_offset_in_line(start - original_byte_offset, line_text),
961                );
962                let x_end = layout.x_for_index(self.safe_display_offset_in_line(
963                    end.min(original_byte_offset + line_len) - original_byte_offset,
964                    line_text,
965                ));
966                return Some(Bounds::from_corners(
967                    point(bounds.left() + x_start, *y_offset),
968                    point(bounds.left() + x_end, *y_offset + line_height),
969                ));
970            }
971            original_byte_offset += line_len + 1;
972        }
973        None
974    }
975
976    fn character_index_for_point(
977        &mut self,
978        pt: Point<Pixels>,
979        window: &mut Window,
980        _: &mut Context<Self>,
981    ) -> Option<usize> {
982        Some(self.offset_to_utf16(self.index_for_point(pt, window)))
983    }
984}
985
986impl Input {
987    fn offset_to_utf16(&self, offset: usize) -> usize {
988        if self.value.is_empty() {
989            return 0;
990        }
991        self.value[..offset.min(self.value.len())]
992            .chars()
993            .map(|c| c.len_utf16())
994            .sum()
995    }
996    fn offset_from_utf16(&self, target: usize) -> usize {
997        let mut utf8 = 0;
998        let mut utf16 = 0;
999        for c in self.value.chars() {
1000            if utf16 >= target {
1001                break;
1002            }
1003            utf16 += c.len_utf16();
1004            utf8 += c.len_utf8();
1005        }
1006        utf8
1007    }
1008    fn text_for_display(&self) -> SharedString {
1009        if self.value.is_empty() {
1010            self.placeholder.clone()
1011        } else if self.is_password() {
1012            let masked = self
1013                .value
1014                .chars()
1015                .map(|c| if c == '\n' { '\n' } else { self.mask_char })
1016                .collect::<String>();
1017            SharedString::from(masked)
1018        } else {
1019            self.value.clone()
1020        }
1021    }
1022    fn safe_display_offset_in_line(&self, line_offset: usize, line_text: &str) -> usize {
1023        if !self.last_layout_is_masked {
1024            return line_offset;
1025        }
1026        let mut count = 0;
1027        let mut bytes = 0;
1028        for c in line_text.chars() {
1029            if bytes >= line_offset {
1030                break;
1031            }
1032            bytes += c.len_utf8();
1033            count += 1;
1034        }
1035        count * self.mask_char.len_utf8()
1036    }
1037}
1038
1039fn line_start_at(value: &str, offset: usize) -> usize {
1040    value[..offset.min(value.len())]
1041        .rfind('\n')
1042        .map_or(0, |index| index + 1)
1043}
1044
1045fn selected_line_end(value: &str, selection: Range<usize>) -> usize {
1046    if value.is_empty() {
1047        return 0;
1048    }
1049    let mut end = selection.end.min(value.len());
1050    if end > selection.start && end > 0 && value.as_bytes().get(end - 1) == Some(&b'\n') {
1051        end -= 1;
1052    }
1053    value[end..]
1054        .find('\n')
1055        .map_or(value.len(), |relative| end + relative + 1)
1056}
1057
1058fn removable_indent_len(line: &str, indent: &str) -> Option<usize> {
1059    if line.starts_with(indent) {
1060        return Some(indent.len());
1061    }
1062    if indent.chars().all(|ch| ch == ' ') {
1063        let max_spaces = indent.len();
1064        let spaces = line
1065            .as_bytes()
1066            .iter()
1067            .take_while(|byte| **byte == b' ')
1068            .take(max_spaces)
1069            .count();
1070        if spaces > 0 {
1071            return Some(spaces);
1072        }
1073    }
1074    if indent == "\t" && line.starts_with('\t') {
1075        return Some(1);
1076    }
1077    None
1078}
1079
1080fn apply_signed_delta(value: usize, delta: isize) -> usize {
1081    if delta.is_negative() {
1082        value.saturating_sub(delta.unsigned_abs())
1083    } else {
1084        value.saturating_add(delta as usize)
1085    }
1086}
1087
1088struct InputElement {
1089    input: Entity<Input>,
1090    disabled: bool,
1091}
1092
1093struct InputPrepaint {
1094    lines: Vec<(ShapedLine, Pixels)>,
1095    cursor: Option<gpui::PaintQuad>,
1096    selection: Vec<gpui::PaintQuad>,
1097    is_masked: bool,
1098    text_align: gpui::TextAlign,
1099}
1100
1101impl IntoElement for InputElement {
1102    type Element = Self;
1103    fn into_element(self) -> Self::Element {
1104        self
1105    }
1106}
1107
1108impl Element for InputElement {
1109    type RequestLayoutState = ();
1110    type PrepaintState = InputPrepaint;
1111    fn id(&self) -> Option<ElementId> {
1112        None
1113    }
1114    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1115        None
1116    }
1117    fn request_layout(
1118        &mut self,
1119        _: Option<&GlobalElementId>,
1120        _: Option<&InspectorElementId>,
1121        window: &mut Window,
1122        cx: &mut App,
1123    ) -> (LayoutId, ()) {
1124        let input = self.input.read(cx);
1125        let line_count = input
1126            .text_for_display()
1127            .split('\n')
1128            .count()
1129            .max(input.min_rows) as f32;
1130        let mut style = Style::default();
1131        style.size.width = gpui::relative(1.).into();
1132        style.size.height = (window.line_height() * line_count).into();
1133        (window.request_layout(style, [], cx), ())
1134    }
1135
1136    fn prepaint(
1137        &mut self,
1138        _: Option<&GlobalElementId>,
1139        _: Option<&InspectorElementId>,
1140        bounds: Bounds<Pixels>,
1141        _: &mut (),
1142        window: &mut Window,
1143        cx: &mut App,
1144    ) -> InputPrepaint {
1145        let input = self.input.read(cx);
1146        let style = window.text_style();
1147        let theme = &cx.global::<Config>().theme;
1148        let text_c = if self.disabled {
1149            theme.neutral.text_disabled
1150        } else {
1151            style.color
1152        };
1153        let font_size = style.font_size.to_pixels(window.rem_size());
1154        let line_height = window.line_height();
1155        let cursor_offset = input.cursor_offset();
1156        let text = input.text_for_display();
1157        let is_masked = input.is_password();
1158        let text_align = input.text_align;
1159        let text_lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
1160
1161        let original_cursor_line = if input.value.is_empty() {
1162            0
1163        } else {
1164            let mut line = 0;
1165            let mut start = 0;
1166            for l in input.value.split('\n') {
1167                if cursor_offset >= start && cursor_offset <= start + l.len() {
1168                    break;
1169                }
1170                start += l.len() + 1;
1171                line += 1;
1172            }
1173            line
1174        };
1175
1176        let mut lines = Vec::new();
1177        let mut y = bounds.top();
1178        let mut cursor_quad = None;
1179        let mut selection_quads = Vec::new();
1180        let mut original_byte_offset = 0;
1181
1182        for (i, line_text) in text_lines.iter().enumerate() {
1183            let (display, color) = if input.value.is_empty() {
1184                (input.placeholder.clone(), theme.neutral.text_3)
1185            } else {
1186                (SharedString::from(line_text.clone()), text_c)
1187            };
1188            let run = TextRun {
1189                len: display.len(),
1190                font: style.font(),
1191                color,
1192                background_color: None,
1193                underline: None,
1194                strikethrough: None,
1195            };
1196            let shaped = window
1197                .text_system()
1198                .shape_line(display, font_size, &[run], None);
1199
1200            let x_offset = match text_align {
1201                gpui::TextAlign::Left => px(0.0),
1202                gpui::TextAlign::Center => (bounds.size.width - shaped.width) / 2.0,
1203                gpui::TextAlign::Right => bounds.size.width - shaped.width,
1204            };
1205
1206            if !input.selected_range.is_empty() && !input.value.is_empty() {
1207                let range = input.selected_range.clone();
1208                let original_line = input.value.split('\n').nth(i).unwrap_or("");
1209                let line_start = original_byte_offset;
1210                let line_end = original_byte_offset + original_line.len();
1211                let start = range.start.max(line_start);
1212                let end = range.end.min(line_end);
1213                if start < end {
1214                    let d_start =
1215                        input.safe_display_offset_in_line(start - line_start, original_line);
1216                    let d_end = input.safe_display_offset_in_line(end - line_start, original_line);
1217                    let x_start = shaped.x_for_index(d_start);
1218                    let x_end = shaped.x_for_index(d_end);
1219                    selection_quads.push(fill(
1220                        Bounds::new(
1221                            point(bounds.left() + x_offset + x_start, y),
1222                            size(x_end - x_start, line_height),
1223                        ),
1224                        theme.primary.base.opacity(0.3),
1225                    ));
1226                }
1227            }
1228            if i == original_cursor_line
1229                && input.selected_range.is_empty()
1230                && input.cursor_visible
1231                && !input.value.is_empty()
1232            {
1233                let original_line = input.value.split('\n').nth(i).unwrap_or("");
1234                let line_start = original_byte_offset;
1235                let col = cursor_offset - line_start;
1236                let d_col = if is_masked {
1237                    let mut count = 0;
1238                    let mut bytes = 0;
1239                    for c in original_line.chars() {
1240                        if bytes >= col {
1241                            break;
1242                        }
1243                        bytes += c.len_utf8();
1244                        count += 1;
1245                    }
1246                    count * input.mask_char.len_utf8()
1247                } else {
1248                    col
1249                };
1250
1251                let x = shaped.x_for_index(d_col);
1252                let ch = font_size.add(px(6.0));
1253                let ct = y + (line_height - ch) / 2.0;
1254                cursor_quad = Some(fill(
1255                    Bounds::new(point(bounds.left() + x_offset + x, ct), size(px(2.), ch)),
1256                    theme.primary.base,
1257                ));
1258            } else if i == 0 && input.value.is_empty() && input.cursor_visible {
1259                let x = match text_align {
1260                    gpui::TextAlign::Left => px(0.0),
1261                    gpui::TextAlign::Center => (bounds.size.width - shaped.width) / 2.0,
1262                    gpui::TextAlign::Right => bounds.size.width - shaped.width,
1263                };
1264                let ch = font_size.add(px(6.0));
1265                let ct = y + (line_height - ch) / 2.0;
1266                cursor_quad = Some(fill(
1267                    Bounds::new(point(bounds.left() + x, ct), size(px(2.), ch)),
1268                    theme.primary.base,
1269                ));
1270            }
1271
1272            lines.push((shaped, y));
1273            y = y + line_height;
1274            original_byte_offset += input
1275                .value
1276                .split('\n')
1277                .nth(i)
1278                .map(|l| l.len() + 1)
1279                .unwrap_or(0);
1280        }
1281        InputPrepaint {
1282            lines,
1283            cursor: cursor_quad,
1284            selection: selection_quads,
1285            is_masked,
1286            text_align,
1287        }
1288    }
1289
1290    fn paint(
1291        &mut self,
1292        _: Option<&GlobalElementId>,
1293        _: Option<&InspectorElementId>,
1294        bounds: Bounds<Pixels>,
1295        _: &mut (),
1296        prepaint: &mut InputPrepaint,
1297        window: &mut Window,
1298        cx: &mut App,
1299    ) {
1300        let focus_handle = self.input.read(cx).focus_handle.clone();
1301        window.handle_input(
1302            &focus_handle,
1303            ElementInputHandler::new(bounds, self.input.clone()),
1304            cx,
1305        );
1306        for s in prepaint.selection.drain(..) {
1307            window.paint_quad(s);
1308        }
1309        let text_align = prepaint.text_align;
1310        for (line, y) in &prepaint.lines {
1311            let _ = line.paint(point(bounds.left(), *y), window.line_height(), window, cx);
1312        }
1313        if focus_handle.is_focused(window) {
1314            if let Some(c) = prepaint.cursor.take() {
1315                window.paint_quad(c);
1316            }
1317        }
1318        let line_layouts = prepaint.lines.clone();
1319        let is_masked = prepaint.is_masked;
1320        self.input.update(cx, |input, _| {
1321            input.last_line_layouts = line_layouts;
1322            input.last_bounds = Some(bounds);
1323            input.last_layout_is_masked = is_masked;
1324        });
1325    }
1326}
1327
1328impl Render for Input {
1329    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1330        let focused = self.focus_handle(cx).is_focused(window);
1331        if focused && self.blink_task.is_none() {
1332            self.start_blink(cx);
1333        } else if !focused && self.blink_task.is_some() {
1334            self.blink_task = None;
1335        }
1336
1337        let theme = cx.global::<Config>().theme.clone();
1338        let icon_sz = 16.0;
1339        let (bg, border_c) = if self.disabled {
1340            (theme.neutral.hover, theme.neutral.border)
1341        } else if focused {
1342            (theme.neutral.card, theme.primary.base)
1343        } else {
1344            (theme.neutral.card, theme.neutral.border)
1345        };
1346        let fh = self.focus_handle(cx);
1347        let line_height = window.line_height();
1348
1349        let mut row = gpui::div()
1350            .flex()
1351            .flex_row()
1352            .when_some(self.width, |s, w| s.w(w))
1353            .when_some(self.height, |s, h| s.h(h))
1354            .when(self.height.is_none(), |s| {
1355                if self.min_rows > 1 {
1356                    s.h_auto()
1357                        .min_h(line_height * self.min_rows as f32 + px(16.0))
1358                } else {
1359                    s.min_h(px(34.0))
1360                }
1361            })
1362            .rounded(px(theme.radius.md))
1363            .bg(bg)
1364            .border_1()
1365            .border_color(border_c)
1366            .text_size(px(theme.font_size.md))
1367            .overflow_hidden();
1368
1369        if self.min_rows > 1 {
1370            row = row.items_start();
1371        } else {
1372            row = row.items_center();
1373        }
1374
1375        if !self.disabled {
1376            row = row.track_focus(&fh).cursor_text();
1377        } else {
1378            row = row.cursor_not_allowed();
1379        }
1380
1381        if !self.disabled {
1382            row = row
1383                .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1384                .on_mouse_move(cx.listener(Self::on_mouse_move))
1385                .on_action(cx.listener(Self::backspace))
1386                .on_action(cx.listener(Self::delete))
1387                .on_action(cx.listener(Self::left))
1388                .on_action(cx.listener(Self::select_left))
1389                .on_action(cx.listener(Self::right))
1390                .on_action(cx.listener(Self::select_right))
1391                .on_action(cx.listener(Self::home))
1392                .on_action(cx.listener(Self::select_home))
1393                .on_action(cx.listener(Self::end))
1394                .on_action(cx.listener(Self::select_end))
1395                .on_action(cx.listener(Self::select_all))
1396                .on_action(cx.listener(Self::copy))
1397                .on_action(cx.listener(Self::paste))
1398                .on_action(cx.listener(Self::cut))
1399                .on_action(cx.listener(Self::enter))
1400                .on_action(cx.listener(Self::up))
1401                .on_action(cx.listener(Self::select_up))
1402                .on_action(cx.listener(Self::down))
1403                .on_action(cx.listener(Self::select_down));
1404        }
1405
1406        if let Some(ref p_render) = self.prepend {
1407            row = row.child(
1408                gpui::div()
1409                    .flex_none()
1410                    .items_start()
1411                    .bg(theme.neutral.hover)
1412                    .border_r_1()
1413                    .border_color(theme.neutral.border)
1414                    .flex()
1415                    .items_center()
1416                    .justify_center()
1417                    .text_color(theme.neutral.text_3)
1418                    .child(p_render(window, cx)),
1419            );
1420        }
1421
1422        let mut inner = gpui::div().flex_1().flex().flex_row().gap_2().px(px(12.0));
1423        if self.min_rows > 1 {
1424            inner = inner.items_start().py_2();
1425        } else {
1426            inner = inner.items_center();
1427        }
1428
1429        if let Some(icon) = self.icon_prefix {
1430            inner = inner.child(Icon::new(icon).size(px(icon_sz)).color(theme.neutral.icon));
1431        }
1432
1433        inner = inner.child(InputElement {
1434            input: cx.entity().clone(),
1435            disabled: self.disabled,
1436        });
1437
1438        if self.clearable && !self.value.is_empty() && !self.disabled {
1439            inner = inner.child(
1440                gpui::div()
1441                    .flex_none()
1442                    .cursor_pointer()
1443                    .hover(|s| s.cursor_pointer())
1444                    .child(
1445                        Icon::new(IconName::X)
1446                            .size(px(14.0))
1447                            .color(theme.neutral.icon),
1448                    )
1449                    .on_mouse_down(
1450                        MouseButton::Left,
1451                        cx.listener(
1452                            move |this: &mut Self,
1453                                  _: &MouseDownEvent,
1454                                  _: &mut Window,
1455                                  cx: &mut Context<Self>| {
1456                                this.clear(cx);
1457                                cx.stop_propagation();
1458                            },
1459                        ),
1460                    ),
1461            );
1462        }
1463
1464        if self.input_type == InputType::Password && !self.disabled {
1465            let visible = self.password_visible;
1466            inner = inner.child(
1467                gpui::div()
1468                    .cursor_pointer()
1469                    .flex_none()
1470                    .child(
1471                        Icon::new(if visible {
1472                            IconName::EyeOff
1473                        } else {
1474                            IconName::Eye
1475                        })
1476                        .size(px(14.0))
1477                        .color(theme.neutral.icon),
1478                    )
1479                    .on_mouse_down(
1480                        MouseButton::Left,
1481                        cx.listener(
1482                            move |this: &mut Self,
1483                                  _: &MouseDownEvent,
1484                                  window: &mut Window,
1485                                  cx: &mut Context<Self>| {
1486                                this.toggle_password(&TogglePassword, window, cx);
1487                            },
1488                        ),
1489                    ),
1490            );
1491        }
1492
1493        if let Some(icon) = self.icon_suffix {
1494            inner = inner.child(Icon::new(icon).size(px(icon_sz)).color(theme.neutral.icon));
1495        }
1496
1497        row = row.child(inner);
1498
1499        if let Some(ref a_render) = self.append {
1500            row = row.child(
1501                gpui::div()
1502                    .flex_none()
1503                    .items_start()
1504                    .bg(theme.neutral.hover)
1505                    .border_l_1()
1506                    .border_color(theme.neutral.border)
1507                    .flex()
1508                    .items_center()
1509                    .justify_center()
1510                    .text_color(theme.neutral.text_3)
1511                    .child(a_render(window, cx)),
1512            );
1513        }
1514
1515        row
1516    }
1517}
1518
1519#[cfg(test)]
1520mod width_tests {
1521    #[test]
1522    fn input_width_sm_sets_compact_width() {
1523        let source = include_str!("input.rs")
1524            .split("#[cfg(test)]")
1525            .next()
1526            .unwrap();
1527
1528        assert!(source.contains("width: Option<Pixels>"));
1529        assert!(source.contains("pub fn width_sm(self) -> Self"));
1530        assert!(source.contains(".when_some(self.width, |s, w| s.w(w))"));
1531    }
1532
1533    #[test]
1534    fn input_text_addons_are_available_for_self_bootstrapped_demos() {
1535        let source = include_str!("input.rs")
1536            .split("#[cfg(test)]")
1537            .next()
1538            .unwrap();
1539
1540        assert!(source.contains("pub fn prepend_text"));
1541        assert!(source.contains("pub fn append_text"));
1542        assert!(source.contains("pub fn prepend_icon"));
1543    }
1544
1545    #[test]
1546    fn input_exposes_code_editor_indentation_hooks() {
1547        let source = include_str!("input.rs")
1548            .split("#[cfg(test)]")
1549            .next()
1550            .unwrap();
1551
1552        assert!(source.contains("pub fn indent_selection"));
1553        assert!(source.contains("pub fn outdent_selection"));
1554        assert!(source.contains("fn reindent_selected_lines"));
1555    }
1556
1557    #[test]
1558    fn removable_indent_supports_partial_soft_tabs() {
1559        assert_eq!(
1560            super::removable_indent_len("    let x = 1;", "    "),
1561            Some(4)
1562        );
1563        assert_eq!(super::removable_indent_len("  let x = 1;", "    "), Some(2));
1564        assert_eq!(super::removable_indent_len("\tlet x = 1;", "\t"), Some(1));
1565        assert_eq!(super::removable_indent_len("let x = 1;", "    "), None);
1566    }
1567}