Skip to main content

gpui_kit/controls/textarea/
mod.rs

1//! A multi-line editable text control.
2//!
3//! `TextArea` is a view rather than a `RenderOnce` builder, for the same
4//! reason [`crate::controls::input::TextInput`] is: the caret, the selection,
5//! the in-progress input method composition, and the vertical scroll position
6//! all outlive a frame. Text wraps at the width the control was given, so
7//! there is no horizontal scrolling, and the caret moves by visual line.
8//!
9//! ```no_run
10//! # use gpui::{App, AppContext as _, Context, Window};
11//! # use gpui_kit::controls::textarea::{TextArea, TextAreaEvent};
12//! # struct Host;
13//! # fn example(window: &mut Window, cx: &mut Context<Host>) {
14//! let notes = cx.new(|cx| {
15//!     TextArea::new("review.notes", window, cx)
16//!         .placeholder("What changed, and why")
17//!         .rows(4)
18//!         .max_rows(12)
19//! });
20//! cx.subscribe(&notes, |_host, notes, event, cx| {
21//!     if let TextAreaEvent::Submit = event {
22//!         let _typed = notes.read(cx).value().to_string();
23//!     }
24//! })
25//! .detach();
26//! # }
27//! ```
28
29mod 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
89/// The key context every text area publishes, so a host can layer its own
90/// bindings on top without re-declaring these.
91pub const KEY_CONTEXT: &str = "TextArea";
92
93/// The visible rows a text area occupies when the caller asks for none.
94const DEFAULT_ROWS: usize = 3;
95
96/// Installs the editing key bindings.
97///
98/// Called by [`crate::install`]. Bindings are scoped to the text area key
99/// context, so they never shadow a host's global shortcuts.
100pub(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        // Enter belongs to the text here; a submission is the modified chord.
128        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/// What a text area reports to its owner.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum TextAreaEvent {
192    /// The text changed, by typing, deletion, paste, or a programmatic set.
193    Change(SharedString),
194    /// The submit chord was pressed while the area had focus.
195    Submit,
196    /// Editing was abandoned with the cancel key.
197    Cancel,
198    Focus,
199    Blur,
200}
201
202impl EventEmitter<TextAreaEvent> for TextArea {}
203
204/// Wrapped, multi-line editable text.
205///
206/// Enter inserts a line and the platform modifier plus enter submits. Motion
207/// follows visual rows with a preserved goal column, and the frame grows from
208/// `rows` to `max_rows` before it scrolls rather than pushing the page around.
209pub struct TextArea {
210    ident: Ident,
211    focus_handle: FocusHandle,
212    content: SharedString,
213    placeholder: SharedString,
214    /// A caret is an empty selection, so one range describes both.
215    selected_range: Range<usize>,
216    selection_reversed: bool,
217    /// The range the input method is currently composing, which is underlined
218    /// and replaced wholesale as composition continues.
219    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    /// The rows the frame decided to occupy, which grows with the text until
229    /// `max_rows` and is measured rather than guessed.
230    visible_rows: usize,
231    scroll_offset: Pixels,
232    /// The horizontal position vertical motion aims for, so a run of up or
233    /// down keys through a short line does not drag the caret leftwards.
234    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    /// Held so the focus listeners live as long as the area does.
242    _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    /// Seeds the initial text, with the caret at the end.
291    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    /// Keeps the value focusable and exposed while refusing keyboard,
308    /// pointer, IME, and accessibility value changes.
309    pub fn read_only(mut self, read_only: bool) -> Self {
310        self.read_only = read_only;
311        self
312    }
313
314    /// The rows the area occupies before it has anything longer to show.
315    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    /// Grows with the text up to this many rows, then scrolls instead.
322    pub fn max_rows(mut self, max_rows: usize) -> Self {
323        self.max_rows = Some(max_rows.max(1));
324        self
325    }
326
327    /// Truncates input past a length in bytes of UTF-8.
328    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    /// Replaces the text from the host side, for example when a form resets.
342    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    /// The visual row the caret sits on, counting wrapped rows.
391    ///
392    /// Zero until the area has been laid out once, because a wrapped row only
393    /// exists once a width is known.
394    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    /// Moves the caret by whole visual rows, keeping the column it aimed for.
514    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    /// The bounds of the visual row the caret sits on, in content offsets.
540    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        // Line breaks survive a paste here, but only in one shape, so the
732        // stored text never depends on where it was copied from.
733        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        // The content is deliberately absent: an area holds whatever a person
821        // wrote, and a debug log is not a place for it.
822        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}