Skip to main content

ui/
input.rs

1//! [`TextField`] — a single-line text field with IME, selection and clipboard.
2//!
3//! Unlike the rest of this crate, a text field cannot be a plain function
4//! returning a `Div`: editing needs state (content, selection, IME marked
5//! range), a focus handle, and gpui's [`EntityInputHandler`]. So it is an
6//! entity the caller holds — SwiftUI's `TextField` bound to `@State`, not a
7//! stateless view.
8//!
9//! Ported from gpui's `examples/input.rs` (Apache-2.0), restyled onto
10//! [`Theme`] tokens, with the key bindings **scoped to the field's key
11//! context** rather than installed globally: a component library must not make
12//! `cmd-a` mean "select all text" for the whole application.
13//!
14//! ```ignore
15//! ui::input::init(cx);                      // once, at startup
16//! let field = cx.new(|cx| TextField::new(cx).with_placeholder("Search…"));
17//! // …then render it: .child(field.clone())
18//! ```
19
20use std::ops::Range;
21
22use gpui::{
23    App, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler, Entity,
24    EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, LayoutId, MouseButton,
25    MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, SharedString, Style,
26    TextRun, UTF16Selection, UnderlineStyle, Window, WrappedLine, actions, div, fill, prelude::*,
27    px, relative,
28};
29use unicode_segmentation::UnicodeSegmentation as _;
30
31use theme::Theme;
32
33actions!(
34    bezel_text_field,
35    [
36        Backspace,
37        Delete,
38        Left,
39        Right,
40        SelectLeft,
41        SelectRight,
42        SelectAll,
43        Home,
44        End,
45        SelectHome,
46        SelectEnd,
47        WordLeft,
48        WordRight,
49        SelectWordLeft,
50        SelectWordRight,
51        DeleteWordLeft,
52        DeleteWordRight,
53        DeleteToLineStart,
54        DeleteToLineEnd,
55        ShowCharacterPalette,
56        Paste,
57        Cut,
58        Copy,
59        Up,
60        Down,
61        SelectUp,
62        SelectDown,
63        InsertNewline,
64        Undo,
65        Redo,
66    ]
67);
68
69/// How many undo steps a field keeps by default.
70///
71/// Steps, not keystrokes: a run of typing coalesces into one, so this is deeper
72/// than it looks. A text field is not a document — nobody walks a search box
73/// back through a long history — and the ceiling is what stops a long-lived
74/// field accumulating snapshots forever. Override per field with
75/// [`TextField::with_undo_limit`].
76pub const DEFAULT_UNDO_LIMIT: usize = 10;
77
78/// Width of the caret. Named because horizontal scrolling has to keep the caret
79/// itself on screen, not merely the character before it.
80const CARET_WIDTH: Pixels = px(2.);
81
82/// The key context the field claims; bindings from [`init`] are scoped to it.
83pub const KEY_CONTEXT: &str = "TextField";
84
85/// Claimed *in addition* to [`KEY_CONTEXT`] by a multi-line field.
86///
87/// Vertical motion and `enter` hang off this rather than off every field,
88/// because a single-line field is routinely nested inside something that has
89/// already claimed those keys: [`crate::palette`] and [`crate::combobox`] both
90/// bind `up`, `down`, `ctrl-n`, `ctrl-p` and `enter` to drive their lists, and
91/// their query field sits *deeper* in the focus path — so binding those on
92/// every `TextField` would win the dispatch and break list navigation in both.
93pub const MULTILINE_KEY_CONTEXT: &str = "TextArea";
94
95/// Install the default key bindings. Call once at startup.
96///
97/// Every binding is scoped to [`KEY_CONTEXT`], so they are inert outside a
98/// focused field and an app is free to bind the same chords elsewhere.
99///
100/// **Optional.** This is a convenience, not a requirement: every action above
101/// is a public type, so an app that wants its own keymap simply does not call
102/// this and binds what it likes instead —
103///
104/// ```ignore
105/// use ui::input::{self, Home, KEY_CONTEXT};
106/// cx.bind_keys([KeyBinding::new("ctrl-a", Home, Some(KEY_CONTEXT))]);
107/// ```
108///
109/// It is all-or-nothing, so taking the clipboard defaults while replacing the
110/// motion ones means rebinding the lot. That is deliberate until something
111/// needs finer grain.
112pub fn init(cx: &mut App) {
113    let ctx = Some(KEY_CONTEXT);
114    cx.bind_keys([
115        // Character movement and editing, everywhere.
116        KeyBinding::new("backspace", Backspace, ctx),
117        KeyBinding::new("delete", Delete, ctx),
118        KeyBinding::new("left", Left, ctx),
119        KeyBinding::new("right", Right, ctx),
120        KeyBinding::new("shift-left", SelectLeft, ctx),
121        KeyBinding::new("shift-right", SelectRight, ctx),
122        KeyBinding::new("home", Home, ctx),
123        KeyBinding::new("end", End, ctx),
124        KeyBinding::new("shift-home", SelectHome, ctx),
125        KeyBinding::new("shift-end", SelectEnd, ctx),
126    ]);
127
128    // Multi-line only — see [`MULTILINE_KEY_CONTEXT`] for why these cannot be
129    // bound on every field.
130    let area = Some(MULTILINE_KEY_CONTEXT);
131    cx.bind_keys([
132        KeyBinding::new("enter", InsertNewline, area),
133        KeyBinding::new("up", Up, area),
134        KeyBinding::new("down", Down, area),
135        KeyBinding::new("shift-up", SelectUp, area),
136        KeyBinding::new("shift-down", SelectDown, area),
137    ]);
138
139    #[cfg(target_os = "macos")]
140    cx.bind_keys([
141        KeyBinding::new("cmd-a", SelectAll, ctx),
142        KeyBinding::new("cmd-c", Copy, ctx),
143        KeyBinding::new("cmd-x", Cut, ctx),
144        KeyBinding::new("cmd-v", Paste, ctx),
145        KeyBinding::new("cmd-z", Undo, ctx),
146        KeyBinding::new("cmd-shift-z", Redo, ctx),
147        KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, ctx),
148        // cmd = line, option = word: the macOS convention.
149        KeyBinding::new("cmd-left", Home, ctx),
150        KeyBinding::new("cmd-right", End, ctx),
151        KeyBinding::new("cmd-shift-left", SelectHome, ctx),
152        KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
153        KeyBinding::new("alt-left", WordLeft, ctx),
154        KeyBinding::new("alt-right", WordRight, ctx),
155        KeyBinding::new("alt-shift-left", SelectWordLeft, ctx),
156        KeyBinding::new("alt-shift-right", SelectWordRight, ctx),
157        KeyBinding::new("cmd-backspace", DeleteToLineStart, ctx),
158        KeyBinding::new("alt-backspace", DeleteWordLeft, ctx),
159        KeyBinding::new("alt-delete", DeleteWordRight, ctx),
160        // The emacs bindings macOS honours in every native text field.
161        KeyBinding::new("ctrl-a", Home, ctx),
162        KeyBinding::new("ctrl-e", End, ctx),
163        KeyBinding::new("ctrl-b", Left, ctx),
164        KeyBinding::new("ctrl-f", Right, ctx),
165        KeyBinding::new("ctrl-h", Backspace, ctx),
166        KeyBinding::new("ctrl-d", Delete, ctx),
167        KeyBinding::new("ctrl-k", DeleteToLineEnd, ctx),
168    ]);
169
170    // `C-n`/`C-p` are emacs' vertical motion and macOS `NSTextView` natives
171    // both — the two tests a chord has to pass to earn a binding here.
172    #[cfg(target_os = "macos")]
173    cx.bind_keys([
174        KeyBinding::new("ctrl-n", Down, area),
175        KeyBinding::new("ctrl-p", Up, area),
176    ]);
177
178    #[cfg(not(target_os = "macos"))]
179    cx.bind_keys([
180        KeyBinding::new("ctrl-a", SelectAll, ctx),
181        KeyBinding::new("ctrl-c", Copy, ctx),
182        KeyBinding::new("ctrl-x", Cut, ctx),
183        KeyBinding::new("ctrl-v", Paste, ctx),
184        // ctrl = word on Windows/Linux, where there is no line modifier.
185        KeyBinding::new("ctrl-left", WordLeft, ctx),
186        KeyBinding::new("ctrl-right", WordRight, ctx),
187        KeyBinding::new("ctrl-shift-left", SelectWordLeft, ctx),
188        KeyBinding::new("ctrl-shift-right", SelectWordRight, ctx),
189        KeyBinding::new("ctrl-backspace", DeleteWordLeft, ctx),
190        KeyBinding::new("ctrl-delete", DeleteWordRight, ctx),
191        KeyBinding::new("ctrl-z", Undo, ctx),
192        KeyBinding::new("ctrl-shift-z", Redo, ctx),
193    ]);
194}
195
196/// What shape the field takes.
197///
198/// Editing is identical across all three — every action works on the content
199/// and a byte range, and none of them cares where the lines break. What this
200/// decides is the box: how tall it is, whether text wraps, and with that what
201/// `enter` and a pasted newline are allowed to mean.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
203pub enum Shape {
204    /// One line, no wrapping. `enter` does not insert; a pasted newline becomes
205    /// a space rather than silently truncating what was pasted.
206    #[default]
207    Line,
208    /// Exactly `rows` lines tall, wrapping, scrolling past that.
209    Rows(usize),
210    /// Wraps and grows with the content between `min` and `max` rows, then
211    /// scrolls — the composer shape.
212    Grow { min: usize, max: usize },
213}
214
215impl Shape {
216    /// Whether newlines are content. The single branch every editing policy
217    /// hangs off, so it is asked once rather than matched in each caller.
218    fn is_multiline(self) -> bool {
219        !matches!(self, Self::Line)
220    }
221}
222
223/// A point the field can be returned to.
224///
225/// A whole snapshot rather than a diff: a field holds a sentence, not a file,
226/// and `SharedString` clones are a refcount bump. A rope and a transaction log
227/// is what an editor needs and would be the wrong machinery here.
228#[derive(Clone)]
229struct Snapshot {
230    content: SharedString,
231    selection: Range<usize>,
232    reversed: bool,
233}
234
235/// Which way an edit went, so a run of the same kind can coalesce into one
236/// undo step instead of giving the text back a character at a time.
237#[derive(Clone, Copy, PartialEq, Eq)]
238pub enum EditKind {
239    Insert,
240    Delete,
241}
242
243/// A text field. [`Shape`] decides whether it is one line or many; everything
244/// else about it is the same either way.
245pub struct TextField {
246    focus_handle: FocusHandle,
247    content: SharedString,
248    placeholder: SharedString,
249    shape: Shape,
250    selected_range: Range<usize>,
251    selection_reversed: bool,
252    /// The IME composition range (underlined while composing).
253    marked_range: Option<Range<usize>>,
254    /// One entry per hard newline; each wraps into rows of its own. Empty until
255    /// the first paint.
256    last_layout: Vec<WrappedLine>,
257    last_bounds: Option<Bounds<Pixels>>,
258    is_selecting: bool,
259    /// The column vertical motion is trying to keep, in pixels from the left of
260    /// the row. Held across a run of up/down so that walking through a short
261    /// line and out the other side returns to the column you started in, and
262    /// dropped by anything horizontal — which is every other way the caret
263    /// moves, so [`TextField::move_to`] and [`TextField::select_to`] clear it
264    /// and the vertical handlers put it back.
265    goal_x: Option<Pixels>,
266    /// How far the text is scrolled inside the box. Clamped every frame,
267    /// because the content it is measured against changes under it.
268    ///
269    /// Both axes, though only ever one at a time: a wrapped field's lines are
270    /// shaped to the box width so they cannot overflow sideways, and a
271    /// single-line field is exactly one row tall so it cannot overflow
272    /// downwards. The clamp falls out of that and needs no test for shape.
273    scroll: Point<Pixels>,
274    /// Points to return to, oldest first. Bounded by `undo_limit`: the field
275    /// outlives a lot of typing, and an unbounded history of a growing string
276    /// is a slow leak nothing ever reclaims.
277    undo: std::collections::VecDeque<Snapshot>,
278    /// Undone points, newest last. Cleared by any fresh edit — the usual
279    /// model, and the only one where redo cannot resurrect a branch the text
280    /// has already diverged from.
281    redo: Vec<Snapshot>,
282    undo_limit: usize,
283    /// The kind of the last edit and the offset it left the caret at, which is
284    /// what decides whether the next edit joins that group or starts a new one.
285    /// Adjacency rather than a pause, so there is no timing threshold to invent.
286    last_edit: Option<(EditKind, usize)>,
287    /// A context this field claims *in addition* to [`KEY_CONTEXT`] and
288    /// [`MULTILINE_KEY_CONTEXT`] — see [`Self::with_key_context`].
289    key_context: Option<SharedString>,
290    /// Set by anything that moves the caret, cleared once a frame has scrolled
291    /// it back into view.
292    ///
293    /// Without the flag the wheel could never win: following the caret
294    /// unconditionally would snap the view back to it on the very next frame,
295    /// so scrolling away to read would be impossible.
296    follow_caret: bool,
297}
298
299impl TextField {
300    pub fn new(cx: &mut Context<Self>) -> Self {
301        Self {
302            // A field is a tab stop from birth; a stateless control needs
303            // `focus::focusable` because its handle lives in the caller.
304            focus_handle: cx.focus_handle().tab_stop(true),
305            content: "".into(),
306            placeholder: "".into(),
307            shape: Shape::Line,
308            selected_range: 0..0,
309            selection_reversed: false,
310            marked_range: None,
311            last_layout: Vec::new(),
312            last_bounds: None,
313            is_selecting: false,
314            goal_x: None,
315            scroll: Point::default(),
316            undo: std::collections::VecDeque::new(),
317            redo: Vec::new(),
318            undo_limit: DEFAULT_UNDO_LIMIT,
319            last_edit: None,
320            key_context: None,
321            follow_caret: false,
322        }
323    }
324
325    /// How many undo steps to keep. App-wide configuration would be a gpui
326    /// global alongside [`crate::input::init`], not a [`Theme`] field — the
327    /// theme is rebuilt on every light/dark switch, which would quietly reset
328    /// anything behavioural parked in it.
329    pub fn with_undo_limit(mut self, limit: usize) -> Self {
330        self.undo_limit = limit;
331        self
332    }
333
334    pub fn with_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
335        self.placeholder = placeholder.into();
336        self
337    }
338
339    /// Claim an extra key context on this field, so an app can bind a key
340    /// *here* that it does not want bound in every other field.
341    ///
342    /// The composer case, and the reason this exists: `enter` sends a message
343    /// and `shift-enter` breaks a line, while the notes field two panels over
344    /// still takes `enter` as a newline. gpui resolves a keystroke to the
345    /// binding whose context matches **deepest** in the focus path, and nothing
346    /// is deeper than the focused field — so a container around it cannot win
347    /// `enter`, however it is bound. Rebinding [`MULTILINE_KEY_CONTEXT`]
348    /// globally would win, and would take the newline away from every other
349    /// multi-line field in the app. A context of the field's own is the only
350    /// thing that is both deep enough and narrow enough.
351    ///
352    /// ```ignore
353    /// const COMPOSER: &str = "Composer";
354    /// cx.bind_keys([
355    ///     KeyBinding::new("enter", Send, Some(COMPOSER)),
356    ///     KeyBinding::new("shift-enter", input::InsertNewline, Some(COMPOSER)),
357    /// ]);
358    /// let field = cx.new(|cx| {
359    ///     TextField::new(cx)
360    ///         .with_shape(Shape::Grow { min: 3, max: 12 })
361    ///         .with_key_context(COMPOSER)
362    /// });
363    /// ```
364    pub fn with_key_context(mut self, context: impl Into<SharedString>) -> Self {
365        self.key_context = Some(context.into());
366        self
367    }
368
369    pub fn with_shape(mut self, shape: Shape) -> Self {
370        self.shape = shape;
371        self
372    }
373
374    pub fn shape(&self) -> Shape {
375        self.shape
376    }
377
378    pub fn content(&self) -> &SharedString {
379        &self.content
380    }
381
382    /// Replace the content, putting the cursor at the end.
383    pub fn set_content(&mut self, content: impl Into<SharedString>, cx: &mut Context<Self>) {
384        self.content = normalize(&content.into(), self.shape).into();
385        // A programmatic reset is not something the user did, so there is
386        // nothing here for them to undo back past.
387        self.undo.clear();
388        self.redo.clear();
389        self.last_edit = None;
390        let end = self.content.len();
391        self.selected_range = end..end;
392        self.marked_range = None;
393        cx.notify();
394    }
395
396    pub fn clear(&mut self, cx: &mut Context<Self>) {
397        self.set_content("", cx);
398    }
399
400    /// [`Self::with_placeholder`] after construction, for a hint that follows
401    /// something else — the field naming whichever agent, file or channel is
402    /// selected rather than being rebuilt each time one is.
403    pub fn set_placeholder(
404        &mut self,
405        placeholder: impl Into<SharedString>,
406        cx: &mut Context<Self>,
407    ) {
408        self.placeholder = placeholder.into();
409        cx.notify();
410    }
411
412    /// Where the caret is, as a byte offset into [`Self::content`].
413    ///
414    /// What a caller needs to read the text *behind* the caret: the `#` an
415    /// autocomplete triggers on, the word a lookup would act on. With a
416    /// selection this is the moving end, which is where typing would land.
417    pub fn cursor(&self) -> usize {
418        self.cursor_offset()
419    }
420
421    /// Where a byte offset sits on screen — the bounds of the row it is on, in
422    /// window coordinates.
423    ///
424    /// The anchor for anything that hangs off a position *in the text* rather
425    /// than off the field: a mention picker under the `#` that opened it, in a
426    /// box that is also growing a row at a time. Pass it to
427    /// [`crate::popover::menu_at`], which takes a window point.
428    ///
429    /// `None` until the field has painted once — this is measured off the
430    /// shaped layout, and there is none before then.
431    pub fn offset_bounds(&self, offset: usize, window: &Window) -> Option<Bounds<Pixels>> {
432        self.row_bounds(self.text_origin()?, offset..offset, window.line_height())
433    }
434
435    /// The rectangle `range` spans, starting from the row it opens on, relative
436    /// to `origin`.
437    ///
438    /// The IME's candidate panel and [`Self::offset_bounds`] are the same
439    /// question asked by two callers, so they ask it here — computed apart they
440    /// would answer differently the first time one of them forgot the scroll.
441    fn row_bounds(
442        &self,
443        origin: Point<Pixels>,
444        range: Range<usize>,
445        line_height: Pixels,
446    ) -> Option<Bounds<Pixels>> {
447        let start = position_for_offset(&self.last_layout, range.start, line_height)?;
448        let end = position_for_offset(&self.last_layout, range.end, line_height)?;
449        Some(Bounds::from_corners(
450            origin + start,
451            origin + gpui::point(end.x, end.y + line_height),
452        ))
453    }
454
455    fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
456        if self.selected_range.is_empty() {
457            self.move_to(self.previous_boundary(self.cursor_offset()), cx);
458        } else {
459            self.move_to(self.selected_range.start, cx)
460        }
461    }
462
463    fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
464        if self.selected_range.is_empty() {
465            self.move_to(self.next_boundary(self.selected_range.end), cx);
466        } else {
467            self.move_to(self.selected_range.end, cx)
468        }
469    }
470
471    fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
472        self.select_to(self.previous_boundary(self.cursor_offset()), cx);
473    }
474
475    fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
476        self.select_to(self.next_boundary(self.cursor_offset()), cx);
477    }
478
479    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
480        self.move_to(0, cx);
481        self.select_to(self.content.len(), cx)
482    }
483
484    fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
485        self.move_to(line_start(&self.content, self.cursor_offset()), cx);
486    }
487
488    fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
489        self.move_to(line_end(&self.content, self.cursor_offset()), cx);
490    }
491
492    fn select_home(&mut self, _: &SelectHome, _: &mut Window, cx: &mut Context<Self>) {
493        self.select_to(line_start(&self.content, self.cursor_offset()), cx);
494    }
495
496    fn select_end(&mut self, _: &SelectEnd, _: &mut Window, cx: &mut Context<Self>) {
497        self.select_to(line_end(&self.content, self.cursor_offset()), cx);
498    }
499
500    fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) {
501        self.vertical(-1, false, window, cx);
502    }
503
504    fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) {
505        self.vertical(1, false, window, cx);
506    }
507
508    fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
509        self.vertical(-1, true, window, cx);
510    }
511
512    fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
513        self.vertical(1, true, window, cx);
514    }
515
516    /// Move the caret `rows` rows, keeping the goal column.
517    ///
518    /// Rows are *visual*, so this walks soft wraps one at a time rather than
519    /// jumping a whole paragraph — the opposite call from `ctrl-a`/`ctrl-e`,
520    /// and the right one: down should land where it looks like it will.
521    ///
522    /// Geometry rather than arithmetic on line numbers, so wrapped rows and hard
523    /// newlines are the same case and neither needs counting.
524    fn vertical(&mut self, rows: i32, extend: bool, window: &mut Window, cx: &mut Context<Self>) {
525        if self.last_layout.is_empty() {
526            return;
527        }
528        let line_height = window.line_height();
529        let Some(at) = position_for_offset(&self.last_layout, self.cursor_offset(), line_height)
530        else {
531            return;
532        };
533        let goal = self.goal_x.unwrap_or(at.x);
534        let target = at.y + line_height * rows as f32;
535        // Off the top is the start of the text and off the bottom is its end —
536        // what every native field does with up/down on the first/last row.
537        let offset = if target < px(0.) {
538            0
539        } else {
540            offset_for_position(&self.last_layout, gpui::point(goal, target), line_height)
541        };
542
543        if extend {
544            self.select_to(offset, cx);
545        } else {
546            self.move_to(offset, cx);
547        }
548        // Both of the above clear the goal; this is the one motion that keeps it.
549        self.goal_x = Some(goal);
550    }
551
552    /// `enter`. Guarded as well as bound to [`MULTILINE_KEY_CONTEXT`], because
553    /// an action can also be dispatched directly.
554    fn insert_newline(&mut self, _: &InsertNewline, window: &mut Window, cx: &mut Context<Self>) {
555        if self.shape.is_multiline() {
556            self.replace_text_in_range(None, "\n", window, cx);
557        }
558    }
559
560    fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
561        self.move_to(
562            previous_word_boundary(&self.content, self.cursor_offset()),
563            cx,
564        );
565    }
566
567    fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
568        self.move_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
569    }
570
571    fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
572        self.select_to(
573            previous_word_boundary(&self.content, self.cursor_offset()),
574            cx,
575        );
576    }
577
578    fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
579        self.select_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
580    }
581
582    /// Every delete-by-unit action is "extend the selection over the unit, then
583    /// replace it" — so a non-empty selection always wins, matching how every
584    /// native field behaves.
585    fn delete_word_left(
586        &mut self,
587        _: &DeleteWordLeft,
588        window: &mut Window,
589        cx: &mut Context<Self>,
590    ) {
591        if self.selected_range.is_empty() {
592            self.select_to(
593                previous_word_boundary(&self.content, self.cursor_offset()),
594                cx,
595            );
596        }
597        self.replace_text_in_range(None, "", window, cx)
598    }
599
600    fn delete_word_right(
601        &mut self,
602        _: &DeleteWordRight,
603        window: &mut Window,
604        cx: &mut Context<Self>,
605    ) {
606        if self.selected_range.is_empty() {
607            self.select_to(next_word_boundary(&self.content, self.cursor_offset()), cx);
608        }
609        self.replace_text_in_range(None, "", window, cx)
610    }
611
612    fn delete_to_line_start(
613        &mut self,
614        _: &DeleteToLineStart,
615        window: &mut Window,
616        cx: &mut Context<Self>,
617    ) {
618        if self.selected_range.is_empty() {
619            self.select_to(line_start(&self.content, self.cursor_offset()), cx);
620        }
621        self.replace_text_in_range(None, "", window, cx)
622    }
623
624    fn delete_to_line_end(
625        &mut self,
626        _: &DeleteToLineEnd,
627        window: &mut Window,
628        cx: &mut Context<Self>,
629    ) {
630        if self.selected_range.is_empty() {
631            self.select_to(line_end(&self.content, self.cursor_offset()), cx);
632        }
633        self.replace_text_in_range(None, "", window, cx)
634    }
635
636    fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
637        if self.selected_range.is_empty() {
638            let prev = self.previous_boundary(self.cursor_offset());
639            if self.cursor_offset() == prev {
640                return;
641            }
642            self.select_to(prev, cx)
643        }
644        self.replace_text_in_range(None, "", window, cx)
645    }
646
647    fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
648        if self.selected_range.is_empty() {
649            let next = self.next_boundary(self.cursor_offset());
650            if self.cursor_offset() == next {
651                return;
652            }
653            self.select_to(next, cx)
654        }
655        self.replace_text_in_range(None, "", window, cx)
656    }
657
658    fn on_mouse_down(
659        &mut self,
660        event: &MouseDownEvent,
661        window: &mut Window,
662        cx: &mut Context<Self>,
663    ) {
664        self.is_selecting = true;
665        let offset = self.index_for_mouse_position(event.position, window.line_height());
666        if event.modifiers.shift {
667            self.select_to(offset, cx);
668        } else {
669            self.move_to(offset, cx)
670        }
671    }
672
673    fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
674        self.is_selecting = false;
675    }
676
677    /// Scrolling is the one thing that moves the view without moving the caret,
678    /// so it deliberately does not set `follow_caret` — the next frame clamps
679    /// this, and the caret is left wherever it was.
680    fn on_scroll_wheel(
681        &mut self,
682        event: &gpui::ScrollWheelEvent,
683        window: &mut Window,
684        cx: &mut Context<Self>,
685    ) {
686        let delta = event.delta.pixel_delta(window.line_height());
687        self.scroll.x = (self.scroll.x - delta.x).max(px(0.));
688        self.scroll.y = (self.scroll.y - delta.y).max(px(0.));
689        cx.notify();
690    }
691
692    fn on_mouse_move(
693        &mut self,
694        event: &MouseMoveEvent,
695        window: &mut Window,
696        cx: &mut Context<Self>,
697    ) {
698        if self.is_selecting {
699            let offset = self.index_for_mouse_position(event.position, window.line_height());
700            self.select_to(offset, cx);
701        }
702    }
703
704    fn show_character_palette(
705        &mut self,
706        _: &ShowCharacterPalette,
707        window: &mut Window,
708        _: &mut Context<Self>,
709    ) {
710        window.show_character_palette();
711    }
712
713    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
714        if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
715            self.replace_text_in_range(None, &normalize(&text, self.shape), window, cx);
716        }
717    }
718
719    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
720        if !self.selected_range.is_empty() {
721            cx.write_to_clipboard(ClipboardItem::new_string(
722                self.content[self.selected_range.clone()].to_string(),
723            ));
724        }
725    }
726
727    fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
728        if !self.selected_range.is_empty() {
729            cx.write_to_clipboard(ClipboardItem::new_string(
730                self.content[self.selected_range.clone()].to_string(),
731            ));
732            self.replace_text_in_range(None, "", window, cx)
733        }
734    }
735
736    fn snapshot(&self) -> Snapshot {
737        Snapshot {
738            content: self.content.clone(),
739            selection: self.selected_range.clone(),
740            reversed: self.selection_reversed,
741        }
742    }
743
744    fn restore(&mut self, point: Snapshot, cx: &mut Context<Self>) {
745        self.content = point.content;
746        self.selected_range = point.selection;
747        self.selection_reversed = point.reversed;
748        self.marked_range = None;
749        // The next edit must not join whatever group was open before.
750        self.last_edit = None;
751        self.follow_caret = true;
752        cx.notify();
753    }
754
755    /// Record the state before an edit, unless that edit continues the group the
756    /// last one opened. Contiguity is the whole rule: the same kind of edit,
757    /// starting where the caret was left. Type a run and it is one step; move
758    /// the caret, or switch from typing to deleting, and the next one starts a
759    /// group of its own.
760    fn push_undo(&mut self, kind: EditKind, at: usize) {
761        if !joins_group(self.last_edit, kind, at) {
762            self.undo.push_back(self.snapshot());
763            while self.undo.len() > self.undo_limit {
764                self.undo.pop_front();
765            }
766        }
767        self.redo.clear();
768    }
769
770    fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
771        let Some(point) = self.undo.pop_back() else {
772            return;
773        };
774        self.redo.push(self.snapshot());
775        self.restore(point, cx);
776    }
777
778    fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
779        let Some(point) = self.redo.pop() else {
780            return;
781        };
782        self.undo.push_back(self.snapshot());
783        self.restore(point, cx);
784    }
785
786    fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
787        self.selected_range = offset..offset;
788        self.goal_x = None;
789        self.follow_caret = true;
790        cx.notify()
791    }
792
793    fn cursor_offset(&self) -> usize {
794        if self.selection_reversed {
795            self.selected_range.start
796        } else {
797            self.selected_range.end
798        }
799    }
800
801    /// Where the shaped text starts on screen: the box, moved up by the scroll.
802    /// Every mapping between a screen point and a byte offset goes through it.
803    fn text_origin(&self) -> Option<Point<Pixels>> {
804        Some(self.last_bounds?.origin - self.scroll)
805    }
806
807    fn index_for_mouse_position(&self, position: Point<Pixels>, line_height: Pixels) -> usize {
808        if self.content.is_empty() || self.last_layout.is_empty() {
809            return 0;
810        }
811        let Some(origin) = self.text_origin() else {
812            return 0;
813        };
814        offset_for_position(&self.last_layout, position - origin, line_height)
815    }
816
817    fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
818        self.goal_x = None;
819        self.follow_caret = true;
820        if self.selection_reversed {
821            self.selected_range.start = offset
822        } else {
823            self.selected_range.end = offset
824        };
825        if self.selected_range.end < self.selected_range.start {
826            self.selection_reversed = !self.selection_reversed;
827            self.selected_range = self.selected_range.end..self.selected_range.start;
828        }
829        cx.notify()
830    }
831
832    fn offset_from_utf16(&self, offset: usize) -> usize {
833        offset_from_utf16(&self.content, offset)
834    }
835
836    fn offset_to_utf16(&self, offset: usize) -> usize {
837        offset_to_utf16(&self.content, offset)
838    }
839
840    fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
841        self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
842    }
843
844    fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
845        self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
846    }
847
848    fn previous_boundary(&self, offset: usize) -> usize {
849        previous_boundary(&self.content, offset)
850    }
851
852    fn next_boundary(&self, offset: usize) -> usize {
853        next_boundary(&self.content, offset)
854    }
855}
856
857// ---------------------------------------------------------------------------
858// Pure offset math — the part with the sharp edges, kept free of gpui so it
859// can be unit-tested.
860// ---------------------------------------------------------------------------
861
862/// UTF-16 offset (what the platform IME speaks) → byte offset.
863pub fn offset_from_utf16(text: &str, offset: usize) -> usize {
864    let mut utf8_offset = 0;
865    let mut utf16_count = 0;
866    for ch in text.chars() {
867        if utf16_count >= offset {
868            break;
869        }
870        utf16_count += ch.len_utf16();
871        utf8_offset += ch.len_utf8();
872    }
873    utf8_offset
874}
875
876/// Byte offset → UTF-16 offset.
877pub fn offset_to_utf16(text: &str, offset: usize) -> usize {
878    let mut utf16_offset = 0;
879    let mut utf8_count = 0;
880    for ch in text.chars() {
881        if utf8_count >= offset {
882            break;
883        }
884        utf8_count += ch.len_utf8();
885        utf16_offset += ch.len_utf16();
886    }
887    utf16_offset
888}
889
890/// Previous *grapheme* boundary, so arrow keys and backspace step over a flag
891/// emoji or a combining mark as one unit instead of splitting it into pieces
892/// that render as garbage.
893pub fn previous_boundary(text: &str, offset: usize) -> usize {
894    text.grapheme_indices(true)
895        .rev()
896        .find_map(|(idx, _)| (idx < offset).then_some(idx))
897        .unwrap_or(0)
898}
899
900/// Next grapheme boundary; clamps to the end of the text.
901pub fn next_boundary(text: &str, offset: usize) -> usize {
902    text.grapheme_indices(true)
903        .find_map(|(idx, _)| (idx > offset).then_some(idx))
904        .unwrap_or(text.len())
905}
906
907/// Whether an edit continues the group the last one opened, rather than
908/// starting an undo step of its own.
909///
910/// Two conditions, both structural: the same kind of edit, landing where the
911/// last one left the caret. Deliberately not "within N milliseconds" — a time
912/// threshold is a number nobody has measured, and adjacency is what actually
913/// distinguishes a run of typing from a fresh thought somewhere else.
914pub fn joins_group(last: Option<(EditKind, usize)>, kind: EditKind, at: usize) -> bool {
915    last.is_some_and(|(last_kind, offset)| last_kind == kind && at == offset)
916}
917
918/// The line breaks a field of this shape is allowed to hold.
919///
920/// CRLF is folded to LF whatever the shape: `shape_text` splits on `\n` alone,
921/// so a surviving `\r` shapes as a glyph and puts every offset after it out by
922/// one. A single-line field then keeps the text but not the breaks — a pasted
923/// newline becomes a space rather than silently truncating what was pasted.
924///
925/// The invariant this buys: a [`Shape::Line`] field's content never contains a
926/// newline, so nothing downstream has to ask whether it might.
927pub fn normalize(text: &str, shape: Shape) -> String {
928    let text = text.replace("\r\n", "\n").replace('\r', "\n");
929    if shape.is_multiline() {
930        text
931    } else {
932        text.replace('\n', " ")
933    }
934}
935
936/// Start of the logical line holding `offset` — the byte after the previous
937/// newline.
938///
939/// Logical, not visual: with soft wrapping these two readings diverge, and
940/// `ctrl-a` here goes to the start of the whole paragraph rather than stopping
941/// at the wrap. That is emacs' `C-a`, and a deliberate divergence from macOS
942/// `NSTextView`, which stops at the visual row. On text with no newline — every
943/// [`Shape::Line`] field — the two are identical.
944pub fn line_start(text: &str, offset: usize) -> usize {
945    text[..offset].rfind('\n').map_or(0, |at| at + 1)
946}
947
948/// End of the logical line holding `offset` — the byte before the next newline.
949pub fn line_end(text: &str, offset: usize) -> usize {
950    text[offset..]
951        .find('\n')
952        .map_or(text.len(), |at| offset + at)
953}
954
955/// A word-bound segment counts as a word if it has any alphanumeric content;
956/// whitespace and punctuation runs are the things word motion skips over.
957fn is_word(segment: &str) -> bool {
958    segment.chars().any(char::is_alphanumeric)
959}
960
961/// Start of the word at or before `offset` — option-left.
962///
963/// Word units are Unicode word bounds (UAX#29), not space-delimited runs. In
964/// practice that means `foo.bar` and `foo_bar` are ONE word — a dot or
965/// underscore between letters does not break — while `a-b`, `path/to/file` and
966/// `foo, bar` do break. Good defaults for identifiers and paths, and verified
967/// against the segmenter rather than assumed (see tests).
968pub fn previous_word_boundary(text: &str, offset: usize) -> usize {
969    text.split_word_bound_indices()
970        .filter(|(start, _)| *start < offset)
971        .rfind(|(_, segment)| is_word(segment))
972        .map(|(start, _)| start)
973        .unwrap_or(0)
974}
975
976/// End of the word at or after `offset` — option-right.
977pub fn next_word_boundary(text: &str, offset: usize) -> usize {
978    text.split_word_bound_indices()
979        .filter(|(start, segment)| start + segment.len() > offset)
980        .find(|(_, segment)| is_word(segment))
981        .map(|(start, segment)| start + segment.len())
982        .unwrap_or(text.len())
983}
984
985/// What gets painted, and whether it is the placeholder — which is the only
986/// reason the colour differs.
987fn display_text(field: &TextField) -> (SharedString, bool) {
988    if field.content.is_empty() {
989        (field.placeholder.clone(), true)
990    } else {
991        (field.content.clone(), false)
992    }
993}
994
995// ---------------------------------------------------------------------------
996// Line geometry. `shape_text` returns one `WrappedLine` per hard newline, each
997// wrapping into rows of its own; gpui resolves positions *within* a line, so
998// everything here is walking that list and nothing re-implements shaping.
999// ---------------------------------------------------------------------------
1000
1001/// Each shaped line with the byte offset it starts at. `shape_text` splits on
1002/// `\n` and drops the separator, so each line starts one byte past the last.
1003fn lines_from(lines: &[WrappedLine]) -> impl Iterator<Item = (usize, &WrappedLine)> {
1004    lines.iter().scan(0usize, |start, line| {
1005        let at = *start;
1006        *start = at + line.len() + 1;
1007        Some((at, line))
1008    })
1009}
1010
1011/// Every visual row: the byte range it covers and its top edge, relative to the
1012/// text origin. A wrap boundary resolves to a byte index exactly the way
1013/// `WrappedLineLayout::position_for_index` does it internally — that mapping is
1014/// not exposed, and selection needs one quad per row.
1015fn rows(lines: &[WrappedLine], line_height: Pixels) -> Vec<(Range<usize>, Pixels)> {
1016    let mut out = Vec::new();
1017    let mut top = px(0.);
1018    for (start, line) in lines_from(lines) {
1019        let mut row_start = start;
1020        for boundary in line.wrap_boundaries() {
1021            let at = start + line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index;
1022            out.push((row_start..at, top));
1023            row_start = at;
1024            top += line_height;
1025        }
1026        out.push((row_start..start + line.len(), top));
1027        top += line_height;
1028    }
1029    out
1030}
1031
1032/// Byte offset → position relative to the text origin.
1033fn position_for_offset(
1034    lines: &[WrappedLine],
1035    offset: usize,
1036    line_height: Pixels,
1037) -> Option<Point<Pixels>> {
1038    let mut top = px(0.);
1039    for (start, line) in lines_from(lines) {
1040        if offset <= start + line.len() {
1041            let local = line.position_for_index(offset.saturating_sub(start), line_height)?;
1042            return Some(gpui::point(local.x, local.y + top));
1043        }
1044        top += line.size(line_height).height;
1045    }
1046    None
1047}
1048
1049/// Position relative to the text origin → the closest byte offset.
1050fn offset_for_position(
1051    lines: &[WrappedLine],
1052    position: Point<Pixels>,
1053    line_height: Pixels,
1054) -> usize {
1055    let mut top = px(0.);
1056    let mut last = 0;
1057    for (start, line) in lines_from(lines) {
1058        let height = line.size(line_height).height;
1059        last = start + line.len();
1060        if position.y < top + height {
1061            let local = gpui::point(position.x, position.y - top);
1062            let (Ok(index) | Err(index)) = line.closest_index_for_position(local, line_height);
1063            return start + index;
1064        }
1065        top += height;
1066    }
1067    last
1068}
1069
1070/// The selection as one rect per visual row, relative to the text origin.
1071///
1072/// A row's left edge is always x=0, so a continuation row is taken from there
1073/// rather than by looking the offset up — at a soft wrap the two rows share a
1074/// byte offset, and the lookup resolves it to the end of the earlier row.
1075fn selection_rows(
1076    lines: &[WrappedLine],
1077    range: &Range<usize>,
1078    line_height: Pixels,
1079) -> Vec<Bounds<Pixels>> {
1080    rows(lines, line_height)
1081        .into_iter()
1082        .filter(|(row, _)| range.start <= row.end && range.end >= row.start)
1083        .filter_map(|(row, top)| {
1084            let left = if range.start <= row.start {
1085                px(0.)
1086            } else {
1087                position_for_offset(lines, range.start, line_height)?.x
1088            };
1089            let right = position_for_offset(lines, range.end.min(row.end), line_height)?.x;
1090            (right > left).then(|| {
1091                Bounds::from_corners(
1092                    gpui::point(left, top),
1093                    gpui::point(right, top + line_height),
1094                )
1095            })
1096        })
1097        .collect()
1098}
1099
1100impl EntityInputHandler for TextField {
1101    fn text_for_range(
1102        &mut self,
1103        range_utf16: Range<usize>,
1104        actual_range: &mut Option<Range<usize>>,
1105        _window: &mut Window,
1106        _cx: &mut Context<Self>,
1107    ) -> Option<String> {
1108        let range = self.range_from_utf16(&range_utf16);
1109        actual_range.replace(self.range_to_utf16(&range));
1110        Some(self.content[range].to_string())
1111    }
1112
1113    fn selected_text_range(
1114        &mut self,
1115        _ignore_disabled_input: bool,
1116        _window: &mut Window,
1117        _cx: &mut Context<Self>,
1118    ) -> Option<UTF16Selection> {
1119        Some(UTF16Selection {
1120            range: self.range_to_utf16(&self.selected_range),
1121            reversed: self.selection_reversed,
1122        })
1123    }
1124
1125    fn marked_text_range(
1126        &self,
1127        _window: &mut Window,
1128        _cx: &mut Context<Self>,
1129    ) -> Option<Range<usize>> {
1130        self.marked_range
1131            .as_ref()
1132            .map(|range| self.range_to_utf16(range))
1133    }
1134
1135    fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
1136        self.marked_range = None;
1137    }
1138
1139    fn replace_text_in_range(
1140        &mut self,
1141        range_utf16: Option<Range<usize>>,
1142        new_text: &str,
1143        _: &mut Window,
1144        cx: &mut Context<Self>,
1145    ) {
1146        let range = range_utf16
1147            .as_ref()
1148            .map(|range_utf16| self.range_from_utf16(range_utf16))
1149            .or(self.marked_range.clone())
1150            .unwrap_or(self.selected_range.clone());
1151
1152        // Every edit lands here — typing, deleting, cut, paste, and the IME
1153        // *committing*. Not `replace_and_mark_text_in_range`, which is the
1154        // composing path: provisional text must not become undo steps, or every
1155        // keystroke of Japanese input would be one.
1156        let kind = if new_text.is_empty() {
1157            EditKind::Delete
1158        } else {
1159            EditKind::Insert
1160        };
1161        // A delete grows leftwards, so its group continues at the range's end;
1162        // an insert continues at its start.
1163        self.push_undo(
1164            kind,
1165            if new_text.is_empty() {
1166                range.end
1167            } else {
1168                range.start
1169            },
1170        );
1171
1172        self.content =
1173            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
1174                .into();
1175        self.selected_range = range.start + new_text.len()..range.start + new_text.len();
1176        self.marked_range.take();
1177        self.last_edit = Some((kind, self.selected_range.end));
1178        self.follow_caret = true;
1179        cx.notify();
1180    }
1181
1182    fn replace_and_mark_text_in_range(
1183        &mut self,
1184        range_utf16: Option<Range<usize>>,
1185        new_text: &str,
1186        new_selected_range_utf16: Option<Range<usize>>,
1187        _window: &mut Window,
1188        cx: &mut Context<Self>,
1189    ) {
1190        let range = range_utf16
1191            .as_ref()
1192            .map(|range_utf16| self.range_from_utf16(range_utf16))
1193            .or(self.marked_range.clone())
1194            .unwrap_or(self.selected_range.clone());
1195
1196        self.content =
1197            (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
1198                .into();
1199        self.marked_range =
1200            (!new_text.is_empty()).then(|| range.start..range.start + new_text.len());
1201        self.selected_range = new_selected_range_utf16
1202            .as_ref()
1203            .map(|range_utf16| self.range_from_utf16(range_utf16))
1204            .map(|new_range| new_range.start + range.start..new_range.end + range.end)
1205            .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
1206
1207        cx.notify();
1208    }
1209
1210    fn bounds_for_range(
1211        &mut self,
1212        range_utf16: Range<usize>,
1213        bounds: Bounds<Pixels>,
1214        window: &mut Window,
1215        _cx: &mut Context<Self>,
1216    ) -> Option<Bounds<Pixels>> {
1217        let range = self.range_from_utf16(&range_utf16);
1218        // The IME panel anchors under the composing text, so this has to be the
1219        // row that text is on, not the whole field. `bounds` is what
1220        // `last_bounds` is set from, so this is the same origin
1221        // [`TextField::offset_bounds`] measures from.
1222        self.row_bounds(bounds.origin - self.scroll, range, window.line_height())
1223    }
1224
1225    fn character_index_for_point(
1226        &mut self,
1227        point: Point<Pixels>,
1228        window: &mut Window,
1229        _cx: &mut Context<Self>,
1230    ) -> Option<usize> {
1231        self.last_bounds?.localize(&point)?;
1232        let origin = self.text_origin()?;
1233        let offset = offset_for_position(&self.last_layout, point - origin, window.line_height());
1234        Some(self.offset_to_utf16(offset))
1235    }
1236}
1237
1238impl Focusable for TextField {
1239    fn focus_handle(&self, _: &App) -> FocusHandle {
1240        self.focus_handle.clone()
1241    }
1242}
1243
1244impl Render for TextField {
1245    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1246        let theme = Theme::of(cx);
1247        let mut key_context = gpui::KeyContext::default();
1248        key_context.add(KEY_CONTEXT);
1249        if self.shape.is_multiline() {
1250            key_context.add(MULTILINE_KEY_CONTEXT);
1251        }
1252        if let Some(extra) = self.key_context.clone() {
1253            key_context.add(extra);
1254        }
1255        div()
1256            .key_context(key_context)
1257            .track_focus(&self.focus_handle(cx))
1258            .cursor(CursorStyle::IBeam)
1259            .on_action(cx.listener(Self::backspace))
1260            .on_action(cx.listener(Self::delete))
1261            .on_action(cx.listener(Self::left))
1262            .on_action(cx.listener(Self::right))
1263            .on_action(cx.listener(Self::select_left))
1264            .on_action(cx.listener(Self::select_right))
1265            .on_action(cx.listener(Self::select_all))
1266            .on_action(cx.listener(Self::home))
1267            .on_action(cx.listener(Self::end))
1268            .on_action(cx.listener(Self::select_home))
1269            .on_action(cx.listener(Self::select_end))
1270            .on_action(cx.listener(Self::word_left))
1271            .on_action(cx.listener(Self::word_right))
1272            .on_action(cx.listener(Self::select_word_left))
1273            .on_action(cx.listener(Self::select_word_right))
1274            .on_action(cx.listener(Self::up))
1275            .on_action(cx.listener(Self::down))
1276            .on_action(cx.listener(Self::select_up))
1277            .on_action(cx.listener(Self::select_down))
1278            .on_action(cx.listener(Self::insert_newline))
1279            .on_action(cx.listener(Self::undo))
1280            .on_action(cx.listener(Self::redo))
1281            .on_action(cx.listener(Self::delete_word_left))
1282            .on_action(cx.listener(Self::delete_word_right))
1283            .on_action(cx.listener(Self::delete_to_line_start))
1284            .on_action(cx.listener(Self::delete_to_line_end))
1285            .on_action(cx.listener(Self::show_character_palette))
1286            .on_action(cx.listener(Self::paste))
1287            .on_action(cx.listener(Self::cut))
1288            .on_action(cx.listener(Self::copy))
1289            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
1290            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
1291            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
1292            .on_mouse_move(cx.listener(Self::on_mouse_move))
1293            .on_scroll_wheel(cx.listener(Self::on_scroll_wheel))
1294            .w_full()
1295            .px(px(10.0))
1296            .py(px(7.0))
1297            .rounded(px(Theme::button_radius()))
1298            .bg(theme.input_bg)
1299            .border_1()
1300            .border_color(if self.focus_handle.is_focused(_window) {
1301                theme.ring
1302            } else {
1303                theme.border
1304            })
1305            .text_size(px(13.0))
1306            .line_height(px(18.0))
1307            .text_color(theme.text)
1308            .child(TextFieldElement { field: cx.entity() })
1309    }
1310}
1311
1312/// Paints the shaped lines plus selection and caret. A custom element because
1313/// all three are geometry derived from the shaped text, which only exists after
1314/// layout.
1315struct TextFieldElement {
1316    field: Entity<TextField>,
1317}
1318
1319struct FieldPrepaint {
1320    lines: Vec<WrappedLine>,
1321    /// Top-left of the text, which is the box moved up by the scroll offset.
1322    origin: Point<Pixels>,
1323    cursor: Option<PaintQuad>,
1324    /// One quad per visual row the selection covers.
1325    selection: Vec<PaintQuad>,
1326}
1327
1328impl IntoElement for TextFieldElement {
1329    type Element = Self;
1330
1331    fn into_element(self) -> Self::Element {
1332        self
1333    }
1334}
1335
1336impl Element for TextFieldElement {
1337    type RequestLayoutState = ();
1338    type PrepaintState = FieldPrepaint;
1339
1340    fn id(&self) -> Option<ElementId> {
1341        None
1342    }
1343
1344    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1345        None
1346    }
1347
1348    fn request_layout(
1349        &mut self,
1350        _id: Option<&GlobalElementId>,
1351        _inspector_id: Option<&gpui::InspectorElementId>,
1352        window: &mut Window,
1353        cx: &mut App,
1354    ) -> (LayoutId, ()) {
1355        let mut style = Style::default();
1356        style.size.width = relative(1.).into();
1357        let line_height = window.line_height();
1358        let field = self.field.read(cx);
1359        let shape = field.shape;
1360
1361        let (min, max) = match shape {
1362            Shape::Line => {
1363                style.size.height = line_height.into();
1364                return (window.request_layout(style, [], cx), ());
1365            }
1366            Shape::Rows(rows) => {
1367                style.size.height = (line_height * rows.max(1) as f32).into();
1368                return (window.request_layout(style, [], cx), ());
1369            }
1370            // Growing needs the row count, and the row count needs shaping at
1371            // the width layout is still deciding — which is exactly what a
1372            // measured layout is for.
1373            Shape::Grow { min, max } => (min.max(1), max.max(min.max(1))),
1374        };
1375
1376        let text = display_text(field).0;
1377        let id = window.request_measured_layout(style, move |known, available, window, _cx| {
1378            let text_style = window.text_style();
1379            let font_size = text_style.font_size.to_pixels(window.rem_size());
1380            // Prefer the width layout has already settled on. Taffy also probes
1381            // with min/max-content, where there is no width to wrap against —
1382            // and counting rows off unwrapped text under-reports them, which
1383            // would size the box for fewer lines than it goes on to paint.
1384            let wrap_width = known.width.or(match available.width {
1385                gpui::AvailableSpace::Definite(width) => Some(width),
1386                _ => None,
1387            });
1388            let run = TextRun {
1389                len: text.len(),
1390                font: text_style.font(),
1391                color: text_style.color,
1392                background_color: None,
1393                underline: None,
1394                strikethrough: None,
1395            };
1396            let count = window
1397                .text_system()
1398                .shape_text(text.clone(), font_size, &[run], wrap_width, None)
1399                .map(|lines| {
1400                    lines
1401                        .iter()
1402                        .map(|line| line.wrap_boundaries().len() + 1)
1403                        .sum::<usize>()
1404                })
1405                .unwrap_or(1);
1406            gpui::size(
1407                wrap_width.unwrap_or(px(0.)),
1408                line_height * count.clamp(min, max) as f32,
1409            )
1410        });
1411        (id, ())
1412    }
1413
1414    fn prepaint(
1415        &mut self,
1416        _id: Option<&GlobalElementId>,
1417        _inspector_id: Option<&gpui::InspectorElementId>,
1418        bounds: Bounds<Pixels>,
1419        _request_layout: &mut Self::RequestLayoutState,
1420        window: &mut Window,
1421        cx: &mut App,
1422    ) -> FieldPrepaint {
1423        let theme = Theme::of(cx).clone();
1424        let field = self.field.read(cx);
1425        let selected_range = field.selected_range.clone();
1426        let cursor = field.cursor_offset();
1427        let shape = field.shape;
1428        let marked_range = field.marked_range.clone();
1429        let scrolled = field.scroll;
1430        let follow_caret = field.follow_caret;
1431        let style = window.text_style();
1432
1433        let (text, is_placeholder) = display_text(field);
1434        let text_color = if is_placeholder {
1435            theme.text_faint
1436        } else {
1437            style.color
1438        };
1439
1440        let run = TextRun {
1441            len: text.len(),
1442            font: style.font(),
1443            color: text_color,
1444            background_color: None,
1445            underline: None,
1446            strikethrough: None,
1447        };
1448        // The IME composition range is underlined so the user can see what is
1449        // still provisional.
1450        let runs = if let Some(marked) = marked_range.as_ref() {
1451            vec![
1452                TextRun {
1453                    len: marked.start,
1454                    ..run.clone()
1455                },
1456                TextRun {
1457                    len: marked.end - marked.start,
1458                    underline: Some(UnderlineStyle {
1459                        color: Some(run.color),
1460                        thickness: px(1.0),
1461                        wavy: false,
1462                    }),
1463                    ..run.clone()
1464                },
1465                TextRun {
1466                    len: text.len() - marked.end,
1467                    ..run
1468                },
1469            ]
1470            .into_iter()
1471            .filter(|run| run.len > 0)
1472            .collect()
1473        } else {
1474            vec![run]
1475        };
1476
1477        let font_size = style.font_size.to_pixels(window.rem_size());
1478        let line_height = window.line_height();
1479        // A single line never wraps: it scrolls sideways instead, so shaping it
1480        // against the field's width would fold it into rows nothing can reach.
1481        let wrap_width = shape.is_multiline().then_some(bounds.size.width);
1482        let lines = window
1483            .text_system()
1484            .shape_text(text, font_size, &runs, wrap_width, None)
1485            .map(|lines| lines.into_vec())
1486            .unwrap_or_default();
1487
1488        // Clamp every frame, not just when scrolling: the content this is
1489        // measured against shrinks under it — delete the last line while parked
1490        // at the bottom and an unclamped offset leaves the box showing nothing.
1491        //
1492        // Only one axis is ever live. Wrapped lines are shaped to the box width,
1493        // so `max.x` is zero for a multi-line field; a single line is one row
1494        // tall, so `max.y` is zero for a single-line one. Neither needs asking
1495        // which shape it is.
1496        let content_height: Pixels = lines.iter().map(|l| l.size(line_height).height).sum();
1497        let content_width = lines.iter().map(|l| l.width()).fold(px(0.), Pixels::max);
1498        let max = gpui::point(
1499            (content_width - bounds.size.width).max(px(0.)),
1500            (content_height - bounds.size.height).max(px(0.)),
1501        );
1502        let mut scroll = gpui::point(
1503            scrolled.x.clamp(px(0.), max.x),
1504            scrolled.y.clamp(px(0.), max.y),
1505        );
1506        if follow_caret && let Some(at) = position_for_offset(&lines, cursor, line_height) {
1507            if at.y < scroll.y {
1508                scroll.y = at.y;
1509            } else if at.y + line_height > scroll.y + bounds.size.height {
1510                scroll.y = at.y + line_height - bounds.size.height;
1511            }
1512            // The caret is the thing being kept in view, so it is its own width
1513            // that has to clear the right edge — not the character before it.
1514            if at.x < scroll.x {
1515                scroll.x = at.x;
1516            } else if at.x + CARET_WIDTH > scroll.x + bounds.size.width {
1517                scroll.x = at.x + CARET_WIDTH - bounds.size.width;
1518            }
1519            scroll.x = scroll.x.clamp(px(0.), max.x);
1520            scroll.y = scroll.y.clamp(px(0.), max.y);
1521        }
1522        self.field.update(cx, |field, _| {
1523            field.scroll = scroll;
1524            field.follow_caret = false;
1525        });
1526        let origin = bounds.origin - scroll;
1527
1528        let (selection, cursor) = if selected_range.is_empty() {
1529            let at = position_for_offset(&lines, cursor, line_height).unwrap_or_default();
1530            (
1531                Vec::new(),
1532                Some(fill(
1533                    Bounds::new(origin + at, gpui::size(CARET_WIDTH, line_height)),
1534                    theme.caret,
1535                )),
1536            )
1537        } else {
1538            (
1539                selection_rows(&lines, &selected_range, line_height)
1540                    .into_iter()
1541                    .map(|rect| {
1542                        fill(
1543                            Bounds::new(origin + rect.origin, rect.size),
1544                            theme.selection,
1545                        )
1546                    })
1547                    .collect(),
1548                None,
1549            )
1550        };
1551
1552        FieldPrepaint {
1553            lines,
1554            origin,
1555            cursor,
1556            selection,
1557        }
1558    }
1559
1560    fn paint(
1561        &mut self,
1562        _id: Option<&GlobalElementId>,
1563        _inspector_id: Option<&gpui::InspectorElementId>,
1564        bounds: Bounds<Pixels>,
1565        _request_layout: &mut Self::RequestLayoutState,
1566        prepaint: &mut Self::PrepaintState,
1567        window: &mut Window,
1568        cx: &mut App,
1569    ) {
1570        let focus_handle = self.field.read(cx).focus_handle.clone();
1571        window.handle_input(
1572            &focus_handle,
1573            ElementInputHandler::new(bounds, self.field.clone()),
1574            cx,
1575        );
1576        let line_height = window.line_height();
1577        let lines = std::mem::take(&mut prepaint.lines);
1578        let selection = std::mem::take(&mut prepaint.selection);
1579        let cursor = prepaint.cursor.take();
1580        let origin = prepaint.origin;
1581
1582        // Scrolled text runs past the box in both directions, so everything the
1583        // field draws is masked to it — text, selection and caret alike.
1584        window.with_content_mask(Some(gpui::ContentMask { bounds }), |window| {
1585            for selection in selection {
1586                window.paint_quad(selection);
1587            }
1588
1589            let mut top = origin;
1590            for line in &lines {
1591                line.paint(top, line_height, gpui::TextAlign::Left, None, window, cx)
1592                    .ok();
1593                top.y += line.size(line_height).height;
1594            }
1595
1596            // The caret only exists while focused — an unfocused field showing
1597            // one reads as two cursors on screen.
1598            if focus_handle.is_focused(window)
1599                && let Some(cursor) = cursor
1600            {
1601                window.paint_quad(cursor);
1602            }
1603        });
1604
1605        self.field.update(cx, |field, _| {
1606            field.last_layout = lines;
1607            field.last_bounds = Some(bounds);
1608        });
1609    }
1610}