1mod element;
30pub(crate) mod layout;
31
32use std::ops::Range;
33use std::sync::{Arc, Mutex};
34
35use gpui::{
36 AccessibleAction, App, Bounds, ClipboardItem, Context, CursorStyle, EntityInputHandler,
37 EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, MouseButton,
38 MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
39 SharedString, StatefulInteractiveElement, Styled, Subscription, UTF16Selection, Window,
40 accesskit::ActionData, actions, div, point, prelude::FluentBuilder as _, px,
41};
42use gpui_kit_semantics::{NodeSpec, Role, Semantic};
43use gpui_kit_theme::{ActiveTheme, ControlSize, Radius};
44
45use crate::controls::text_edit;
46use crate::foundation::{ActiveDirection, Disableable, Ident, Sizable, StyledExt};
47use element::TextAreaElement;
48use layout::Layout;
49
50actions!(
51 gpui_kit_textarea,
52 [
53 Backspace,
54 Delete,
55 DeleteToLineStart,
56 DeleteWordLeft,
57 DeleteWordRight,
58 Left,
59 Right,
60 Up,
61 Down,
62 WordLeft,
63 WordRight,
64 SelectLeft,
65 SelectRight,
66 SelectUp,
67 SelectDown,
68 SelectWordLeft,
69 SelectWordRight,
70 SelectToLineStart,
71 SelectToLineEnd,
72 SelectToDocumentStart,
73 SelectToDocumentEnd,
74 SelectAll,
75 LineStart,
76 LineEnd,
77 DocumentStart,
78 DocumentEnd,
79 Newline,
80 Copy,
81 Cut,
82 Paste,
83 Submit,
84 Cancel,
85 ShowCharacterPalette,
86 ]
87);
88
89pub const KEY_CONTEXT: &str = "TextArea";
92
93const DEFAULT_ROWS: usize = 3;
95
96pub(crate) fn install(cx: &mut App) {
101 let primary = if cfg!(target_os = "macos") {
102 "cmd"
103 } else {
104 "ctrl"
105 };
106 let word = if cfg!(target_os = "macos") {
107 "alt"
108 } else {
109 "ctrl"
110 };
111
112 let mut bindings = vec![
113 KeyBinding::new("backspace", Backspace, Some(KEY_CONTEXT)),
114 KeyBinding::new("delete", Delete, Some(KEY_CONTEXT)),
115 KeyBinding::new("left", Left, Some(KEY_CONTEXT)),
116 KeyBinding::new("right", Right, Some(KEY_CONTEXT)),
117 KeyBinding::new("up", Up, Some(KEY_CONTEXT)),
118 KeyBinding::new("down", Down, Some(KEY_CONTEXT)),
119 KeyBinding::new("shift-left", SelectLeft, Some(KEY_CONTEXT)),
120 KeyBinding::new("shift-right", SelectRight, Some(KEY_CONTEXT)),
121 KeyBinding::new("shift-up", SelectUp, Some(KEY_CONTEXT)),
122 KeyBinding::new("shift-down", SelectDown, Some(KEY_CONTEXT)),
123 KeyBinding::new("home", LineStart, Some(KEY_CONTEXT)),
124 KeyBinding::new("end", LineEnd, Some(KEY_CONTEXT)),
125 KeyBinding::new("shift-home", SelectToLineStart, Some(KEY_CONTEXT)),
126 KeyBinding::new("shift-end", SelectToLineEnd, Some(KEY_CONTEXT)),
127 KeyBinding::new("enter", Newline, Some(KEY_CONTEXT)),
129 KeyBinding::new(&format!("{primary}-enter"), Submit, Some(KEY_CONTEXT)),
130 KeyBinding::new("escape", Cancel, Some(KEY_CONTEXT)),
131 KeyBinding::new(&format!("{primary}-home"), DocumentStart, Some(KEY_CONTEXT)),
132 KeyBinding::new(&format!("{primary}-end"), DocumentEnd, Some(KEY_CONTEXT)),
133 KeyBinding::new(
134 &format!("{primary}-shift-home"),
135 SelectToDocumentStart,
136 Some(KEY_CONTEXT),
137 ),
138 KeyBinding::new(
139 &format!("{primary}-shift-end"),
140 SelectToDocumentEnd,
141 Some(KEY_CONTEXT),
142 ),
143 KeyBinding::new(&format!("{word}-left"), WordLeft, Some(KEY_CONTEXT)),
144 KeyBinding::new(&format!("{word}-right"), WordRight, Some(KEY_CONTEXT)),
145 KeyBinding::new(
146 &format!("{word}-shift-left"),
147 SelectWordLeft,
148 Some(KEY_CONTEXT),
149 ),
150 KeyBinding::new(
151 &format!("{word}-shift-right"),
152 SelectWordRight,
153 Some(KEY_CONTEXT),
154 ),
155 KeyBinding::new(
156 &format!("{word}-backspace"),
157 DeleteWordLeft,
158 Some(KEY_CONTEXT),
159 ),
160 KeyBinding::new(
161 &format!("{word}-delete"),
162 DeleteWordRight,
163 Some(KEY_CONTEXT),
164 ),
165 KeyBinding::new(&format!("{primary}-a"), SelectAll, Some(KEY_CONTEXT)),
166 KeyBinding::new(&format!("{primary}-c"), Copy, Some(KEY_CONTEXT)),
167 KeyBinding::new(&format!("{primary}-x"), Cut, Some(KEY_CONTEXT)),
168 KeyBinding::new(&format!("{primary}-v"), Paste, Some(KEY_CONTEXT)),
169 ];
170
171 if cfg!(target_os = "macos") {
172 bindings.extend([
173 KeyBinding::new("cmd-left", LineStart, Some(KEY_CONTEXT)),
174 KeyBinding::new("cmd-right", LineEnd, Some(KEY_CONTEXT)),
175 KeyBinding::new("cmd-up", DocumentStart, Some(KEY_CONTEXT)),
176 KeyBinding::new("cmd-down", DocumentEnd, Some(KEY_CONTEXT)),
177 KeyBinding::new("cmd-shift-left", SelectToLineStart, Some(KEY_CONTEXT)),
178 KeyBinding::new("cmd-shift-right", SelectToLineEnd, Some(KEY_CONTEXT)),
179 KeyBinding::new("cmd-shift-up", SelectToDocumentStart, Some(KEY_CONTEXT)),
180 KeyBinding::new("cmd-shift-down", SelectToDocumentEnd, Some(KEY_CONTEXT)),
181 KeyBinding::new("cmd-backspace", DeleteToLineStart, Some(KEY_CONTEXT)),
182 KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(KEY_CONTEXT)),
183 ]);
184 }
185
186 cx.bind_keys(bindings);
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum TextAreaEvent {
192 Change(SharedString),
194 Submit,
196 Cancel,
198 Focus,
199 Blur,
200}
201
202impl EventEmitter<TextAreaEvent> for TextArea {}
203
204pub struct TextArea {
210 ident: Ident,
211 focus_handle: FocusHandle,
212 content: SharedString,
213 placeholder: SharedString,
214 selected_range: Range<usize>,
216 selection_reversed: bool,
217 marked_range: Option<Range<usize>>,
220 size: ControlSize,
221 disabled: bool,
222 invalid: bool,
223 required: bool,
224 read_only: bool,
225 max_length: Option<usize>,
226 rows: usize,
227 max_rows: Option<usize>,
228 visible_rows: usize,
231 scroll_offset: Pixels,
232 goal_x: Option<Pixels>,
235 is_selecting: bool,
236 last_layout: Option<Layout>,
237 last_layout_text: SharedString,
238 last_bounds: Option<Bounds<Pixels>>,
239 accessibility_revision: u64,
240 accessible_snapshot: Arc<Mutex<Option<text_edit::PublishedAccessibleText>>>,
241 _subscriptions: Vec<Subscription>,
243}
244
245impl TextArea {
246 pub fn new(ident: impl Into<Ident>, window: &mut Window, cx: &mut Context<Self>) -> Self {
247 let focus_handle = cx.focus_handle();
248 let subscriptions = vec![
249 cx.on_focus(&focus_handle, window, |_, _, cx| {
250 cx.emit(TextAreaEvent::Focus)
251 }),
252 cx.on_blur(&focus_handle, window, |_, _, cx| {
253 cx.emit(TextAreaEvent::Blur)
254 }),
255 ];
256 Self {
257 ident: ident.into(),
258 focus_handle,
259 content: SharedString::default(),
260 placeholder: SharedString::default(),
261 selected_range: 0..0,
262 selection_reversed: false,
263 marked_range: None,
264 size: ControlSize::Md,
265 disabled: false,
266 invalid: false,
267 required: false,
268 read_only: false,
269 max_length: None,
270 rows: DEFAULT_ROWS,
271 max_rows: None,
272 visible_rows: DEFAULT_ROWS,
273 scroll_offset: px(0.0),
274 goal_x: None,
275 is_selecting: false,
276 last_layout: None,
277 last_layout_text: SharedString::default(),
278 last_bounds: None,
279 accessibility_revision: 0,
280 accessible_snapshot: Arc::default(),
281 _subscriptions: subscriptions,
282 }
283 }
284
285 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
286 self.placeholder = placeholder.into();
287 self
288 }
289
290 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
292 self.content = text.into();
293 self.selected_range = self.content.len()..self.content.len();
294 self
295 }
296
297 pub fn invalid(mut self, invalid: bool) -> Self {
298 self.invalid = invalid;
299 self
300 }
301
302 pub fn required(mut self, required: bool) -> Self {
303 self.required = required;
304 self
305 }
306
307 pub fn read_only(mut self, read_only: bool) -> Self {
310 self.read_only = read_only;
311 self
312 }
313
314 pub fn rows(mut self, rows: usize) -> Self {
316 self.rows = rows.max(1);
317 self.visible_rows = self.rows;
318 self
319 }
320
321 pub fn max_rows(mut self, max_rows: usize) -> Self {
323 self.max_rows = Some(max_rows.max(1));
324 self
325 }
326
327 pub fn max_length(mut self, max_length: usize) -> Self {
329 self.max_length = Some(max_length);
330 self
331 }
332
333 pub fn value(&self) -> &SharedString {
334 &self.content
335 }
336
337 pub fn is_empty(&self) -> bool {
338 self.content.is_empty()
339 }
340
341 pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
343 self.content = value.into();
344 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
345 let end = self.content.len();
346 self.selected_range = end..end;
347 self.marked_range = None;
348 self.scroll_offset = px(0.0);
349 self.goal_x = None;
350 cx.emit(TextAreaEvent::Change(self.content.clone()));
351 cx.notify();
352 }
353
354 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
355 self.disabled = disabled;
356 if disabled {
357 self.marked_range = None;
358 self.is_selecting = false;
359 self.goal_x = None;
360 }
361 cx.notify();
362 }
363
364 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
365 self.read_only = read_only;
366 cx.notify();
367 }
368
369 pub fn set_invalid(&mut self, invalid: bool, cx: &mut Context<Self>) {
370 self.invalid = invalid;
371 cx.notify();
372 }
373
374 pub fn is_disabled(&self) -> bool {
375 self.disabled
376 }
377
378 pub fn selected_range(&self) -> Range<usize> {
379 self.selected_range.clone()
380 }
381
382 pub fn cursor_offset(&self) -> usize {
383 if self.selection_reversed {
384 self.selected_range.start
385 } else {
386 self.selected_range.end
387 }
388 }
389
390 pub fn cursor_row(&self) -> usize {
395 self.last_layout
396 .as_ref()
397 .map(|layout| layout.row_for_offset(self.cursor_offset()))
398 .unwrap_or(0)
399 }
400
401 pub(crate) fn placeholder_text(&self) -> &SharedString {
402 &self.placeholder
403 }
404
405 pub(crate) fn marked_range(&self) -> Option<Range<usize>> {
406 self.marked_range.clone()
407 }
408
409 pub(crate) fn scroll_offset(&self) -> Pixels {
410 self.scroll_offset
411 }
412
413 pub(crate) fn row_limits(&self) -> (usize, usize) {
414 (self.rows, self.max_rows.unwrap_or(self.rows).max(self.rows))
415 }
416
417 pub(crate) fn visible_rows(&self) -> usize {
418 self.visible_rows
419 }
420
421 pub(crate) fn set_visible_rows(&mut self, rows: usize) {
422 self.visible_rows = rows;
423 }
424
425 pub(crate) fn set_scroll_offset(&mut self, offset: Pixels) {
426 self.scroll_offset = offset;
427 }
428
429 pub(crate) fn set_last_layout(
430 &mut self,
431 layout: Layout,
432 text: SharedString,
433 bounds: Bounds<Pixels>,
434 ) -> bool {
435 let rows = layout.accessible_rows(&text);
436 let changed = self.last_layout_text != text
437 || self
438 .last_layout
439 .as_ref()
440 .map(|layout| layout.accessible_rows(&text))
441 != Some(rows);
442 self.last_layout = Some(layout);
443 self.last_layout_text = text;
444 self.last_bounds = Some(bounds);
445 changed
446 }
447
448 fn accessible_rows(&self) -> Option<Vec<Range<usize>>> {
449 (self.last_layout_text == self.content).then(|| {
450 self.last_layout
451 .as_ref()
452 .map(|layout| layout.accessible_rows(&self.content))
453 .unwrap_or_else(|| std::iter::once(0..0).collect())
454 })
455 }
456
457 fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
458 self.selected_range = offset..offset;
459 self.selection_reversed = false;
460 self.goal_x = None;
461 cx.notify();
462 }
463
464 fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
465 if self.selection_reversed {
466 self.selected_range.start = offset;
467 } else {
468 self.selected_range.end = offset;
469 }
470 if self.selected_range.end < self.selected_range.start {
471 self.selection_reversed = !self.selection_reversed;
472 self.selected_range = self.selected_range.end..self.selected_range.start;
473 }
474 self.goal_x = None;
475 cx.notify();
476 }
477
478 fn select_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
479 self.selected_range = range;
480 self.selection_reversed = false;
481 self.goal_x = None;
482 cx.notify();
483 }
484
485 fn previous_boundary(&self, offset: usize) -> usize {
486 text_edit::previous_boundary(&self.content, offset)
487 }
488
489 fn next_boundary(&self, offset: usize) -> usize {
490 text_edit::next_boundary(&self.content, offset)
491 }
492
493 fn previous_word_boundary(&self, offset: usize) -> usize {
494 text_edit::previous_word_boundary(&self.content, offset)
495 }
496
497 fn next_word_boundary(&self, offset: usize) -> usize {
498 text_edit::next_word_boundary(&self.content, offset)
499 }
500
501 pub(crate) fn index_for_position(&self, position: Point<Pixels>) -> usize {
502 let (Some(bounds), Some(layout)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
503 else {
504 return 0;
505 };
506 let local = point(
507 position.x - bounds.left(),
508 position.y - bounds.top() + self.scroll_offset,
509 );
510 layout.offset_for_position(local).min(self.content.len())
511 }
512
513 fn move_by_row(&mut self, delta: isize, extend: bool, cx: &mut Context<Self>) {
515 let Some(layout) = self.last_layout.as_ref() else {
516 return;
517 };
518 let caret = self.cursor_offset();
519 let position = layout.position_for_offset(caret);
520 let goal = self.goal_x.unwrap_or(position.x);
521 let row = layout.row_for_offset(caret) as isize + delta;
522 let offset = if row < 0 {
523 0
524 } else if row as usize >= layout.total_rows() {
525 self.content.len()
526 } else {
527 layout
528 .offset_at_row(row as usize, goal)
529 .min(self.content.len())
530 };
531 if extend {
532 self.select_to(offset, cx);
533 } else {
534 self.move_to(offset, cx);
535 }
536 self.goal_x = Some(goal);
537 }
538
539 fn caret_row_range(&self) -> Range<usize> {
541 let Some(layout) = self.last_layout.as_ref() else {
542 return 0..self.content.len();
543 };
544 let row = layout.row_for_offset(self.cursor_offset());
545 let range = layout.row_range(row);
546 range.start.min(self.content.len())..range.end.min(self.content.len())
547 }
548
549 fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
550 if self.selected_range.is_empty() {
551 self.move_to(self.previous_boundary(self.cursor_offset()), cx);
552 } else {
553 self.move_to(self.selected_range.start, cx);
554 }
555 }
556
557 fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
558 if self.selected_range.is_empty() {
559 self.move_to(self.next_boundary(self.cursor_offset()), cx);
560 } else {
561 self.move_to(self.selected_range.end, cx);
562 }
563 }
564
565 fn up(&mut self, _: &Up, _: &mut Window, cx: &mut Context<Self>) {
566 self.move_by_row(-1, false, cx);
567 }
568
569 fn down(&mut self, _: &Down, _: &mut Window, cx: &mut Context<Self>) {
570 self.move_by_row(1, false, cx);
571 }
572
573 fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
574 self.move_to(self.previous_word_boundary(self.cursor_offset()), cx);
575 }
576
577 fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
578 self.move_to(self.next_word_boundary(self.cursor_offset()), cx);
579 }
580
581 fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
582 self.select_to(self.previous_boundary(self.cursor_offset()), cx);
583 }
584
585 fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
586 self.select_to(self.next_boundary(self.cursor_offset()), cx);
587 }
588
589 fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
590 self.move_by_row(-1, true, cx);
591 }
592
593 fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
594 self.move_by_row(1, true, cx);
595 }
596
597 fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
598 self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
599 }
600
601 fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
602 self.select_to(self.next_word_boundary(self.cursor_offset()), cx);
603 }
604
605 fn select_to_line_start(
606 &mut self,
607 _: &SelectToLineStart,
608 _: &mut Window,
609 cx: &mut Context<Self>,
610 ) {
611 self.select_to(self.caret_row_range().start, cx);
612 }
613
614 fn select_to_line_end(&mut self, _: &SelectToLineEnd, _: &mut Window, cx: &mut Context<Self>) {
615 self.select_to(self.caret_row_range().end, cx);
616 }
617
618 fn select_to_document_start(
619 &mut self,
620 _: &SelectToDocumentStart,
621 _: &mut Window,
622 cx: &mut Context<Self>,
623 ) {
624 self.select_to(0, cx);
625 }
626
627 fn select_to_document_end(
628 &mut self,
629 _: &SelectToDocumentEnd,
630 _: &mut Window,
631 cx: &mut Context<Self>,
632 ) {
633 self.select_to(self.content.len(), cx);
634 }
635
636 fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
637 self.select_range(0..self.content.len(), cx);
638 }
639
640 fn line_start(&mut self, _: &LineStart, _: &mut Window, cx: &mut Context<Self>) {
641 self.move_to(self.caret_row_range().start, cx);
642 }
643
644 fn line_end(&mut self, _: &LineEnd, _: &mut Window, cx: &mut Context<Self>) {
645 self.move_to(self.caret_row_range().end, cx);
646 }
647
648 fn document_start(&mut self, _: &DocumentStart, _: &mut Window, cx: &mut Context<Self>) {
649 self.move_to(0, cx);
650 }
651
652 fn document_end(&mut self, _: &DocumentEnd, _: &mut Window, cx: &mut Context<Self>) {
653 self.move_to(self.content.len(), cx);
654 }
655
656 fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
657 self.replace_text_in_range(None, "\n", window, cx);
658 }
659
660 fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
661 if self.selected_range.is_empty() {
662 self.select_to(self.previous_boundary(self.cursor_offset()), cx);
663 }
664 self.replace_text_in_range(None, "", window, cx);
665 }
666
667 fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
668 if self.selected_range.is_empty() {
669 self.select_to(self.next_boundary(self.cursor_offset()), cx);
670 }
671 self.replace_text_in_range(None, "", window, cx);
672 }
673
674 fn delete_word_left(
675 &mut self,
676 _: &DeleteWordLeft,
677 window: &mut Window,
678 cx: &mut Context<Self>,
679 ) {
680 if self.selected_range.is_empty() {
681 self.select_to(self.previous_word_boundary(self.cursor_offset()), cx);
682 }
683 self.replace_text_in_range(None, "", window, cx);
684 }
685
686 fn delete_word_right(
687 &mut self,
688 _: &DeleteWordRight,
689 window: &mut Window,
690 cx: &mut Context<Self>,
691 ) {
692 if self.selected_range.is_empty() {
693 self.select_to(self.next_word_boundary(self.cursor_offset()), cx);
694 }
695 self.replace_text_in_range(None, "", window, cx);
696 }
697
698 fn delete_to_line_start(
699 &mut self,
700 _: &DeleteToLineStart,
701 window: &mut Window,
702 cx: &mut Context<Self>,
703 ) {
704 if self.selected_range.is_empty() {
705 self.select_to(self.caret_row_range().start, cx);
706 }
707 self.replace_text_in_range(None, "", window, cx);
708 }
709
710 fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
711 if self.selected_range.is_empty() {
712 return;
713 }
714 let selected = self.content[self.selected_range.clone()].to_string();
715 cx.write_to_clipboard(ClipboardItem::new_string(selected));
716 }
717
718 fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
719 if self.selected_range.is_empty() {
720 return;
721 }
722 let selected = self.content[self.selected_range.clone()].to_string();
723 cx.write_to_clipboard(ClipboardItem::new_string(selected));
724 self.replace_text_in_range(None, "", window, cx);
725 }
726
727 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
728 let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
729 return;
730 };
731 let text = text.replace("\r\n", "\n").replace('\r', "\n");
734 self.replace_text_in_range(None, &text, window, cx);
735 }
736
737 fn submit(&mut self, _: &Submit, _: &mut Window, cx: &mut Context<Self>) {
738 cx.emit(TextAreaEvent::Submit);
739 }
740
741 fn cancel(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
742 cx.emit(TextAreaEvent::Cancel);
743 }
744
745 fn show_character_palette(
746 &mut self,
747 _: &ShowCharacterPalette,
748 window: &mut Window,
749 _: &mut Context<Self>,
750 ) {
751 window.show_character_palette();
752 }
753
754 fn on_mouse_down(
755 &mut self,
756 event: &MouseDownEvent,
757 window: &mut Window,
758 cx: &mut Context<Self>,
759 ) {
760 if self.disabled {
761 return;
762 }
763 window.focus(&self.focus_handle, cx);
764 self.is_selecting = true;
765 let offset = self.index_for_position(event.position);
766 if event.modifiers.shift {
767 self.select_to(offset, cx);
768 } else if event.click_count >= 3 {
769 self.select_range(text_edit::paragraph_at(&self.content, offset), cx);
770 } else if event.click_count == 2 {
771 self.select_range(text_edit::word_at(&self.content, offset), cx);
772 } else {
773 self.move_to(offset, cx);
774 }
775 }
776
777 fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
778 if self.is_selecting {
779 self.select_to(self.index_for_position(event.position), cx);
780 }
781 }
782
783 fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
784 self.is_selecting = false;
785 }
786
787 fn offset_to_utf16(&self, offset: usize) -> usize {
788 text_edit::offset_to_utf16(&self.content, offset)
789 }
790
791 fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
792 text_edit::range_to_utf16(&self.content, range)
793 }
794
795 fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
796 text_edit::range_from_utf16(&self.content, range_utf16)
797 }
798
799 fn semantics(&self) -> NodeSpec {
800 let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::MultilineInput)
801 .disabled(self.disabled)
802 .read_only(self.read_only)
803 .invalid(self.invalid)
804 .required(self.required);
805 if !self.disabled {
806 spec = spec.focus(&self.focus_handle);
807 }
808 if !self.placeholder.is_empty() {
809 spec = spec.placeholder(self.placeholder.clone());
810 }
811 if !self.content.is_empty() {
812 spec = spec.value(self.content.clone());
813 }
814 spec
815 }
816}
817
818impl std::fmt::Debug for TextArea {
819 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820 formatter
823 .debug_struct("TextArea")
824 .field("id", &self.ident)
825 .field("size", &self.size)
826 .field("disabled", &self.disabled)
827 .field("invalid", &self.invalid)
828 .field("rows", &self.rows)
829 .field("length", &self.content.len())
830 .finish()
831 }
832}
833
834impl Disableable for TextArea {
835 fn disabled(mut self, disabled: bool) -> Self {
836 self.disabled = disabled;
837 self
838 }
839}
840
841impl Sizable for TextArea {
842 fn control_size(mut self, size: ControlSize) -> Self {
843 self.size = size;
844 self
845 }
846}
847
848impl Focusable for TextArea {
849 fn focus_handle(&self, _cx: &App) -> FocusHandle {
850 self.focus_handle.clone()
851 }
852}
853
854impl EntityInputHandler for TextArea {
855 fn text_for_range(
856 &mut self,
857 range_utf16: Range<usize>,
858 actual_range: &mut Option<Range<usize>>,
859 _window: &mut Window,
860 _cx: &mut Context<Self>,
861 ) -> Option<String> {
862 let range = self.range_from_utf16(&range_utf16);
863 actual_range.replace(self.range_to_utf16(&range));
864 Some(self.content.get(range)?.to_string())
865 }
866
867 fn selected_text_range(
868 &mut self,
869 _ignore_disabled_input: bool,
870 _window: &mut Window,
871 _cx: &mut Context<Self>,
872 ) -> Option<UTF16Selection> {
873 Some(UTF16Selection {
874 range: self.range_to_utf16(&self.selected_range),
875 reversed: self.selection_reversed,
876 })
877 }
878
879 fn marked_text_range(
880 &self,
881 _window: &mut Window,
882 _cx: &mut Context<Self>,
883 ) -> Option<Range<usize>> {
884 self.marked_range
885 .as_ref()
886 .map(|range| self.range_to_utf16(range))
887 }
888
889 fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
890 self.marked_range = None;
891 }
892
893 fn replace_text_in_range(
894 &mut self,
895 range_utf16: Option<Range<usize>>,
896 new_text: &str,
897 _window: &mut Window,
898 cx: &mut Context<Self>,
899 ) {
900 if self.disabled || self.read_only {
901 return;
902 }
903 let range = range_utf16
904 .as_ref()
905 .map(|range| self.range_from_utf16(range))
906 .or_else(|| self.marked_range.clone())
907 .unwrap_or_else(|| self.selected_range.clone());
908
909 let new_text = text_edit::normalize_multiline(new_text);
910 let new_text =
911 text_edit::fit_to_max_length(&self.content, self.max_length, &range, &new_text);
912 self.content =
913 (self.content[..range.start].to_owned() + &new_text + &self.content[range.end..])
914 .into();
915 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
916 let caret = range.start + new_text.len();
917 self.selected_range = caret..caret;
918 self.selection_reversed = false;
919 self.marked_range = None;
920 self.goal_x = None;
921 cx.emit(TextAreaEvent::Change(self.content.clone()));
922 cx.notify();
923 }
924
925 fn replace_and_mark_text_in_range(
926 &mut self,
927 range_utf16: Option<Range<usize>>,
928 new_text: &str,
929 new_selected_range_utf16: Option<Range<usize>>,
930 _window: &mut Window,
931 cx: &mut Context<Self>,
932 ) {
933 if self.disabled || self.read_only {
934 return;
935 }
936 let range = range_utf16
937 .as_ref()
938 .map(|range| self.range_from_utf16(range))
939 .or_else(|| self.marked_range.clone())
940 .unwrap_or_else(|| self.selected_range.clone());
941
942 let new_text = text_edit::normalize_multiline(new_text);
943 self.content =
944 (self.content[..range.start].to_owned() + &new_text + &self.content[range.end..])
945 .into();
946 self.accessibility_revision = self.accessibility_revision.wrapping_add(1);
947 self.marked_range =
948 (!new_text.is_empty()).then(|| range.start..range.start + new_text.len());
949 self.selected_range = new_selected_range_utf16
950 .as_ref()
951 .map(|range_utf16| self.range_from_utf16(range_utf16))
952 .map(|new_range| new_range.start + range.start..new_range.end + range.start)
953 .unwrap_or_else(|| {
954 let caret = range.start + new_text.len();
955 caret..caret
956 });
957 self.selection_reversed = false;
958 self.goal_x = None;
959 cx.emit(TextAreaEvent::Change(self.content.clone()));
960 cx.notify();
961 }
962
963 fn bounds_for_range(
964 &mut self,
965 range_utf16: Range<usize>,
966 bounds: Bounds<Pixels>,
967 _window: &mut Window,
968 _cx: &mut Context<Self>,
969 ) -> Option<Bounds<Pixels>> {
970 let layout = self.last_layout.as_ref()?;
971 let range = self.range_from_utf16(&range_utf16);
972 let start = layout.position_for_offset(range.start);
973 let end = layout.position_for_offset(range.end);
974 Some(Bounds::from_corners(
975 point(
976 bounds.left() + start.x,
977 bounds.top() + start.y - self.scroll_offset,
978 ),
979 point(
980 bounds.left() + end.x,
981 bounds.top() + end.y + layout.line_height() - self.scroll_offset,
982 ),
983 ))
984 }
985
986 fn character_index_for_point(
987 &mut self,
988 point: Point<Pixels>,
989 _window: &mut Window,
990 _cx: &mut Context<Self>,
991 ) -> Option<usize> {
992 let offset = self.index_for_position(point);
993 Some(self.offset_to_utf16(offset))
994 }
995}
996
997impl Render for TextArea {
998 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
999 if self.disabled && self.focus_handle.is_focused(window) {
1000 window.blur();
1001 }
1002 let theme = cx.theme().clone();
1003 let metrics = theme.control.get(self.size);
1004 let focused = self.focus_handle.is_focused(window);
1005 let spec = self.semantics();
1006 let content = self.content.clone();
1007 let (anchor, focus) = if self.selection_reversed {
1008 (self.selected_range.end, self.selected_range.start)
1009 } else {
1010 (self.selected_range.start, self.selected_range.end)
1011 };
1012 let accessible_snapshot = self.accessible_snapshot.clone();
1013 let selection_representable = text_edit::accessible_text_is_representable(&content);
1014 let accessible_rows = self.accessible_rows();
1015 let accessibility_revision = self.accessibility_revision;
1016 let entity = cx.entity().clone();
1017 let accessible_direction = if cx.layout_direction().is_rtl() {
1018 gpui::accesskit::TextDirection::RightToLeft
1019 } else {
1020 gpui::accesskit::TextDirection::LeftToRight
1021 };
1022
1023 div()
1024 .id(self.ident.element_id())
1025 .key_context(KEY_CONTEXT)
1026 .when(!self.disabled, |element| {
1027 element.track_focus(&self.focus_handle)
1028 })
1029 .when(!self.disabled && !self.read_only, |element| {
1030 element
1031 .on_action(cx.listener(Self::backspace))
1032 .on_action(cx.listener(Self::delete))
1033 .on_action(cx.listener(Self::delete_word_left))
1034 .on_action(cx.listener(Self::delete_word_right))
1035 .on_action(cx.listener(Self::delete_to_line_start))
1036 .on_action(cx.listener(Self::left))
1037 .on_action(cx.listener(Self::right))
1038 .on_action(cx.listener(Self::up))
1039 .on_action(cx.listener(Self::down))
1040 .on_action(cx.listener(Self::word_left))
1041 .on_action(cx.listener(Self::word_right))
1042 .on_action(cx.listener(Self::select_left))
1043 .on_action(cx.listener(Self::select_right))
1044 .on_action(cx.listener(Self::select_up))
1045 .on_action(cx.listener(Self::select_down))
1046 .on_action(cx.listener(Self::select_word_left))
1047 .on_action(cx.listener(Self::select_word_right))
1048 .on_action(cx.listener(Self::select_to_line_start))
1049 .on_action(cx.listener(Self::select_to_line_end))
1050 .on_action(cx.listener(Self::select_to_document_start))
1051 .on_action(cx.listener(Self::select_to_document_end))
1052 .on_action(cx.listener(Self::select_all))
1053 .on_action(cx.listener(Self::line_start))
1054 .on_action(cx.listener(Self::line_end))
1055 .on_action(cx.listener(Self::document_start))
1056 .on_action(cx.listener(Self::document_end))
1057 .on_action(cx.listener(Self::newline))
1058 .on_action(cx.listener(Self::copy))
1059 .on_action(cx.listener(Self::cut))
1060 .on_action(cx.listener(Self::paste))
1061 .on_action(cx.listener(Self::submit))
1062 .on_action(cx.listener(Self::cancel))
1063 .on_action(cx.listener(Self::show_character_palette))
1064 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1065 .on_mouse_move(cx.listener(Self::on_mouse_move))
1066 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
1067 .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
1068 .cursor(CursorStyle::IBeam)
1069 })
1070 .when_some(accessible_rows.clone(), move |element, accessible_rows| {
1071 element.a11y_synthetic_children(move |builder| {
1072 let snapshot = text_edit::publish_accessible_text(
1073 builder,
1074 &content,
1075 anchor,
1076 focus,
1077 accessible_direction,
1078 &accessible_rows,
1079 accessibility_revision,
1080 );
1081 *accessible_snapshot
1082 .lock()
1083 .unwrap_or_else(|poisoned| poisoned.into_inner()) = snapshot;
1084 })
1085 })
1086 .when(
1087 !self.disabled && selection_representable && accessible_rows.is_some(),
1088 |element| {
1089 let selection_entity = entity.clone();
1090 let selection_snapshot = self.accessible_snapshot.clone();
1091 element.on_a11y_action(
1092 AccessibleAction::SetTextSelection,
1093 move |data, _, cx| {
1094 let Some(ActionData::SetTextSelection(selection)) = data else {
1095 return;
1096 };
1097 let published = selection_snapshot
1098 .lock()
1099 .unwrap_or_else(|poisoned| poisoned.into_inner())
1100 .clone();
1101 selection_entity.update(cx, |area, cx| {
1102 if area.disabled {
1103 return;
1104 }
1105 let Some(published) = published.as_ref() else {
1106 return;
1107 };
1108 let Some(anchor) = text_edit::byte_offset_for_published_position(
1109 &area.content,
1110 area.accessibility_revision,
1111 published,
1112 selection.anchor,
1113 ) else {
1114 return;
1115 };
1116 let Some(focus) = text_edit::byte_offset_for_published_position(
1117 &area.content,
1118 area.accessibility_revision,
1119 published,
1120 selection.focus,
1121 ) else {
1122 return;
1123 };
1124 area.selected_range = anchor.min(focus)..anchor.max(focus);
1125 area.selection_reversed = focus < anchor;
1126 area.marked_range = None;
1127 area.goal_x = None;
1128 cx.notify();
1129 });
1130 },
1131 )
1132 },
1133 )
1134 .when(!self.disabled && !self.read_only, |element| {
1135 element.on_a11y_action(AccessibleAction::SetValue, move |data, window, cx| {
1136 let Some(ActionData::Value(value)) = data else {
1137 return;
1138 };
1139 entity.update(cx, |area, cx| {
1140 if area.disabled || area.read_only {
1141 return;
1142 }
1143 let end = text_edit::offset_to_utf16(&area.content, area.content.len());
1144 area.replace_text_in_range(Some(0..end), value, window, cx);
1145 });
1146 })
1147 })
1148 .w_full()
1149 .column()
1150 .px(px(metrics.padding_x))
1151 .py(px(theme.spacing.xs))
1152 .radius(&theme, Radius::Control)
1153 .well(&theme)
1154 .when(self.invalid, |element| {
1155 element.border_color(theme.colors.danger)
1156 })
1157 .when(focused, |element| element.shadow(theme.focus_ring()))
1158 .text_size(px(metrics.font_size))
1159 .text_color(if self.disabled {
1160 theme.colors.text_faint
1161 } else {
1162 theme.colors.text
1163 })
1164 .child(TextAreaElement::new(cx.entity()))
1165 .semantic_in(cx, spec)
1166 }
1167}