Skip to main content

glassy_ui/
input.rs

1use std::ops::Range;
2use std::rc::Rc;
3
4use crate::motion::StyledSlot;
5use crate::theme::ActiveTheme;
6use gpui::{
7    actions, div, fill, point, prelude::*, px, relative, size, App, Bounds, ClipboardItem,
8    CursorStyle, Element, ElementId, ElementInputHandler, Entity, EntityInputHandler, FocusHandle,
9    Focusable, FontWeight, GlobalElementId, Hsla, IntoElement, KeyBinding, LayoutId, MouseButton,
10    MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, Render, RenderOnce,
11    ShapedLine, SharedString, Style, StyleRefinement, Styled, TextRun, UTF16Selection,
12    UnderlineStyle, Window,
13};
14
15use crate::chrome::{box_shadow, field_chrome, FieldState};
16use crate::compat::{AccessibilityExt, Role};
17use crate::icon::{Icon, IconName};
18
19type InputChangeHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>;
20
21actions!(
22    glassy_input,
23    [
24        Backspace,
25        Delete,
26        Left,
27        Right,
28        SelectLeft,
29        SelectRight,
30        SelectAll,
31        Home,
32        End,
33        Paste,
34        Cut,
35        Copy,
36        Newline,
37    ]
38);
39
40/// Bind field keys. Call once at startup.
41pub fn init(cx: &mut App) {
42    cx.bind_keys([
43        KeyBinding::new("backspace", Backspace, Some("KitInput")),
44        KeyBinding::new("delete", Delete, Some("KitInput")),
45        KeyBinding::new("left", Left, Some("KitInput")),
46        KeyBinding::new("right", Right, Some("KitInput")),
47        KeyBinding::new("shift-left", SelectLeft, Some("KitInput")),
48        KeyBinding::new("shift-right", SelectRight, Some("KitInput")),
49        KeyBinding::new("cmd-a", SelectAll, Some("KitInput")),
50        KeyBinding::new("ctrl-a", SelectAll, Some("KitInput")),
51        KeyBinding::new("cmd-v", Paste, Some("KitInput")),
52        KeyBinding::new("ctrl-v", Paste, Some("KitInput")),
53        KeyBinding::new("cmd-c", Copy, Some("KitInput")),
54        KeyBinding::new("ctrl-c", Copy, Some("KitInput")),
55        KeyBinding::new("cmd-x", Cut, Some("KitInput")),
56        KeyBinding::new("ctrl-x", Cut, Some("KitInput")),
57        KeyBinding::new("home", Home, Some("KitInput")),
58        KeyBinding::new("end", End, Some("KitInput")),
59        KeyBinding::new("enter", Newline, Some("KitInput")),
60    ]);
61}
62
63pub(crate) struct InputState {
64    focus_handle: FocusHandle,
65    content: SharedString,
66    placeholder: SharedString,
67    selected_range: Range<usize>,
68    selection_reversed: bool,
69    marked_range: Option<Range<usize>>,
70    last_layout: Option<ShapedLine>,
71    last_bounds: Option<Bounds<Pixels>>,
72    is_selecting: bool,
73    multiline: bool,
74    disabled: bool,
75    value_prop: SharedString,
76    on_change: Option<InputChangeHandler>,
77}
78
79impl InputState {
80    fn new(
81        cx: &mut gpui::Context<Self>,
82        placeholder: SharedString,
83        content: SharedString,
84        multiline: bool,
85    ) -> Self {
86        let len = content.len();
87        Self {
88            focus_handle: cx.focus_handle(),
89            content: content.clone(),
90            placeholder,
91            selected_range: len..len,
92            selection_reversed: false,
93            marked_range: None,
94            last_layout: None,
95            last_bounds: None,
96            is_selecting: false,
97            multiline,
98            disabled: false,
99            value_prop: content,
100            on_change: None,
101        }
102    }
103
104    fn emit_change(&self, window: &mut Window, cx: &mut gpui::Context<Self>) {
105        if let Some(on_change) = self.on_change.clone() {
106            on_change(self.content.clone(), window, cx);
107        }
108    }
109
110    fn clamp_selection(&mut self) {
111        let len = self.content.len();
112        if self.selected_range.start > len {
113            self.selected_range.start = len;
114        }
115        if self.selected_range.end > len {
116            self.selected_range.end = len;
117        }
118    }
119
120    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut gpui::Context<Self>) {
121        if self.disabled {
122            return;
123        }
124        if self.selected_range.is_empty() {
125            self.move_to(self.previous_boundary(self.cursor_offset()), cx);
126        } else {
127            self.move_to(self.selected_range.start, cx);
128        }
129    }
130
131    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut gpui::Context<Self>) {
132        if self.disabled {
133            return;
134        }
135        if self.selected_range.is_empty() {
136            self.move_to(self.next_boundary(self.selected_range.end), cx);
137        } else {
138            self.move_to(self.selected_range.end, cx);
139        }
140    }
141
142    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut gpui::Context<Self>) {
143        if !self.disabled {
144            self.select_to(self.previous_boundary(self.cursor_offset()), cx);
145        }
146    }
147
148    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut gpui::Context<Self>) {
149        if !self.disabled {
150            self.select_to(self.next_boundary(self.cursor_offset()), cx);
151        }
152    }
153
154    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut gpui::Context<Self>) {
155        if self.disabled {
156            return;
157        }
158        self.move_to(0, cx);
159        self.select_to(self.content.len(), cx);
160    }
161
162    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut gpui::Context<Self>) {
163        if !self.disabled {
164            self.move_to(0, cx);
165        }
166    }
167
168    fn end(&mut self, _: &End, _: &mut Window, cx: &mut gpui::Context<Self>) {
169        if !self.disabled {
170            self.move_to(self.content.len(), cx);
171        }
172    }
173
174    fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut gpui::Context<Self>) {
175        if self.disabled {
176            return;
177        }
178        if self.selected_range.is_empty() {
179            let prev = self.previous_boundary(self.cursor_offset());
180            if self.cursor_offset() == prev {
181                return;
182            }
183            self.select_to(prev, cx);
184        }
185        self.replace_text_in_range(None, "", window, cx);
186    }
187
188    fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut gpui::Context<Self>) {
189        if self.disabled {
190            return;
191        }
192        if self.selected_range.is_empty() {
193            let next = self.next_boundary(self.cursor_offset());
194            if self.cursor_offset() == next {
195                return;
196            }
197            self.select_to(next, cx);
198        }
199        self.replace_text_in_range(None, "", window, cx);
200    }
201
202    fn on_mouse_down(
203        &mut self,
204        event: &MouseDownEvent,
205        window: &mut Window,
206        cx: &mut gpui::Context<Self>,
207    ) {
208        if self.disabled {
209            return;
210        }
211        window.focus(&self.focus_handle);
212        self.is_selecting = true;
213        if event.modifiers.shift {
214            self.select_to(self.index_for_mouse_position(event.position), cx);
215        } else {
216            self.move_to(self.index_for_mouse_position(event.position), cx);
217        }
218    }
219
220    fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut gpui::Context<Self>) {
221        self.is_selecting = false;
222    }
223
224    fn on_mouse_move(
225        &mut self,
226        event: &MouseMoveEvent,
227        _: &mut Window,
228        cx: &mut gpui::Context<Self>,
229    ) {
230        if self.is_selecting && !self.disabled {
231            self.select_to(self.index_for_mouse_position(event.position), cx);
232        }
233    }
234
235    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut gpui::Context<Self>) {
236        if self.disabled {
237            return;
238        }
239        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
240            let text = if self.multiline {
241                text
242            } else {
243                text.replace('\n', " ")
244            };
245            self.replace_text_in_range(None, &text, window, cx);
246        }
247    }
248
249    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut gpui::Context<Self>) {
250        if !self.selected_range.is_empty() {
251            cx.write_to_clipboard(ClipboardItem::new_string(
252                self.content[self.selected_range.clone()].to_string(),
253            ));
254        }
255    }
256
257    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut gpui::Context<Self>) {
258        if self.disabled || self.selected_range.is_empty() {
259            return;
260        }
261        cx.write_to_clipboard(ClipboardItem::new_string(
262            self.content[self.selected_range.clone()].to_string(),
263        ));
264        self.replace_text_in_range(None, "", window, cx);
265    }
266
267    fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut gpui::Context<Self>) {
268        if self.disabled || !self.multiline {
269            return;
270        }
271        self.replace_text_in_range(None, "\n", window, cx);
272    }
273
274    fn move_to(&mut self, offset: usize, cx: &mut gpui::Context<Self>) {
275        self.selected_range = offset..offset;
276        self.selection_reversed = false;
277        cx.notify();
278    }
279
280    fn cursor_offset(&self) -> usize {
281        if self.selection_reversed {
282            self.selected_range.start
283        } else {
284            self.selected_range.end
285        }
286    }
287
288    fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
289        if self.content.is_empty() {
290            return 0;
291        }
292        let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
293        else {
294            return 0;
295        };
296        if position.y < bounds.top() {
297            return 0;
298        }
299        if position.y > bounds.bottom() {
300            return self.content.len();
301        }
302        line.closest_index_for_x(position.x - bounds.left())
303    }
304
305    fn select_to(&mut self, offset: usize, cx: &mut gpui::Context<Self>) {
306        if self.selection_reversed {
307            self.selected_range.start = offset;
308        } else {
309            self.selected_range.end = offset;
310        }
311        if self.selected_range.end < self.selected_range.start {
312            self.selection_reversed = !self.selection_reversed;
313            self.selected_range = self.selected_range.end..self.selected_range.start;
314        }
315        cx.notify();
316    }
317
318    fn offset_from_utf16(&self, offset: usize) -> usize {
319        let mut utf8_offset = 0;
320        let mut utf16_count = 0;
321        for ch in self.content.chars() {
322            if utf16_count >= offset {
323                break;
324            }
325            utf16_count += ch.len_utf16();
326            utf8_offset += ch.len_utf8();
327        }
328        utf8_offset
329    }
330
331    fn offset_to_utf16(&self, offset: usize) -> usize {
332        let mut utf16_offset = 0;
333        let mut utf8_count = 0;
334        for ch in self.content.chars() {
335            if utf8_count >= offset {
336                break;
337            }
338            utf8_count += ch.len_utf8();
339            utf16_offset += ch.len_utf16();
340        }
341        utf16_offset
342    }
343
344    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
345        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
346    }
347
348    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
349        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
350    }
351
352    fn previous_boundary(&self, offset: usize) -> usize {
353        if offset == 0 {
354            return 0;
355        }
356        let mut i = offset - 1;
357        while i > 0 && !self.content.is_char_boundary(i) {
358            i -= 1;
359        }
360        i
361    }
362
363    fn next_boundary(&self, offset: usize) -> usize {
364        if offset >= self.content.len() {
365            return self.content.len();
366        }
367        let mut i = offset + 1;
368        while i < self.content.len() && !self.content.is_char_boundary(i) {
369            i += 1;
370        }
371        i
372    }
373}
374
375impl Focusable for InputState {
376    fn focus_handle(&self, _: &App) -> FocusHandle {
377        self.focus_handle.clone()
378    }
379}
380
381impl EntityInputHandler for InputState {
382    fn text_for_range(
383        &mut self,
384        range_utf16: Range<usize>,
385        actual_range: &mut Option<Range<usize>>,
386        _: &mut Window,
387        _: &mut gpui::Context<Self>,
388    ) -> Option<String> {
389        let range = self.range_from_utf16(&range_utf16);
390        actual_range.replace(self.range_to_utf16(&range));
391        Some(self.content[range].to_string())
392    }
393
394    fn selected_text_range(
395        &mut self,
396        _: bool,
397        _: &mut Window,
398        _: &mut gpui::Context<Self>,
399    ) -> Option<UTF16Selection> {
400        if self.disabled {
401            return None;
402        }
403        Some(UTF16Selection {
404            range: self.range_to_utf16(&self.selected_range),
405            reversed: self.selection_reversed,
406        })
407    }
408
409    fn marked_text_range(
410        &self,
411        _: &mut Window,
412        _: &mut gpui::Context<Self>,
413    ) -> Option<Range<usize>> {
414        self.marked_range
415            .as_ref()
416            .map(|range| self.range_to_utf16(range))
417    }
418
419    fn unmark_text(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) {
420        self.marked_range = None;
421    }
422
423    fn replace_text_in_range(
424        &mut self,
425        range_utf16: Option<Range<usize>>,
426        new_text: &str,
427        window: &mut Window,
428        cx: &mut gpui::Context<Self>,
429    ) {
430        if self.disabled {
431            return;
432        }
433        let new_text = if self.multiline {
434            new_text.to_string()
435        } else {
436            new_text.replace('\n', " ")
437        };
438        let range = range_utf16
439            .as_ref()
440            .map(|range_utf16| self.range_from_utf16(range_utf16))
441            .or_else(|| self.marked_range.clone())
442            .unwrap_or_else(|| self.selected_range.clone());
443
444        self.content =
445            (self.content[0..range.start].to_owned() + &new_text + &self.content[range.end..])
446                .into();
447        let cursor = range.start + new_text.len();
448        self.selected_range = cursor..cursor;
449        self.marked_range.take();
450        self.emit_change(window, cx);
451        cx.notify();
452    }
453
454    fn replace_and_mark_text_in_range(
455        &mut self,
456        range_utf16: Option<Range<usize>>,
457        new_text: &str,
458        new_selected_range_utf16: Option<Range<usize>>,
459        window: &mut Window,
460        cx: &mut gpui::Context<Self>,
461    ) {
462        if self.disabled {
463            return;
464        }
465        let range = range_utf16
466            .as_ref()
467            .map(|range_utf16| self.range_from_utf16(range_utf16))
468            .or_else(|| self.marked_range.clone())
469            .unwrap_or_else(|| self.selected_range.clone());
470
471        self.content =
472            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
473                .into();
474        if !new_text.is_empty() {
475            self.marked_range = Some(range.start..range.start + new_text.len());
476        } else {
477            self.marked_range = None;
478        }
479        self.selected_range = new_selected_range_utf16
480            .as_ref()
481            .map(|range_utf16| self.range_from_utf16(range_utf16))
482            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
483            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
484        self.emit_change(window, cx);
485        cx.notify();
486    }
487
488    fn bounds_for_range(
489        &mut self,
490        range_utf16: Range<usize>,
491        bounds: Bounds<Pixels>,
492        _: &mut Window,
493        _: &mut gpui::Context<Self>,
494    ) -> Option<Bounds<Pixels>> {
495        let last_layout = self.last_layout.as_ref()?;
496        let range = self.range_from_utf16(&range_utf16);
497        Some(Bounds::from_corners(
498            point(
499                bounds.left() + last_layout.x_for_index(range.start),
500                bounds.top(),
501            ),
502            point(
503                bounds.left() + last_layout.x_for_index(range.end),
504                bounds.bottom(),
505            ),
506        ))
507    }
508
509    fn character_index_for_point(
510        &mut self,
511        point: Point<Pixels>,
512        _: &mut Window,
513        _: &mut gpui::Context<Self>,
514    ) -> Option<usize> {
515        let line_point = self.last_bounds?.localize(&point)?;
516        let last_layout = self.last_layout.as_ref()?;
517        let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
518        Some(self.offset_to_utf16(utf8_index))
519    }
520}
521
522impl Render for InputState {
523    fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
524        gpui::Empty
525    }
526}
527
528struct TextElement {
529    input: Entity<InputState>,
530    color: Hsla,
531    placeholder: Hsla,
532    caret: Hsla,
533    show_caret: bool,
534    fill: bool,
535}
536
537struct PrepaintState {
538    line: Option<ShapedLine>,
539    cursor: Option<PaintQuad>,
540    selection: Option<PaintQuad>,
541}
542
543impl IntoElement for TextElement {
544    type Element = Self;
545
546    fn into_element(self) -> Self::Element {
547        self
548    }
549}
550
551impl Element for TextElement {
552    type RequestLayoutState = ();
553    type PrepaintState = PrepaintState;
554
555    fn id(&self) -> Option<ElementId> {
556        None
557    }
558
559    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
560        None
561    }
562
563    fn request_layout(
564        &mut self,
565        _id: Option<&GlobalElementId>,
566        _inspector_id: Option<&gpui::InspectorElementId>,
567        window: &mut Window,
568        cx: &mut App,
569    ) -> (LayoutId, Self::RequestLayoutState) {
570        let mut style = Style::default();
571        style.size.width = relative(1.).into();
572        style.size.height = if self.fill {
573            relative(1.).into()
574        } else {
575            window.line_height().into()
576        };
577        (window.request_layout(style, [], cx), ())
578    }
579
580    fn prepaint(
581        &mut self,
582        _id: Option<&GlobalElementId>,
583        _inspector_id: Option<&gpui::InspectorElementId>,
584        bounds: Bounds<Pixels>,
585        _request_layout: &mut Self::RequestLayoutState,
586        window: &mut Window,
587        cx: &mut App,
588    ) -> Self::PrepaintState {
589        let input = self.input.read(cx);
590        let content = input.content.clone();
591        let selected_range = input.selected_range.clone();
592        let cursor = input.cursor_offset();
593        let empty = content.is_empty();
594        let (display_text, text_color) = if empty {
595            (input.placeholder.clone(), self.placeholder)
596        } else {
597            (SharedString::from(content.replace('\n', " ")), self.color)
598        };
599
600        let run = TextRun {
601            len: display_text.len(),
602            font: window.text_style().font(),
603            color: text_color,
604            background_color: None,
605            underline: None,
606            strikethrough: None,
607        };
608        let runs = if let Some(marked_range) = input.marked_range.as_ref() {
609            vec![
610                TextRun {
611                    len: marked_range.start,
612                    ..run.clone()
613                },
614                TextRun {
615                    len: marked_range.end - marked_range.start,
616                    underline: Some(UnderlineStyle {
617                        color: Some(run.color),
618                        thickness: px(1.0),
619                        wavy: false,
620                    }),
621                    ..run.clone()
622                },
623                TextRun {
624                    len: display_text.len() - marked_range.end,
625                    ..run
626                },
627            ]
628            .into_iter()
629            .filter(|run| run.len > 0)
630            .collect()
631        } else {
632            vec![run]
633        };
634
635        let font_size = window.text_style().font_size.to_pixels(window.rem_size());
636        let line = window
637            .text_system()
638            .shape_line(display_text, font_size, &runs, None);
639
640        let cursor_pos = if empty {
641            px(0.)
642        } else {
643            line.x_for_index(cursor)
644        };
645        let (selection, cursor_quad) = if empty || selected_range.is_empty() {
646            (
647                None,
648                Some(fill(
649                    Bounds::new(
650                        point(bounds.left() + cursor_pos, bounds.top() + px(1.)),
651                        size(px(1.), px(16.)),
652                    ),
653                    self.caret,
654                )),
655            )
656        } else {
657            (
658                Some(fill(
659                    Bounds::from_corners(
660                        point(
661                            bounds.left() + line.x_for_index(selected_range.start),
662                            bounds.top(),
663                        ),
664                        point(
665                            bounds.left() + line.x_for_index(selected_range.end),
666                            bounds.bottom(),
667                        ),
668                    ),
669                    self.caret.opacity(0.18),
670                )),
671                None,
672            )
673        };
674        PrepaintState {
675            line: Some(line),
676            cursor: cursor_quad,
677            selection,
678        }
679    }
680
681    fn paint(
682        &mut self,
683        _id: Option<&GlobalElementId>,
684        _inspector_id: Option<&gpui::InspectorElementId>,
685        bounds: Bounds<Pixels>,
686        _request_layout: &mut Self::RequestLayoutState,
687        prepaint: &mut Self::PrepaintState,
688        window: &mut Window,
689        cx: &mut App,
690    ) {
691        let focus_handle = self.input.read(cx).focus_handle.clone();
692        let disabled = self.input.read(cx).disabled;
693        if !disabled {
694            window.handle_input(
695                &focus_handle,
696                ElementInputHandler::new(bounds, self.input.clone()),
697                cx,
698            );
699        }
700        if let Some(selection) = prepaint.selection.take() {
701            window.paint_quad(selection);
702        }
703        let line = prepaint.line.take().unwrap();
704        line.paint(bounds.origin, window.line_height(), window, cx)
705            .ok();
706
707        if self.show_caret {
708            if let Some(cursor) = prepaint.cursor.take() {
709                window.paint_quad(cursor);
710            }
711        }
712
713        self.input.update(cx, |input, _cx| {
714            input.last_layout = Some(line);
715            input.last_bounds = Some(bounds);
716        });
717    }
718}
719
720/// Single-line field matching Paper `Glassy UI` → Inputs.
721#[derive(IntoElement)]
722pub struct Input {
723    id: SharedString,
724    placeholder: SharedString,
725    value: SharedString,
726    disabled: bool,
727    invalid: bool,
728    show_focus: bool,
729    leading_icon: Option<IconName>,
730    trailing: Option<SharedString>,
731    helper: Option<SharedString>,
732    multiline: bool,
733    on_change: Option<InputChangeHandler>,
734    style: StyleRefinement,
735}
736
737impl Input {
738    pub fn new(id: impl Into<SharedString>) -> Self {
739        Self {
740            id: id.into(),
741            placeholder: SharedString::default(),
742            value: SharedString::default(),
743            disabled: false,
744            invalid: false,
745            show_focus: false,
746            leading_icon: None,
747            trailing: None,
748            helper: None,
749            multiline: false,
750            on_change: None,
751            style: StyleRefinement::default(),
752        }
753    }
754
755    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
756        self.placeholder = placeholder.into();
757        self
758    }
759
760    pub fn value(mut self, value: impl Into<SharedString>) -> Self {
761        self.value = value.into();
762        self
763    }
764
765    pub fn disabled(mut self, disabled: bool) -> Self {
766        self.disabled = disabled;
767        self
768    }
769
770    pub fn invalid(mut self, invalid: bool) -> Self {
771        self.invalid = invalid;
772        self
773    }
774
775    /// Paint the Paper focus ring and caret even when the field is not focused.
776    pub fn show_focus(mut self, show_focus: bool) -> Self {
777        self.show_focus = show_focus;
778        self
779    }
780
781    pub fn leading_icon(mut self, icon: IconName) -> Self {
782        self.leading_icon = Some(icon);
783        self
784    }
785
786    pub fn trailing(mut self, trailing: impl Into<SharedString>) -> Self {
787        self.trailing = Some(trailing.into());
788        self
789    }
790
791    pub fn helper(mut self, helper: impl Into<SharedString>) -> Self {
792        self.helper = Some(helper.into());
793        self
794    }
795
796    pub fn multiline(mut self, multiline: bool) -> Self {
797        self.multiline = multiline;
798        self
799    }
800
801    /// Fired after the field text changes. `value` is the current contents.
802    pub fn on_change(
803        mut self,
804        listener: impl Fn(SharedString, &mut Window, &mut App) + 'static,
805    ) -> Self {
806        self.on_change = Some(Rc::new(listener));
807        self
808    }
809}
810
811impl Styled for Input {
812    fn style(&mut self) -> &mut StyleRefinement {
813        &mut self.style
814    }
815}
816
817impl RenderOnce for Input {
818    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
819        let theme = cx.theme();
820        let placeholder = self.placeholder.clone();
821        let value = self.value.clone();
822        let multiline = self.multiline;
823        let disabled = self.disabled;
824        let on_change = self.on_change.clone();
825        let state = window.use_keyed_state(self.id.clone(), cx, move |_, cx| {
826            InputState::new(cx, placeholder, value, multiline)
827        });
828        state.update(cx, |input, _| {
829            input.disabled = disabled;
830            input.multiline = multiline;
831            input.on_change = on_change;
832            if input.value_prop.as_ref() != self.value.as_ref() {
833                input.content = self.value.clone();
834                input.value_prop = self.value.clone();
835                input.clamp_selection();
836            }
837        });
838
839        let focus_handle = state.read(cx).focus_handle.clone();
840        let focused = focus_handle.is_focused(window);
841        let field_state = if self.disabled {
842            FieldState::Disabled
843        } else if self.invalid {
844            FieldState::Invalid
845        } else if self.show_focus || focused {
846            FieldState::Focus
847        } else {
848            FieldState::Rest
849        };
850        let chrome = field_chrome(theme, field_state);
851        let show_caret = !self.disabled && (self.show_focus || focused);
852        let has_icon = self.leading_icon.is_some();
853        let pad_x = if has_icon || self.trailing.is_some() {
854            14.0
855        } else {
856            16.0
857        };
858        let width = if multiline { px(320.) } else { px(280.) };
859        let height = if multiline { px(96.) } else { px(36.) };
860        let line_height = if multiline { 20.0 } else { 18.0 };
861        let helper = self.helper.clone();
862        let helper_color = theme.destructive;
863
864        let mut shadows = vec![box_shadow(0., 1., chrome.inset, 0., 0.)];
865        if chrome.shadow_blur > 0.0 {
866            shadows.push(box_shadow(
867                0.,
868                chrome.shadow_y,
869                chrome.shadow,
870                chrome.shadow_blur,
871                0.,
872            ));
873        }
874        if let Some(ring) = chrome.ring {
875            shadows.push(box_shadow(0., 0., ring, 0., 3.));
876        }
877
878        let input_debug_selector = self.id.to_string();
879        let field = div()
880            .id(self.id.clone())
881            .debug_selector(move || input_debug_selector.clone())
882            .key_context("KitInput")
883            .role(Role::TextInput)
884            .when(!self.placeholder.is_empty(), |el| {
885                el.aria_placeholder(self.placeholder.clone())
886            })
887            .track_focus(&focus_handle)
888            .tab_stop(!self.disabled)
889            .flex()
890            .when(multiline, |el| el.items_start())
891            .when(!multiline, |el| el.items_center())
892            .when(has_icon || self.trailing.is_some(), |el| el.gap(px(8.)))
893            .w_full()
894            .h(height)
895            .flex_shrink_0()
896            .px(px(pad_x))
897            .when(multiline, |el| el.py(px(10.)))
898            .rounded(px(6.))
899            .border_1()
900            .border_color(chrome.border)
901            .bg(chrome.bg)
902            .shadow(shadows)
903            .text_color(chrome.fg)
904            .font_family(theme.font_family)
905            .font_weight(FontWeight::NORMAL)
906            .text_size(px(14.))
907            .line_height(px(line_height))
908            .overflow_hidden()
909            .when(self.disabled, |el| el.cursor_default())
910            .when(!self.disabled, |el| el.cursor(CursorStyle::IBeam))
911            .on_action(window.listener_for(&state, InputState::backspace))
912            .on_action(window.listener_for(&state, InputState::delete))
913            .on_action(window.listener_for(&state, InputState::left))
914            .on_action(window.listener_for(&state, InputState::right))
915            .on_action(window.listener_for(&state, InputState::select_left))
916            .on_action(window.listener_for(&state, InputState::select_right))
917            .on_action(window.listener_for(&state, InputState::select_all))
918            .on_action(window.listener_for(&state, InputState::home))
919            .on_action(window.listener_for(&state, InputState::end))
920            .on_action(window.listener_for(&state, InputState::paste))
921            .on_action(window.listener_for(&state, InputState::cut))
922            .on_action(window.listener_for(&state, InputState::copy))
923            .on_action(window.listener_for(&state, InputState::newline))
924            .on_mouse_down(
925                MouseButton::Left,
926                window.listener_for(&state, InputState::on_mouse_down),
927            )
928            .on_mouse_up(
929                MouseButton::Left,
930                window.listener_for(&state, InputState::on_mouse_up),
931            )
932            .on_mouse_up_out(
933                MouseButton::Left,
934                window.listener_for(&state, InputState::on_mouse_up),
935            )
936            .on_mouse_move(window.listener_for(&state, InputState::on_mouse_move))
937            .when_some(self.leading_icon, |el, icon| {
938                el.child(Icon::new(icon).px(px(16.)).color(chrome.placeholder))
939            })
940            .child(
941                div()
942                    .flex_1()
943                    .min_w(px(0.))
944                    .when(multiline, |el| el.h_full())
945                    .overflow_hidden()
946                    .child(TextElement {
947                        input: state.clone(),
948                        color: chrome.fg,
949                        placeholder: chrome.placeholder,
950                        caret: chrome.caret,
951                        show_caret,
952                        fill: multiline,
953                    }),
954            )
955            .when_some(self.trailing, |el, trailing| {
956                el.child(
957                    div()
958                        .flex_shrink_0()
959                        .font_family(theme.font_family)
960                        .font_weight(FontWeight::MEDIUM)
961                        .text_size(px(12.))
962                        .line_height(px(16.))
963                        .text_color(theme.label)
964                        .child(trailing),
965                )
966            });
967
968        div()
969            .flex()
970            .flex_col()
971            .gap(px(8.))
972            .w(width)
973            .flex_shrink_0()
974            .refine_style(&self.style)
975            .child(field)
976            .when_some(helper, |el, helper| {
977                el.child(
978                    div()
979                        .font_family(theme.font_family)
980                        .font_weight(FontWeight::MEDIUM)
981                        .text_size(px(13.))
982                        .line_height(px(16.))
983                        .text_color(helper_color)
984                        .child(helper),
985                )
986            })
987    }
988}
989
990/// Many-line field. Same chrome as [`Input`], 320×96, line-height 20.
991pub fn textarea(id: impl Into<SharedString>) -> Input {
992    Input::new(id).multiline(true)
993}