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
40pub 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#[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 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 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
990pub fn textarea(id: impl Into<SharedString>) -> Input {
992 Input::new(id).multiline(true)
993}