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