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}