Skip to main content

gpui_rhai/
text_area.rs

1use std::collections::BTreeMap;
2use std::ops::Range;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use gpui::{
7    App, Bounds, ClipboardItem, Context, CursorStyle, Element, ElementId, ElementInputHandler,
8    Entity, EntityInputHandler, FocusHandle, Focusable, FontFallbacks, FontWeight, GlobalElementId,
9    IntoElement, KeyBinding, KeyDownEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent,
10    MouseUpEvent, PaintQuad, Pixels, Point, Render, ScrollHandle, SharedString, Style as GpuiStyle,
11    TextAlign, TextRun, UTF16Selection, UnderlineStyle, Window, WrappedLine, actions, div, fill,
12    point, prelude::*, px, relative, rgba, size,
13};
14use rhai::{Engine, FuncRegistration, INT, ImmutableString};
15
16use crate::text_edit::TextBuffer;
17use crate::text_input::{NativeTypography, TextInputCallbacks, native_typography};
18use crate::{
19    ComponentStateSchema, EventSchema, ObjectField, PrimitiveContext, PrimitiveDescriptor,
20    PrimitiveHandler, PrimitiveId, PrimitiveInstance, PrimitiveInstanceId, PrimitiveProps,
21    PrimitiveValue, Rgba8, UiValue, ValueSchema,
22};
23
24const MAX_TEXTAREA_ROWS: i64 = 1_000;
25const MAX_TEXTAREA_GRAPHEMES: i64 = 1_000_000;
26
27actions!(
28    gpui_rhai_textarea,
29    [
30        Backspace,
31        Delete,
32        Left,
33        Right,
34        Up,
35        Down,
36        SelectLeft,
37        SelectRight,
38        SelectUp,
39        SelectDown,
40        SelectAll,
41        Home,
42        End,
43        Newline,
44        ShowCharacterPalette,
45        Paste,
46        Cut,
47        Copy,
48        Undo,
49        Redo,
50    ]
51);
52
53pub fn init_text_area(cx: &mut App) {
54    cx.bind_keys([
55        KeyBinding::new("backspace", Backspace, Some("GPUIRhaiTextarea")),
56        KeyBinding::new("delete", Delete, Some("GPUIRhaiTextarea")),
57        KeyBinding::new("left", Left, Some("GPUIRhaiTextarea")),
58        KeyBinding::new("right", Right, Some("GPUIRhaiTextarea")),
59        KeyBinding::new("up", Up, Some("GPUIRhaiTextarea")),
60        KeyBinding::new("down", Down, Some("GPUIRhaiTextarea")),
61        KeyBinding::new("shift-left", SelectLeft, Some("GPUIRhaiTextarea")),
62        KeyBinding::new("shift-right", SelectRight, Some("GPUIRhaiTextarea")),
63        KeyBinding::new("shift-up", SelectUp, Some("GPUIRhaiTextarea")),
64        KeyBinding::new("shift-down", SelectDown, Some("GPUIRhaiTextarea")),
65        KeyBinding::new("cmd-a", SelectAll, Some("GPUIRhaiTextarea")),
66        KeyBinding::new("cmd-v", Paste, Some("GPUIRhaiTextarea")),
67        KeyBinding::new("cmd-c", Copy, Some("GPUIRhaiTextarea")),
68        KeyBinding::new("cmd-x", Cut, Some("GPUIRhaiTextarea")),
69        KeyBinding::new("cmd-z", Undo, Some("GPUIRhaiTextarea")),
70        KeyBinding::new("shift-cmd-z", Redo, Some("GPUIRhaiTextarea")),
71        KeyBinding::new("home", Home, Some("GPUIRhaiTextarea")),
72        KeyBinding::new("end", End, Some("GPUIRhaiTextarea")),
73        KeyBinding::new("enter", Newline, Some("GPUIRhaiTextarea")),
74        KeyBinding::new(
75            "ctrl-cmd-space",
76            ShowCharacterPalette,
77            Some("GPUIRhaiTextarea"),
78        ),
79    ]);
80}
81
82pub(crate) fn register_text_area_api(engine: &mut Engine) {
83    FuncRegistration::new("grapheme_count")
84        .in_global_namespace()
85        .register_into_engine(engine, |value: ImmutableString| -> INT {
86            INT::try_from(
87                unicode_segmentation::UnicodeSegmentation::graphemes(value.as_str(), true).count(),
88            )
89            .unwrap_or(INT::MAX)
90        });
91}
92
93#[derive(Clone, Debug)]
94struct TextAreaConfig {
95    placeholder: SharedString,
96    disabled: bool,
97    read_only: bool,
98    min_rows: usize,
99    max_rows: usize,
100    rows: Option<usize>,
101    max_length: Option<usize>,
102    typography: NativeTypography,
103    autofocus: bool,
104    placeholder_color: Rgba8,
105    selection_color: Rgba8,
106    caret_color: Rgba8,
107    scroll_color: Option<Rgba8>,
108}
109
110impl TextAreaConfig {
111    fn viewport_rows(&self, measured: usize) -> usize {
112        self.rows
113            .unwrap_or_else(|| measured.clamp(self.min_rows, self.max_rows))
114    }
115}
116
117pub(crate) struct TextAreaEntity {
118    focus: FocusHandle,
119    buffer: TextBuffer,
120    config: TextAreaConfig,
121    callbacks: TextInputCallbacks,
122    layout: Option<TextAreaLayout>,
123    measured_rows: usize,
124    last_bounds: Option<Bounds<Pixels>>,
125    preferred_x: Option<Pixels>,
126    selecting: bool,
127    scroll: ScrollHandle,
128}
129
130impl TextAreaEntity {
131    fn new(
132        value: &str,
133        config: TextAreaConfig,
134        callbacks: TextInputCallbacks,
135        cx: &mut Context<Self>,
136    ) -> Result<Self, String> {
137        validate_text_area_value(value, &config)?;
138        let mut buffer = TextBuffer::new(value);
139        buffer.set_max_length(config.max_length);
140        Ok(Self {
141            focus: cx.focus_handle().tab_stop(!config.disabled),
142            buffer,
143            measured_rows: config.min_rows,
144            config,
145            callbacks,
146            layout: None,
147            last_bounds: None,
148            preferred_x: None,
149            selecting: false,
150            scroll: ScrollHandle::new(),
151        })
152    }
153
154    fn update_props(
155        &mut self,
156        value: &str,
157        config: TextAreaConfig,
158        callbacks: TextInputCallbacks,
159        cx: &mut Context<Self>,
160    ) -> Result<(), String> {
161        validate_text_area_value(value, &config)?;
162        if self.buffer.marked().is_none() {
163            self.buffer.set_controlled(value);
164        }
165        self.buffer.set_max_length(config.max_length);
166        self.focus = self.focus.clone().tab_stop(!config.disabled);
167        self.config = config;
168        self.callbacks = callbacks;
169        cx.notify();
170        Ok(())
171    }
172
173    fn emit_change(&self, window: &mut Window, cx: &mut Context<Self>) {
174        if let Some(change) = self.callbacks.change.clone() {
175            let value = self.buffer.content.clone();
176            window.defer(cx, move |window, cx| change(value, window, cx));
177        }
178    }
179
180    fn reset_preferred_x(&mut self) {
181        self.preferred_x = None;
182    }
183
184    fn ensure_cursor_visible(&self) {
185        let Some(layout) = &self.layout else {
186            return;
187        };
188        let Some(position) = layout.position_for_index(self.buffer.cursor_offset()) else {
189            return;
190        };
191        let viewport =
192            self.config.typography.line_height * self.config.viewport_rows(self.measured_rows);
193        let mut top = (-self.scroll.offset().y).max(px(0.0));
194        if position.y < top {
195            top = position.y;
196        } else if position.y + self.config.typography.line_height > top + viewport {
197            top = position.y + self.config.typography.line_height - viewport;
198        }
199        self.scroll.set_offset(point(px(0.0), -top));
200    }
201
202    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
203        let offset = if self.buffer.selected.is_empty() {
204            self.buffer.previous_boundary(self.buffer.cursor_offset())
205        } else {
206            self.buffer.selected.start
207        };
208        self.buffer.move_to(offset);
209        self.reset_preferred_x();
210        self.ensure_cursor_visible();
211        cx.notify();
212    }
213
214    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
215        let offset = if self.buffer.selected.is_empty() {
216            self.buffer.next_boundary(self.buffer.selected.end)
217        } else {
218            self.buffer.selected.end
219        };
220        self.buffer.move_to(offset);
221        self.reset_preferred_x();
222        self.ensure_cursor_visible();
223        cx.notify();
224    }
225
226    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
227        let offset = self.buffer.previous_boundary(self.buffer.cursor_offset());
228        self.buffer.select_to(offset);
229        self.reset_preferred_x();
230        self.ensure_cursor_visible();
231        cx.notify();
232    }
233
234    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
235        let offset = self.buffer.next_boundary(self.buffer.cursor_offset());
236        self.buffer.select_to(offset);
237        self.reset_preferred_x();
238        self.ensure_cursor_visible();
239        cx.notify();
240    }
241
242    fn up(&mut self, _: &Up, _: &mut Window, cx: &mut Context<Self>) {
243        self.move_vertical(-1, false);
244        cx.notify();
245    }
246
247    fn down(&mut self, _: &Down, _: &mut Window, cx: &mut Context<Self>) {
248        self.move_vertical(1, false);
249        cx.notify();
250    }
251
252    fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
253        self.move_vertical(-1, true);
254        cx.notify();
255    }
256
257    fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
258        self.move_vertical(1, true);
259        cx.notify();
260    }
261
262    fn move_vertical(&mut self, step: i32, extend: bool) {
263        let Some(layout) = &self.layout else {
264            return;
265        };
266        let cursor = self.buffer.cursor_offset();
267        let Some(position) = layout.position_for_index(cursor) else {
268            return;
269        };
270        let x = self.preferred_x.unwrap_or(position.x);
271        self.preferred_x = Some(x);
272        let target_y = if step.is_negative() {
273            position.y - self.config.typography.line_height
274        } else {
275            position.y + self.config.typography.line_height
276        };
277        let offset = layout.closest_index_for_position(point(x, target_y));
278        if extend {
279            self.buffer.select_to(offset);
280        } else {
281            self.buffer.move_to(offset);
282        }
283        self.ensure_cursor_visible();
284    }
285
286    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
287        self.buffer.move_to(0);
288        self.buffer.select_to(self.buffer.content.len());
289        self.reset_preferred_x();
290        self.ensure_cursor_visible();
291        cx.notify();
292    }
293
294    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
295        self.buffer
296            .move_to(self.buffer.line_start(self.buffer.cursor_offset()));
297        self.reset_preferred_x();
298        self.ensure_cursor_visible();
299        cx.notify();
300    }
301
302    fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
303        self.buffer
304            .move_to(self.buffer.line_end(self.buffer.cursor_offset()));
305        self.reset_preferred_x();
306        self.ensure_cursor_visible();
307        cx.notify();
308    }
309
310    fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
311        if self.config.disabled || self.config.read_only {
312            return;
313        }
314        if self.buffer.selected.is_empty() {
315            let previous = self.buffer.previous_boundary(self.buffer.cursor_offset());
316            self.buffer.select_to(previous);
317        }
318        self.buffer.replace(None, "");
319        self.reset_preferred_x();
320        self.ensure_cursor_visible();
321        self.emit_change(window, cx);
322        cx.notify();
323    }
324
325    fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
326        if self.config.disabled || self.config.read_only {
327            return;
328        }
329        if self.buffer.selected.is_empty() {
330            let next = self.buffer.next_boundary(self.buffer.cursor_offset());
331            self.buffer.select_to(next);
332        }
333        self.buffer.replace(None, "");
334        self.reset_preferred_x();
335        self.ensure_cursor_visible();
336        self.emit_change(window, cx);
337        cx.notify();
338    }
339
340    fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
341        if self.config.disabled || self.config.read_only {
342            return;
343        }
344        self.buffer.replace(None, "\n");
345        self.reset_preferred_x();
346        self.ensure_cursor_visible();
347        self.emit_change(window, cx);
348        cx.notify();
349    }
350
351    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
352        if !self.buffer.selected.is_empty() {
353            cx.write_to_clipboard(ClipboardItem::new_string(
354                self.buffer.content[self.buffer.selected.clone()].to_owned(),
355            ));
356        }
357    }
358
359    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
360        if self.config.disabled || self.config.read_only || self.buffer.selected.is_empty() {
361            return;
362        }
363        self.copy(&Copy, window, cx);
364        self.buffer.replace(None, "");
365        self.ensure_cursor_visible();
366        self.emit_change(window, cx);
367        cx.notify();
368    }
369
370    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
371        if self.config.disabled || self.config.read_only {
372            return;
373        }
374        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
375            self.buffer.replace(None, &text);
376            self.ensure_cursor_visible();
377            self.emit_change(window, cx);
378            cx.notify();
379        }
380    }
381
382    fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
383        if !self.config.disabled && !self.config.read_only && self.buffer.undo() {
384            self.reset_preferred_x();
385            self.ensure_cursor_visible();
386            self.emit_change(window, cx);
387            cx.notify();
388        }
389    }
390
391    fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
392        if !self.config.disabled && !self.config.read_only && self.buffer.redo() {
393            self.reset_preferred_x();
394            self.ensure_cursor_visible();
395            self.emit_change(window, cx);
396            cx.notify();
397        }
398    }
399
400    fn show_character_palette(
401        &mut self,
402        _: &ShowCharacterPalette,
403        window: &mut Window,
404        _: &mut Context<Self>,
405    ) {
406        if !self.config.disabled {
407            window.show_character_palette();
408        }
409    }
410
411    fn index_for_mouse(&self, position: Point<Pixels>) -> usize {
412        let (Some(bounds), Some(layout)) = (&self.last_bounds, &self.layout) else {
413            return 0;
414        };
415        layout.closest_index_for_position(position - bounds.origin)
416    }
417
418    fn mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
419        if self.config.disabled {
420            return;
421        }
422        self.focus.focus(window, cx);
423        self.selecting = true;
424        let index = self.index_for_mouse(event.position);
425        if event.modifiers.shift {
426            self.buffer.select_to(index);
427        } else {
428            self.buffer.move_to(index);
429        }
430        self.reset_preferred_x();
431        self.ensure_cursor_visible();
432        cx.notify();
433    }
434
435    fn mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
436        self.selecting = false;
437    }
438
439    fn mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
440        if self.selecting {
441            let index = self.index_for_mouse(event.position);
442            self.buffer.select_to(index);
443            self.reset_preferred_x();
444            self.ensure_cursor_visible();
445            cx.notify();
446        }
447    }
448}
449
450impl EntityInputHandler for TextAreaEntity {
451    fn text_for_range(
452        &mut self,
453        range_utf16: Range<usize>,
454        actual_range: &mut Option<Range<usize>>,
455        _: &mut Window,
456        _: &mut Context<Self>,
457    ) -> Option<String> {
458        let range = self.buffer.range_from_utf16(&range_utf16);
459        actual_range.replace(self.buffer.range_to_utf16(&range));
460        self.buffer.content.get(range).map(ToOwned::to_owned)
461    }
462
463    fn selected_text_range(
464        &mut self,
465        _ignore_disabled_input: bool,
466        _: &mut Window,
467        _: &mut Context<Self>,
468    ) -> Option<UTF16Selection> {
469        Some(UTF16Selection {
470            range: self.buffer.range_to_utf16(&self.buffer.selected),
471            reversed: self.buffer.selection_reversed,
472        })
473    }
474
475    fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
476        self.buffer
477            .marked
478            .as_ref()
479            .map(|range| self.buffer.range_to_utf16(range))
480    }
481
482    fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {
483        let was_marked = self.buffer.marked().is_some();
484        self.buffer.unmark();
485        if was_marked {
486            self.emit_change(window, cx);
487            cx.notify();
488        }
489    }
490
491    fn replace_text_in_range(
492        &mut self,
493        range_utf16: Option<Range<usize>>,
494        text: &str,
495        window: &mut Window,
496        cx: &mut Context<Self>,
497    ) {
498        if self.config.disabled || self.config.read_only {
499            return;
500        }
501        self.buffer.replace(range_utf16.as_ref(), text);
502        self.emit_change(window, cx);
503        cx.notify();
504    }
505
506    fn replace_and_mark_text_in_range(
507        &mut self,
508        range_utf16: Option<Range<usize>>,
509        text: &str,
510        selected_utf16: Option<Range<usize>>,
511        _: &mut Window,
512        cx: &mut Context<Self>,
513    ) {
514        if self.config.disabled || self.config.read_only {
515            return;
516        }
517        self.buffer
518            .replace_and_mark(range_utf16.as_ref(), text, selected_utf16);
519        cx.notify();
520    }
521
522    fn bounds_for_range(
523        &mut self,
524        range_utf16: Range<usize>,
525        bounds: Bounds<Pixels>,
526        _: &mut Window,
527        _: &mut Context<Self>,
528    ) -> Option<Bounds<Pixels>> {
529        let layout = self.layout.as_ref()?;
530        let range = self.buffer.range_from_utf16(&range_utf16);
531        let start = layout.position_for_index(range.start)?;
532        let end = layout.position_for_index(range.end)?;
533        Some(Bounds::from_corners(
534            bounds.origin + start,
535            bounds.origin
536                + point(
537                    end.x.max(start.x + px(1.0)),
538                    end.y + self.config.typography.line_height,
539                ),
540        ))
541    }
542
543    fn character_index_for_point(
544        &mut self,
545        point: Point<Pixels>,
546        _: &mut Window,
547        _: &mut Context<Self>,
548    ) -> Option<usize> {
549        let bounds = self.last_bounds?;
550        let index = self
551            .layout
552            .as_ref()?
553            .closest_index_for_position(point - bounds.origin);
554        Some(self.buffer.offset_to_utf16(index))
555    }
556}
557
558#[derive(Clone, Debug)]
559struct TextAreaLayout {
560    lines: Vec<Arc<WrappedLine>>,
561    starts: Vec<usize>,
562    row_starts: Vec<usize>,
563    line_height: Pixels,
564}
565
566impl TextAreaLayout {
567    fn new(lines: Vec<WrappedLine>, content: &str, line_height: Pixels) -> Self {
568        let mut starts = Vec::with_capacity(lines.len());
569        let mut offset = 0;
570        for line in content.split('\n') {
571            starts.push(offset);
572            offset += line.len() + 1;
573        }
574        while starts.len() < lines.len() {
575            starts.push(content.len());
576        }
577        starts.truncate(lines.len());
578        let mut row_starts = Vec::with_capacity(lines.len());
579        let mut row = 0;
580        for line in &lines {
581            row_starts.push(row);
582            row += line.wrap_boundaries().len() + 1;
583        }
584        Self {
585            lines: lines.into_iter().map(Arc::new).collect(),
586            starts,
587            row_starts,
588            line_height,
589        }
590    }
591
592    fn visual_rows(&self) -> usize {
593        self.lines
594            .iter()
595            .map(|line| line.wrap_boundaries().len() + 1)
596            .sum::<usize>()
597            .max(1)
598    }
599
600    fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
601        let line_index = self
602            .starts
603            .iter()
604            .enumerate()
605            .rev()
606            .find_map(|(line, start)| (*start <= index).then_some(line))?;
607        let line = &self.lines[line_index];
608        let local = index
609            .saturating_sub(self.starts[line_index])
610            .min(line.len());
611        let mut position = line.position_for_index(local, self.line_height)?;
612        position.y += self.line_height * self.row_starts[line_index];
613        Some(position)
614    }
615
616    fn closest_index_for_position(&self, position: Point<Pixels>) -> usize {
617        let row = pixels_to_usize((position.y / self.line_height).max(0.0));
618        let line_index = self
619            .row_starts
620            .iter()
621            .enumerate()
622            .rev()
623            .find_map(|(line, start)| (*start <= row).then_some(line))
624            .unwrap_or(0);
625        let line = &self.lines[line_index];
626        let local_y = position.y - self.line_height * self.row_starts[line_index];
627        let local = line
628            .closest_index_for_position(point(position.x, local_y), self.line_height)
629            .unwrap_or_else(|boundary| boundary);
630        self.starts[line_index] + local.min(line.len())
631    }
632
633    fn selection_quads(
634        &self,
635        selection: &Range<usize>,
636        origin: Point<Pixels>,
637        color: Rgba8,
638    ) -> Vec<PaintQuad> {
639        if selection.is_empty() {
640            return Vec::new();
641        }
642        let mut quads = Vec::new();
643        for (line_index, line) in self.lines.iter().enumerate() {
644            let global_start = self.starts[line_index];
645            let global_end = global_start + line.len();
646            if selection.end < global_start || selection.start > global_end {
647                continue;
648            }
649            for (visual_row, local_range) in visual_ranges(line).into_iter().enumerate() {
650                let row_global = global_start + local_range.start..global_start + local_range.end;
651                let start = selection.start.max(row_global.start);
652                let end = selection.end.min(row_global.end);
653                if start >= end {
654                    continue;
655                }
656                let row_start_x = line.unwrapped_layout.x_for_index(local_range.start);
657                let x1 = line.unwrapped_layout.x_for_index(start - global_start) - row_start_x;
658                let x2 = line.unwrapped_layout.x_for_index(end - global_start) - row_start_x;
659                let y = self.line_height * (self.row_starts[line_index] + visual_row);
660                quads.push(fill(
661                    Bounds::new(
662                        origin + point(x1, y),
663                        size((x2 - x1).max(px(1.0)), self.line_height),
664                    ),
665                    rgba(color.as_rgba_hex()),
666                ));
667            }
668        }
669        quads
670    }
671}
672
673fn visual_ranges(line: &WrappedLine) -> Vec<Range<usize>> {
674    let mut starts = vec![0];
675    for boundary in line.wrap_boundaries() {
676        let run = &line.runs()[boundary.run_ix];
677        starts.push(run.glyphs[boundary.glyph_ix].index);
678    }
679    let mut ranges = starts
680        .windows(2)
681        .map(|window| window[0]..window[1])
682        .collect::<Vec<_>>();
683    ranges.push(starts.last().copied().unwrap_or(0)..line.len());
684    ranges
685}
686
687struct TextAreaElement {
688    input: Entity<TextAreaEntity>,
689}
690
691struct TextAreaPrepaint {
692    layout: TextAreaLayout,
693    cursor: Option<PaintQuad>,
694    selection: Vec<PaintQuad>,
695}
696
697impl IntoElement for TextAreaElement {
698    type Element = Self;
699
700    fn into_element(self) -> Self::Element {
701        self
702    }
703}
704
705impl Element for TextAreaElement {
706    type RequestLayoutState = ();
707    type PrepaintState = TextAreaPrepaint;
708
709    fn id(&self) -> Option<ElementId> {
710        None
711    }
712
713    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
714        None
715    }
716
717    fn request_layout(
718        &mut self,
719        _: Option<&GlobalElementId>,
720        _: Option<&gpui::InspectorElementId>,
721        window: &mut Window,
722        cx: &mut App,
723    ) -> (LayoutId, ()) {
724        let input = self.input.read(cx);
725        let rows = input
726            .layout
727            .as_ref()
728            .map_or(input.measured_rows, TextAreaLayout::visual_rows);
729        let mut style = GpuiStyle::default();
730        style.size.width = relative(1.0).into();
731        style.size.height = (input.config.typography.line_height * rows).into();
732        (window.request_layout(style, [], cx), ())
733    }
734
735    fn prepaint(
736        &mut self,
737        _: Option<&GlobalElementId>,
738        _: Option<&gpui::InspectorElementId>,
739        bounds: Bounds<Pixels>,
740        (): &mut (),
741        window: &mut Window,
742        cx: &mut App,
743    ) -> TextAreaPrepaint {
744        let input = self.input.read(cx);
745        let content: SharedString = if input.buffer.content.is_empty() {
746            input.config.placeholder.clone()
747        } else {
748            input.buffer.content.clone().into()
749        };
750        let text_style = window.text_style();
751        let color = if input.buffer.content.is_empty() {
752            rgba(input.config.placeholder_color.as_rgba_hex()).into()
753        } else {
754            text_style.color
755        };
756        let base = TextRun {
757            len: content.len(),
758            font: text_style.font(),
759            color,
760            background_color: None,
761            underline: None,
762            strikethrough: None,
763        };
764        let runs = marked_runs(&base, input.buffer.marked.as_ref(), content.len());
765        let lines = window
766            .text_system()
767            .shape_text(
768                content,
769                input.config.typography.font_size,
770                &runs,
771                Some(bounds.size.width),
772                None,
773            )
774            .unwrap_or_default()
775            .into_vec();
776        let lines = if lines.is_empty() {
777            window
778                .text_system()
779                .shape_text(
780                    " ".into(),
781                    input.config.typography.font_size,
782                    &[base],
783                    Some(bounds.size.width),
784                    None,
785                )
786                .unwrap_or_default()
787                .into_vec()
788        } else {
789            lines
790        };
791        let layout = TextAreaLayout::new(
792            lines,
793            &input.buffer.content,
794            input.config.typography.line_height,
795        );
796        let selection = layout.selection_quads(
797            &input.buffer.selected,
798            bounds.origin,
799            input.config.selection_color,
800        );
801        let cursor = input
802            .focus
803            .is_focused(window)
804            .then(|| layout.position_for_index(input.buffer.cursor_offset()))
805            .flatten()
806            .map(|position| {
807                fill(
808                    Bounds::new(
809                        bounds.origin + position,
810                        size(px(1.5), input.config.typography.line_height),
811                    ),
812                    rgba(input.config.caret_color.as_rgba_hex()),
813                )
814            });
815        self.input.update(cx, |input, cx| {
816            let visual_rows = layout.visual_rows();
817            let measured = input.config.viewport_rows(visual_rows);
818            if measured != input.measured_rows {
819                input.measured_rows = measured;
820                cx.notify();
821            }
822            input.last_bounds = Some(bounds);
823            input.layout = Some(layout.clone());
824        });
825        TextAreaPrepaint {
826            layout,
827            cursor,
828            selection,
829        }
830    }
831
832    fn paint(
833        &mut self,
834        _: Option<&GlobalElementId>,
835        _: Option<&gpui::InspectorElementId>,
836        bounds: Bounds<Pixels>,
837        (): &mut (),
838        state: &mut TextAreaPrepaint,
839        window: &mut Window,
840        cx: &mut App,
841    ) {
842        let input = self.input.read(cx);
843        let focus = input.focus.clone();
844        let line_height = input.config.typography.line_height;
845        window.handle_input(
846            &focus,
847            ElementInputHandler::new(bounds, self.input.clone()),
848            cx,
849        );
850        for quad in state.selection.drain(..) {
851            window.paint_quad(quad);
852        }
853        for (index, line) in state.layout.lines.iter().enumerate() {
854            let y = line_height * state.layout.row_starts[index];
855            let _ = line.paint(
856                bounds.origin + point(px(0.0), y),
857                line_height,
858                TextAlign::Left,
859                Some(bounds),
860                window,
861                cx,
862            );
863        }
864        if let Some(cursor) = state.cursor.take() {
865            window.paint_quad(cursor);
866        }
867    }
868}
869
870fn marked_runs(base: &TextRun, marked: Option<&Range<usize>>, len: usize) -> Vec<TextRun> {
871    let Some(marked) = marked.filter(|range| range.end <= len) else {
872        return vec![base.clone()];
873    };
874    vec![
875        TextRun {
876            len: marked.start,
877            ..base.clone()
878        },
879        TextRun {
880            len: marked.end - marked.start,
881            underline: Some(UnderlineStyle {
882                color: Some(base.color),
883                thickness: px(1.0),
884                wavy: false,
885            }),
886            ..base.clone()
887        },
888        TextRun {
889            len: len - marked.end,
890            ..base.clone()
891        },
892    ]
893    .into_iter()
894    .filter(|run| run.len > 0)
895    .collect()
896}
897
898impl Render for TextAreaEntity {
899    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
900        let viewport_rows = self.config.viewport_rows(self.measured_rows);
901        let viewport_height = self.config.typography.line_height * viewport_rows;
902        let mut root = div().flex();
903        if let Some(family) = &self.config.typography.family {
904            root = root.font_family(family.clone());
905        }
906        if !self.config.typography.fallbacks.is_empty() {
907            root.text_style().font_fallbacks = Some(FontFallbacks::from_fonts(
908                self.config.typography.fallbacks.clone(),
909            ));
910        }
911        root.font_weight(FontWeight(f32::from(self.config.typography.weight)))
912            .key_context("GPUIRhaiTextarea")
913            .track_focus(&self.focus)
914            .on_key_down(|event: &KeyDownEvent, window, cx| {
915                if event.keystroke.key.as_str() == "tab" {
916                    if event.keystroke.modifiers.shift {
917                        window.focus_prev(cx);
918                    } else {
919                        window.focus_next(cx);
920                    }
921                    cx.stop_propagation();
922                }
923            })
924            .cursor(CursorStyle::IBeam)
925            .on_action(cx.listener(Self::backspace))
926            .on_action(cx.listener(Self::delete))
927            .on_action(cx.listener(Self::left))
928            .on_action(cx.listener(Self::right))
929            .on_action(cx.listener(Self::up))
930            .on_action(cx.listener(Self::down))
931            .on_action(cx.listener(Self::select_left))
932            .on_action(cx.listener(Self::select_right))
933            .on_action(cx.listener(Self::select_up))
934            .on_action(cx.listener(Self::select_down))
935            .on_action(cx.listener(Self::select_all))
936            .on_action(cx.listener(Self::home))
937            .on_action(cx.listener(Self::end))
938            .on_action(cx.listener(Self::newline))
939            .on_action(cx.listener(Self::show_character_palette))
940            .on_action(cx.listener(Self::paste))
941            .on_action(cx.listener(Self::cut))
942            .on_action(cx.listener(Self::copy))
943            .on_action(cx.listener(Self::undo))
944            .on_action(cx.listener(Self::redo))
945            .on_mouse_down(MouseButton::Left, cx.listener(Self::mouse_down))
946            .on_mouse_up(MouseButton::Left, cx.listener(Self::mouse_up))
947            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::mouse_up))
948            .on_mouse_move(cx.listener(Self::mouse_move))
949            .line_height(self.config.typography.line_height)
950            .text_size(self.config.typography.font_size)
951            .opacity(if self.config.disabled { 0.55 } else { 1.0 })
952            .child(
953                div()
954                    .id("textarea-scroll")
955                    .h(viewport_height)
956                    .w_full()
957                    .overflow_y_scroll()
958                    .restrict_scroll_to_axis()
959                    .track_scroll(&self.scroll)
960                    .when_some(self.config.scroll_color, |scroll, color| {
961                        scroll.bg(rgba(color.as_rgba_hex()))
962                    })
963                    .child(TextAreaElement { input: cx.entity() }),
964            )
965    }
966}
967
968impl Focusable for TextAreaEntity {
969    fn focus_handle(&self, _: &App) -> FocusHandle {
970        self.focus.clone()
971    }
972}
973
974#[derive(Default)]
975pub struct TextAreaPrimitiveHandler {
976    instances: BTreeMap<PrimitiveInstanceId, Entity<TextAreaEntity>>,
977}
978
979impl PrimitiveHandler for TextAreaPrimitiveHandler {
980    fn uses_primary_focus(&self) -> bool {
981        true
982    }
983
984    fn accessibility_actions(
985        &self,
986        _instance: &PrimitiveInstanceId,
987    ) -> Vec<gpui::AccessibleAction> {
988        vec![
989            gpui::AccessibleAction::Focus,
990            gpui::AccessibleAction::SetValue,
991        ]
992    }
993
994    fn perform_accessibility_action(
995        &mut self,
996        instance: &PrimitiveInstanceId,
997        action: gpui::AccessibleAction,
998        data: Option<&gpui::accesskit::ActionData>,
999        window: &mut Window,
1000        cx: &mut App,
1001    ) -> Result<(), String> {
1002        let entity = self
1003            .instances
1004            .get(instance)
1005            .cloned()
1006            .ok_or_else(|| "textarea accessibility target is stale".to_owned())?;
1007        match action {
1008            gpui::AccessibleAction::Focus => {
1009                let (disabled, focus) = {
1010                    let input = entity.read(cx);
1011                    (input.config.disabled, input.focus.clone())
1012                };
1013                if disabled {
1014                    return Err("disabled textarea cannot receive focus".to_owned());
1015                }
1016                focus.focus(window, cx);
1017                Ok(())
1018            }
1019            gpui::AccessibleAction::SetValue => {
1020                let Some(gpui::accesskit::ActionData::Value(value)) = data else {
1021                    return Err("textarea SetValue requires string data".to_owned());
1022                };
1023                let callback = {
1024                    let input = entity.read(cx);
1025                    if input.config.disabled || input.config.read_only {
1026                        return Err("textarea is disabled or read-only".to_owned());
1027                    }
1028                    input.callbacks.change.clone()
1029                };
1030                if let Some(callback) = callback {
1031                    callback(value.to_string(), window, cx);
1032                }
1033                Ok(())
1034            }
1035            _ => Err("unsupported textarea accessibility action".to_owned()),
1036        }
1037    }
1038
1039    fn render(
1040        &mut self,
1041        instance: &PrimitiveInstance,
1042        events: &PrimitiveContext,
1043        theme: &crate::PrimitiveTheme,
1044        window: &mut Window,
1045        cx: &mut App,
1046    ) -> Result<gpui::AnyElement, String> {
1047        let id = instance
1048            .id
1049            .clone()
1050            .ok_or_else(|| "TextareaPrimitive requires a stable key".to_owned())?;
1051        if id.key().trim().is_empty() {
1052            return Err("TextareaPrimitive key cannot be empty".to_owned());
1053        }
1054        let value = instance
1055            .node
1056            .props
1057            .string("value")
1058            .unwrap_or_default()
1059            .to_owned();
1060        let config = config_from_props(&instance.node.props, theme, window)?;
1061        let callbacks = primitive_callbacks(events);
1062        let shared_focus = instance.focus_handle().cloned();
1063        let entity = if let Some(entity) = self.instances.get(&id) {
1064            entity.clone()
1065        } else {
1066            let entity = cx.new(|cx| {
1067                let mut input = TextAreaEntity::new(&value, config.clone(), callbacks.clone(), cx)
1068                    .expect("validated Textarea props remain valid during entity creation");
1069                if let Some(focus) = shared_focus.clone() {
1070                    input.focus = focus.tab_stop(!config.disabled);
1071                }
1072                input
1073            });
1074            entity.update(cx, |input, cx| {
1075                let focus = input.focus.clone();
1076                cx.on_focus(&focus, window, |input, window, cx| {
1077                    if let Some(focus) = input.callbacks.focus.clone() {
1078                        window.defer(cx, move |window, cx| focus(window, cx));
1079                    }
1080                })
1081                .detach();
1082                cx.on_blur(&focus, window, |input, window, cx| {
1083                    if let Some(blur) = input.callbacks.blur.clone() {
1084                        window.defer(cx, move |window, cx| blur(window, cx));
1085                    }
1086                })
1087                .detach();
1088                if input.config.autofocus && !input.config.disabled {
1089                    input.focus.focus(window, cx);
1090                }
1091            });
1092            self.instances.insert(id.clone(), entity.clone());
1093            entity
1094        };
1095        entity.update(cx, |input, cx| {
1096            input.update_props(&value, config, callbacks, cx)
1097        })?;
1098        Ok(entity.into_any_element())
1099    }
1100
1101    fn unmount(&mut self, instance: &PrimitiveInstanceId) {
1102        self.instances.remove(instance);
1103    }
1104}
1105
1106fn validate_text_area_value(value: &str, config: &TextAreaConfig) -> Result<(), String> {
1107    if config.min_rows == 0 || config.max_rows < config.min_rows {
1108        return Err("Textarea requires 1 <= min_rows <= max_rows".to_owned());
1109    }
1110    if config.rows == Some(0) {
1111        return Err("Textarea rows must be greater than zero".to_owned());
1112    }
1113    if let Some(max_length) = config.max_length
1114        && unicode_segmentation::UnicodeSegmentation::graphemes(value, true).count() > max_length
1115    {
1116        return Err(format!(
1117            "Textarea controlled value exceeds max_length {max_length}"
1118        ));
1119    }
1120    Ok(())
1121}
1122
1123fn config_from_props(
1124    props: &PrimitiveProps,
1125    theme: &crate::PrimitiveTheme,
1126    window: &Window,
1127) -> Result<TextAreaConfig, String> {
1128    let accent = theme
1129        .color("accent")
1130        .unwrap_or_else(|| Rgba8::from_rgba_hex(0x3b82_f6ff));
1131    Ok(TextAreaConfig {
1132        placeholder: props.string("placeholder").unwrap_or_default().into(),
1133        disabled: props.boolean("disabled").unwrap_or(false),
1134        read_only: props.boolean("read_only").unwrap_or(false),
1135        min_rows: usize_prop(props, "min_rows")?.unwrap_or(3),
1136        max_rows: usize_prop(props, "max_rows")?.unwrap_or(8),
1137        rows: usize_prop(props, "rows")?,
1138        max_length: usize_prop(props, "max_length")?,
1139        typography: native_typography(theme, props.string("typography").unwrap_or("body"), window)?,
1140        autofocus: props.boolean("autofocus").unwrap_or(false),
1141        placeholder_color: part_color(props, "placeholder_style", theme, false)
1142            .or_else(|| theme.color("text_muted"))
1143            .unwrap_or_else(|| Rgba8::from_rgba_hex(0xa1a1_aaff)),
1144        selection_color: part_color(props, "selection_style", theme, true)
1145            .unwrap_or_else(|| with_alpha(accent, 0x55)),
1146        caret_color: part_color(props, "caret_style", theme, true).unwrap_or(accent),
1147        scroll_color: part_color(props, "scroll_style", theme, true),
1148    })
1149}
1150
1151fn part_color(
1152    props: &PrimitiveProps,
1153    name: &str,
1154    theme: &crate::PrimitiveTheme,
1155    prefer_background: bool,
1156) -> Option<Rgba8> {
1157    let Some(PrimitiveValue::Style(style)) = props.get(name) else {
1158        return None;
1159    };
1160    let resolved = style.resolve(&crate::InteractionState::default());
1161    let value = if prefer_background {
1162        resolved
1163            .background
1164            .as_ref()
1165            .or(resolved.text_color.as_ref())
1166    } else {
1167        resolved
1168            .text_color
1169            .as_ref()
1170            .or(resolved.background.as_ref())
1171    }?;
1172    theme.resolve_color(value)
1173}
1174
1175fn with_alpha(color: Rgba8, alpha: u8) -> Rgba8 {
1176    Rgba8::from_rgba_hex((color.as_rgba_hex() & 0xffff_ff00) | u32::from(alpha))
1177}
1178
1179fn primitive_callbacks(events: &PrimitiveContext) -> TextInputCallbacks {
1180    let change_events = events.clone();
1181    let focus_events = events.clone();
1182    let blur_events = events.clone();
1183    TextInputCallbacks {
1184        change: Some(Rc::new(move |value, window, cx| {
1185            let _ = change_events.emit("change", UiValue::String(value), window, cx);
1186        })),
1187        submit: None,
1188        focus: Some(Rc::new(move |window, cx| {
1189            let _ = focus_events.emit("focus", UiValue::Null, window, cx);
1190        })),
1191        blur: Some(Rc::new(move |window, cx| {
1192            let _ = blur_events.emit("blur", UiValue::Null, window, cx);
1193        })),
1194        tab: None,
1195    }
1196}
1197
1198fn usize_prop(props: &PrimitiveProps, name: &str) -> Result<Option<usize>, String> {
1199    match props.get(name) {
1200        Some(PrimitiveValue::Data(UiValue::Integer(value))) => usize::try_from(*value)
1201            .map(Some)
1202            .map_err(|_| format!("Textarea {name} must be a non-negative integer")),
1203        Some(PrimitiveValue::Data(UiValue::Null)) | None => Ok(None),
1204        _ => Err(format!("Textarea {name} must be an integer")),
1205    }
1206}
1207
1208fn pixels_to_usize(value: f32) -> usize {
1209    value.max(0.0).to_string().parse().unwrap_or(usize::MAX)
1210}
1211
1212/// Build the compile-time Textarea primitive schema.
1213///
1214/// # Panics
1215///
1216/// Panics only if the static built-in primitive ID becomes invalid.
1217#[must_use]
1218pub fn text_area_primitive_descriptor() -> PrimitiveDescriptor {
1219    let optional_callback = || ObjectField::optional(ValueSchema::optional(ValueSchema::Callback));
1220    PrimitiveDescriptor {
1221        id: PrimitiveId::parse("gpui_rhai.textarea").expect("static primitive ID"),
1222        export: "TextareaPrimitive".to_owned(),
1223        props: BTreeMap::from([
1224            (
1225                "value".to_owned(),
1226                ObjectField::required(ValueSchema::string()),
1227            ),
1228            (
1229                "placeholder".to_owned(),
1230                ObjectField::optional(ValueSchema::optional(ValueSchema::string())),
1231            ),
1232            (
1233                "disabled".to_owned(),
1234                ObjectField::optional(ValueSchema::Bool).with_default(UiValue::Bool(false)),
1235            ),
1236            (
1237                "read_only".to_owned(),
1238                ObjectField::optional(ValueSchema::Bool).with_default(UiValue::Bool(false)),
1239            ),
1240            (
1241                "min_rows".to_owned(),
1242                ObjectField::optional(ValueSchema::bounded_integer(
1243                    Some(1),
1244                    Some(MAX_TEXTAREA_ROWS),
1245                ))
1246                .with_default(UiValue::Integer(3)),
1247            ),
1248            (
1249                "max_rows".to_owned(),
1250                ObjectField::optional(ValueSchema::bounded_integer(
1251                    Some(1),
1252                    Some(MAX_TEXTAREA_ROWS),
1253                ))
1254                .with_default(UiValue::Integer(8)),
1255            ),
1256            (
1257                "rows".to_owned(),
1258                ObjectField::optional(ValueSchema::optional(ValueSchema::bounded_integer(
1259                    Some(1),
1260                    Some(MAX_TEXTAREA_ROWS),
1261                ))),
1262            ),
1263            (
1264                "max_length".to_owned(),
1265                ObjectField::optional(ValueSchema::optional(ValueSchema::bounded_integer(
1266                    Some(0),
1267                    Some(MAX_TEXTAREA_GRAPHEMES),
1268                ))),
1269            ),
1270            (
1271                "typography".to_owned(),
1272                ObjectField::required(ValueSchema::String {
1273                    allowed: crate::REQUIRED_TYPOGRAPHY
1274                        .iter()
1275                        .map(ToString::to_string)
1276                        .collect(),
1277                }),
1278            ),
1279            (
1280                "autofocus".to_owned(),
1281                ObjectField::optional(ValueSchema::Bool).with_default(UiValue::Bool(false)),
1282            ),
1283            (
1284                "placeholder_style".to_owned(),
1285                ObjectField::required(ValueSchema::Style),
1286            ),
1287            (
1288                "selection_style".to_owned(),
1289                ObjectField::required(ValueSchema::Style),
1290            ),
1291            (
1292                "caret_style".to_owned(),
1293                ObjectField::required(ValueSchema::Style),
1294            ),
1295            (
1296                "scroll_style".to_owned(),
1297                ObjectField::required(ValueSchema::Style),
1298            ),
1299            ("on_change".to_owned(), optional_callback()),
1300            ("on_focus".to_owned(), optional_callback()),
1301            ("on_blur".to_owned(), optional_callback()),
1302        ]),
1303        events: text_area_events(),
1304        state: ComponentStateSchema::default(),
1305        lifecycle: true,
1306        effect: None,
1307    }
1308}
1309
1310fn text_area_events() -> BTreeMap<String, EventSchema> {
1311    BTreeMap::from([
1312        (
1313            "change".to_owned(),
1314            EventSchema {
1315                payload: ValueSchema::string(),
1316            },
1317        ),
1318        (
1319            "focus".to_owned(),
1320            EventSchema {
1321                payload: ValueSchema::Null,
1322            },
1323        ),
1324        (
1325            "blur".to_owned(),
1326            EventSchema {
1327                payload: ValueSchema::Null,
1328            },
1329        ),
1330    ])
1331}
1332
1333#[cfg(test)]
1334mod tests {
1335    use super::*;
1336
1337    #[test]
1338    fn visual_line_layout_maps_positions_and_selection() {
1339        // Native shaping is covered in GPUI tests; this guards the stable row
1340        // bookkeeping used around WrappedLine values.
1341        let layout = TextAreaLayout {
1342            lines: Vec::new(),
1343            starts: Vec::new(),
1344            row_starts: Vec::new(),
1345            line_height: px(20.0),
1346        };
1347        assert_eq!(layout.visual_rows(), 1);
1348    }
1349
1350    #[test]
1351    fn config_rejects_controlled_values_over_grapheme_limit() {
1352        let config = TextAreaConfig {
1353            placeholder: "".into(),
1354            disabled: false,
1355            read_only: false,
1356            min_rows: 3,
1357            max_rows: 8,
1358            rows: None,
1359            max_length: Some(1),
1360            typography: NativeTypography {
1361                family: None,
1362                fallbacks: Vec::new(),
1363                font_size: px(14.0),
1364                line_height: px(20.0),
1365                weight: 400,
1366            },
1367            autofocus: false,
1368            placeholder_color: Rgba8::from_rgba_hex(0xa1a1_aaff),
1369            selection_color: Rgba8::from_rgba_hex(0x3b82_f655),
1370            caret_color: Rgba8::from_rgba_hex(0x3b82_f6ff),
1371            scroll_color: None,
1372        };
1373        assert!(validate_text_area_value("👩🏽‍💻", &config).is_ok());
1374        assert!(validate_text_area_value("👩🏽‍💻x", &config).is_err());
1375    }
1376}