Skip to main content

guise/input/
line.rs

1//! The shared guts of every single-line text field.
2//!
3//! Before this existed each field drew its value as three sibling divs —
4//! `before`, a 1px caret, `after` — which meant the caret landed wherever the
5//! layout engine happened to break the boxes, the mouse could not point at a
6//! character at all, and a value longer than the box was simply clipped. This
7//! module replaces that with a real gpui [`Element`] that shapes the line
8//! through the text system, so:
9//!
10//! - the caret sits on a glyph boundary, and the mouse can hit-test into the
11//!   text (click to place, drag to select, double-click a word, triple-click
12//!   the line),
13//! - a long value scrolls horizontally to keep the caret in view instead of
14//!   disappearing under the border,
15//! - painting registers an [`ElementInputHandler`], which is the only way to
16//!   get IME composition, dead keys, press-and-hold accents, the macOS
17//!   character palette, and the system's own text services. A plain
18//!   `on_key_down` handler cannot see any of them.
19//!
20//! Text entry therefore does **not** run through key handling: the platform
21//! delivers it to [`EntityInputHandler::replace_text_in_range`] after the key
22//! handler declines it. Key handling covers navigation, deletion, the
23//! clipboard, undo, and focus movement only.
24//!
25//! A field opts in by implementing [`LineEditor`] and calling
26//! [`line_input_handler!`] to get the platform trait for free.
27
28use std::ops::Range;
29use std::time::Duration;
30
31use gpui::prelude::*;
32use gpui::{
33    fill, point, px, size, App, Bounds, ClipboardItem, Context, ElementInputHandler, Entity,
34    FocusHandle, GlobalElementId, Hsla, KeyDownEvent, LayoutId, MouseDownEvent, MouseMoveEvent,
35    MouseUpEvent, PaintQuad, Pixels, Point, ShapedLine, SharedString, Style, TextRun,
36    UnderlineStyle, Window,
37};
38
39use super::edit::TextEdit;
40use super::{apply_nav, KeyOutcome};
41use crate::theme::theme;
42
43/// What a masked field shows instead of its characters.
44const MASK: char = '\u{2022}';
45/// The same bullet as a string, so masking is one allocation rather than two.
46const MASK_STR: &str = "\u{2022}";
47
48/// How long the caret stays visible, then hidden. Matches the platform's own
49/// text fields closely enough that the two don't visibly beat against each
50/// other when both are on screen.
51const BLINK: Duration = Duration::from_millis(530);
52
53/// Breathing room kept between the caret and the field edge while scrolling,
54/// so the caret never sits flush against the border.
55const SCROLL_PAD: f32 = 2.0;
56
57/// Per-field state owned by the host entity but written by the element: the
58/// shaped line and bounds from the last paint (which is what hit-testing and
59/// the platform's `bounds_for_range` need), the horizontal scroll offset, the
60/// in-progress IME composition, and the caret blink.
61#[derive(Debug, Default)]
62pub struct LineState {
63    /// The line as it was last shaped — masked text for a password field,
64    /// the placeholder when the value is empty.
65    pub(crate) shaped: Option<ShapedLine>,
66    pub(crate) bounds: Option<Bounds<Pixels>>,
67    /// How far the text is scrolled left, in pixels, to keep the caret visible.
68    pub(crate) scroll: Pixels,
69    /// The IME's in-progress composition, as a char range into the real text.
70    pub(crate) marked: Option<Range<usize>>,
71    /// True between mouse-down and mouse-up, while a drag extends a selection.
72    pub(crate) selecting: bool,
73    /// Whether the last paint masked the text, so index mapping can undo it.
74    pub(crate) masked: bool,
75    /// Whether the last paint showed the placeholder rather than a value.
76    pub(crate) empty: bool,
77    pub(crate) focused: bool,
78    pub(crate) caret_on: bool,
79    /// Whether a blink task is already running for this field.
80    pub(crate) blinking: bool,
81}
82
83impl LineState {
84    pub fn new() -> Self {
85        LineState {
86            caret_on: true,
87            ..Default::default()
88        }
89    }
90
91    /// Byte offset into the *shaped* line for a char index into the real text.
92    /// The two differ when the field is masked, because one bullet stands in
93    /// for one char but takes three bytes.
94    fn shaped_byte(&self, edit: &TextEdit, index: usize) -> usize {
95        if self.masked {
96            index.min(edit.len()) * MASK.len_utf8()
97        } else {
98            edit.byte_of(index)
99        }
100    }
101
102    /// The inverse of [`shaped_byte`](Self::shaped_byte).
103    fn char_index(&self, edit: &TextEdit, byte: usize) -> usize {
104        if self.masked {
105            (byte / MASK.len_utf8()).min(edit.len())
106        } else {
107            edit.char_of(byte)
108        }
109    }
110
111    /// The char index the given window-space point falls on. Returns `None`
112    /// when the field hasn't been painted yet or is showing its placeholder,
113    /// in which case there is nowhere to point but the start.
114    pub(crate) fn index_at(&self, edit: &TextEdit, position: Point<Pixels>) -> Option<usize> {
115        let (bounds, shaped) = (self.bounds?, self.shaped.as_ref()?);
116        if self.empty {
117            return Some(0);
118        }
119        let x = position.x - bounds.left() + self.scroll;
120        Some(self.char_index(edit, shaped.closest_index_for_x(x)))
121    }
122
123    /// Note that something happened worth showing a solid caret for.
124    fn wake(&mut self) {
125        self.caret_on = true;
126    }
127}
128
129/// A field that can be drawn and driven by [`Line`].
130///
131/// Implementors keep the buffer and the [`LineState`]; everything else — glyph
132/// layout, hit-testing, scrolling, IME, the clipboard — is shared.
133/// The platform trait comes along for the ride: a field that can be drawn by
134/// [`Line`] is by definition one the OS can drive text into, and every
135/// implementor gets it from [`line_input_handler!`].
136pub trait LineEditor: 'static + Sized + gpui::EntityInputHandler {
137    fn edit(&self) -> &TextEdit;
138    fn edit_mut(&mut self) -> &mut TextEdit;
139    fn line(&self) -> &LineState;
140    fn line_mut(&mut self) -> &mut LineState;
141    fn line_focus(&self) -> &FocusHandle;
142
143    /// Show bullets instead of characters, and refuse to copy or cut.
144    fn line_masked(&self) -> bool {
145        false
146    }
147
148    /// Accept focus, selection, and copy, but reject every mutation.
149    fn line_read_only(&self) -> bool {
150        false
151    }
152
153    /// Cap on the value's length in chars, enforced on typing and on paste the
154    /// way an `<input maxlength>` is.
155    fn line_max_length(&self) -> Option<usize> {
156        None
157    }
158
159    /// Narrow what may be entered, the way `<input type="number">` does.
160    /// Returns the text to actually insert; dropping every char rejects the
161    /// input. Applied to typing, IME, drops, and paste alike, so a field can't
162    /// be filled with something it rejects by any route.
163    fn line_filter(&self, text: String) -> String {
164        text
165    }
166
167    /// Called after any mutation so the field can emit its change event. The
168    /// element never calls `cx.notify` on the host's behalf; do it here.
169    fn line_changed(&mut self, cx: &mut Context<Self>);
170}
171
172/// Implement gpui's [`EntityInputHandler`] for a [`LineEditor`].
173///
174/// The trait can't be blanket-implemented — it's foreign, and the orphan rules
175/// reject an uncovered type parameter — so each field opts in by name. Every
176/// body here is the same mechanical translation between the platform's UTF-16
177/// offsets and the model's char indices.
178macro_rules! line_input_handler {
179    ($ty:ty) => {
180        impl ::gpui::EntityInputHandler for $ty {
181            fn text_for_range(
182                &mut self,
183                range_utf16: ::std::ops::Range<usize>,
184                actual: &mut Option<::std::ops::Range<usize>>,
185                _window: &mut ::gpui::Window,
186                _cx: &mut ::gpui::Context<Self>,
187            ) -> Option<String> {
188                let range = $crate::input::line::from_utf16(self.edit(), &range_utf16);
189                actual.replace($crate::input::line::to_utf16(self.edit(), &range));
190                Some($crate::input::line::slice(self.edit(), &range))
191            }
192
193            fn selected_text_range(
194                &mut self,
195                _ignore_disabled: bool,
196                _window: &mut ::gpui::Window,
197                _cx: &mut ::gpui::Context<Self>,
198            ) -> Option<::gpui::UTF16Selection> {
199                Some($crate::input::line::utf16_selection(self.edit()))
200            }
201
202            fn marked_text_range(
203                &self,
204                _window: &mut ::gpui::Window,
205                _cx: &mut ::gpui::Context<Self>,
206            ) -> Option<::std::ops::Range<usize>> {
207                let marked = self.line().marked.clone()?;
208                Some($crate::input::line::to_utf16(self.edit(), &marked))
209            }
210
211            fn unmark_text(
212                &mut self,
213                _window: &mut ::gpui::Window,
214                _cx: &mut ::gpui::Context<Self>,
215            ) {
216                self.line_mut().marked = None;
217            }
218
219            fn replace_text_in_range(
220                &mut self,
221                range_utf16: Option<::std::ops::Range<usize>>,
222                text: &str,
223                _window: &mut ::gpui::Window,
224                cx: &mut ::gpui::Context<Self>,
225            ) {
226                $crate::input::line::replace(self, range_utf16, text, None, cx);
227            }
228
229            fn replace_and_mark_text_in_range(
230                &mut self,
231                range_utf16: Option<::std::ops::Range<usize>>,
232                text: &str,
233                selected_utf16: Option<::std::ops::Range<usize>>,
234                _window: &mut ::gpui::Window,
235                cx: &mut ::gpui::Context<Self>,
236            ) {
237                $crate::input::line::replace(self, range_utf16, text, Some(selected_utf16), cx);
238            }
239
240            fn bounds_for_range(
241                &mut self,
242                range_utf16: ::std::ops::Range<usize>,
243                bounds: ::gpui::Bounds<::gpui::Pixels>,
244                _window: &mut ::gpui::Window,
245                _cx: &mut ::gpui::Context<Self>,
246            ) -> Option<::gpui::Bounds<::gpui::Pixels>> {
247                $crate::input::line::range_bounds(self, range_utf16, bounds)
248            }
249
250            fn character_index_for_point(
251                &mut self,
252                point: ::gpui::Point<::gpui::Pixels>,
253                _window: &mut ::gpui::Window,
254                _cx: &mut ::gpui::Context<Self>,
255            ) -> Option<usize> {
256                let index = self.line().index_at(self.edit(), point)?;
257                Some($crate::input::line::to_utf16(self.edit(), &(0..index)).end)
258            }
259        }
260    };
261}
262
263pub(crate) use line_input_handler;
264
265// --- UTF-16 translation -----------------------------------------------------
266//
267// The platform addresses text in UTF-16 code units; the model counts chars.
268// These four helpers are the only place that conversion happens.
269
270pub(crate) fn to_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
271    // Over `chars()` rather than `text()`: the platform calls these several
272    // times per keystroke, and `text()` builds a fresh `String` each time.
273    let buffer = edit.chars();
274    let units = |chars: usize| {
275        buffer[..chars.min(buffer.len())]
276            .iter()
277            .map(|c| c.len_utf16())
278            .sum::<usize>()
279    };
280    units(range.start)..units(range.end)
281}
282
283pub(crate) fn from_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
284    let buffer = edit.chars();
285    let chars = |units: usize| {
286        let mut seen = 0;
287        for (index, c) in buffer.iter().enumerate() {
288            if seen >= units {
289                return index;
290            }
291            seen += c.len_utf16();
292        }
293        buffer.len()
294    };
295    chars(range.start)..chars(range.end)
296}
297
298pub(crate) fn slice(edit: &TextEdit, range: &Range<usize>) -> String {
299    let buffer = edit.chars();
300    let start = range.start.min(buffer.len());
301    let end = range.end.clamp(start, buffer.len());
302    buffer[start..end].iter().collect()
303}
304
305pub(crate) fn utf16_selection(edit: &TextEdit) -> gpui::UTF16Selection {
306    let (start, end) = edit.selection().unwrap_or((edit.cursor(), edit.cursor()));
307    let reversed = edit.cursor() == start && start != end;
308    gpui::UTF16Selection {
309        range: to_utf16(edit, &(start..end)),
310        reversed,
311    }
312}
313
314/// A single-line field never holds a line break, exactly like `<input>`, which
315/// flattens them out of anything pasted or dropped into it. Other control
316/// characters would render as nothing useful, so they go too — and this is
317/// also what makes it impossible for a stray `\t` to be typed.
318fn flatten(text: &str) -> String {
319    text.chars()
320        .map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
321        .filter(|c| !c.is_control())
322        .collect()
323}
324
325/// The shared body of `replace_text_in_range` and its marked-text sibling.
326/// `marking` is `Some(selection)` when the IME is mid-composition.
327pub(crate) fn replace<V: LineEditor>(
328    this: &mut V,
329    range_utf16: Option<Range<usize>>,
330    text: &str,
331    marking: Option<Option<Range<usize>>>,
332    cx: &mut Context<V>,
333) {
334    if this.line_read_only() {
335        return;
336    }
337    let text = this.line_filter(flatten(text));
338
339    let range = range_utf16
340        .map(|r| from_utf16(this.edit(), &r))
341        .or_else(|| this.line().marked.clone())
342        .unwrap_or_else(|| {
343            this.edit()
344                .selection()
345                .map(|(s, e)| s..e)
346                .unwrap_or_else(|| this.edit().cursor()..this.edit().cursor())
347        });
348
349    // `maxlength` counts what would remain, so a replacement that swaps a
350    // selection for something the same size always fits.
351    let text = match this.line_max_length() {
352        Some(max) => {
353            let kept = this.edit().len() - range.len().min(this.edit().len());
354            text.chars().take(max.saturating_sub(kept)).collect()
355        }
356        None => text,
357    };
358
359    let start = range.start;
360    this.edit_mut().replace_range(range, &text);
361    match marking {
362        Some(selection) => {
363            let end = start + text.chars().count();
364            this.line_mut().marked = (!text.is_empty()).then_some(start..end);
365            if let Some(selection) = selection {
366                let selection = from_utf16(this.edit(), &selection);
367                this.edit_mut()
368                    .set_selection(start + selection.start, start + selection.end);
369            }
370        }
371        None => this.line_mut().marked = None,
372    }
373    this.line_mut().wake();
374    this.line_changed(cx);
375}
376
377/// Where a char range sits on screen, for the IME's candidate window.
378pub(crate) fn range_bounds<V: LineEditor>(
379    this: &V,
380    range_utf16: Range<usize>,
381    bounds: Bounds<Pixels>,
382) -> Option<Bounds<Pixels>> {
383    let shaped = this.line().shaped.as_ref()?;
384    let range = from_utf16(this.edit(), &range_utf16);
385    let x = |index: usize| {
386        bounds.left() + shaped.x_for_index(this.line().shaped_byte(this.edit(), index))
387            - this.line().scroll
388    };
389    Some(Bounds::from_corners(
390        point(x(range.start), bounds.top()),
391        point(x(range.end), bounds.bottom()),
392    ))
393}
394
395// --- mouse ------------------------------------------------------------------
396
397/// Focus the field and place (or extend) the selection where the user clicked.
398/// Click counts follow the platform convention a browser also uses: one places
399/// the caret, two takes the word, three takes the whole value.
400pub(crate) fn mouse_down<V: LineEditor>(
401    this: &mut V,
402    event: &MouseDownEvent,
403    window: &mut Window,
404    cx: &mut Context<V>,
405) {
406    window.focus(this.line_focus());
407    let Some(index) = this.line().index_at(this.edit(), event.position) else {
408        cx.notify();
409        return;
410    };
411    match event.click_count {
412        1 if event.modifiers.shift => this.edit_mut().extend_to(index),
413        1 => this.edit_mut().set_cursor(index),
414        2 => {
415            let (start, end) = this.edit().word_at(index);
416            this.edit_mut().set_selection(start, end);
417        }
418        _ => this.edit_mut().select_all(),
419    }
420    this.line_mut().selecting = true;
421    this.line_mut().wake();
422    cx.notify();
423}
424
425/// Extend the selection while the button is held.
426pub(crate) fn mouse_move<V: LineEditor>(
427    this: &mut V,
428    event: &MouseMoveEvent,
429    _window: &mut Window,
430    cx: &mut Context<V>,
431) {
432    if !this.line().selecting {
433        return;
434    }
435    if let Some(index) = this.line().index_at(this.edit(), event.position) {
436        this.edit_mut().extend_to(index);
437        cx.notify();
438    }
439}
440
441pub(crate) fn mouse_up<V: LineEditor>(
442    this: &mut V,
443    _event: &MouseUpEvent,
444    _window: &mut Window,
445    cx: &mut Context<V>,
446) {
447    if this.line().selecting {
448        this.line_mut().selecting = false;
449        cx.notify();
450    }
451}
452
453/// Attach the mouse handling every field shares, and mark it a text surface.
454///
455/// The four handlers have to travel together — a selection drag that only
456/// registers `on_mouse_up` and not `on_mouse_up_out` never ends when the
457/// pointer leaves the field — so they are applied in one place rather than
458/// copied into each field's `render`. Fields that need more (a picker that
459/// also opens its list) add their own handler after; gpui runs both.
460pub(crate) fn wire<V: LineEditor>(
461    element: gpui::Stateful<gpui::Div>,
462    focus: &FocusHandle,
463    cx: &mut Context<V>,
464) -> gpui::Stateful<gpui::Div> {
465    element
466        .track_focus(focus)
467        .cursor(gpui::CursorStyle::IBeam)
468        .on_mouse_down(gpui::MouseButton::Left, cx.listener(mouse_down))
469        .on_mouse_move(cx.listener(mouse_move))
470        .on_mouse_up(gpui::MouseButton::Left, cx.listener(mouse_up))
471        .on_mouse_up_out(gpui::MouseButton::Left, cx.listener(mouse_up))
472}
473
474/// Give a field the Tab-order and focus accessors every form control needs.
475///
476/// These were copy-pasted into three fields and simply absent from the other
477/// four, which left half the inputs in the Tab ring with no way for a host to
478/// order them or focus them on open.
479macro_rules! line_focus_builders {
480    ($ty:ty) => {
481        impl $ty {
482            /// Where this field sits in the window's Tab order. Fields default
483            /// to 0, which walks them in render order; set this only to
484            /// override that.
485            pub fn tab_index(mut self, index: isize) -> Self {
486                self.focus = self.focus.clone().tab_index(index);
487                self
488            }
489
490            /// Leave the field out of the Tab order without disabling it.
491            pub fn tab_stop(mut self, tab_stop: bool) -> Self {
492                self.focus = self.focus.clone().tab_stop(tab_stop);
493                self
494            }
495
496            /// The field's focus handle, so a host can focus it on open.
497            pub fn focus_handle(&self) -> ::gpui::FocusHandle {
498                self.focus.clone()
499            }
500        }
501    };
502}
503
504pub(crate) use line_focus_builders;
505
506// --- keyboard ---------------------------------------------------------------
507
508/// The keys a single-line field handles itself: the clipboard, undo, focus
509/// movement, then navigation and deletion via
510/// [`apply_nav`](super::apply_nav).
511///
512/// Printable input is deliberately absent. Returning [`KeyOutcome::Pass`] for
513/// it lets the platform hand the key to the input handler instead, which is
514/// what makes dead keys, IME composition, and press-and-hold work.
515pub(crate) fn keys<V: LineEditor>(
516    this: &mut V,
517    event: &KeyDownEvent,
518    window: &mut Window,
519    cx: &mut Context<V>,
520) -> KeyOutcome {
521    let ks = &event.keystroke;
522    let m = &ks.modifiers;
523
524    // Tab is focus movement, never a character. Without this the platform
525    // reports a `\t` for it and the field would type one, which is the one
526    // behaviour every HTML form gets right for free.
527    if ks.key == "tab" && !m.platform && !m.control {
528        if m.shift {
529            window.focus_prev();
530        } else {
531            window.focus_next();
532        }
533        cx.stop_propagation();
534        return KeyOutcome::Edited;
535    }
536
537    if m.platform && !m.alt && !m.control {
538        match ks.key.as_str() {
539            "c" => return copy(this, cx),
540            "x" => return cut(this, cx),
541            "v" => return paste(this, cx),
542            "z" if m.shift => return history(this, false, cx),
543            "z" => return history(this, true, cx),
544            "y" => return history(this, false, cx),
545            _ => {}
546        }
547    }
548
549    if this.line_read_only() && mutates(ks.key.as_str()) {
550        return KeyOutcome::Pass;
551    }
552
553    let outcome = apply_nav(this.edit_mut(), ks);
554    if outcome == KeyOutcome::Edited {
555        this.line_mut().wake();
556    }
557    outcome
558}
559
560/// Whether a key would change the text, for the read-only check.
561fn mutates(key: &str) -> bool {
562    matches!(key, "backspace" | "delete" | "k")
563}
564
565fn copy<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
566    if !this.line_masked() {
567        if let Some(text) = this.edit().selected_text() {
568            cx.write_to_clipboard(ClipboardItem::new_string(text));
569        }
570    }
571    cx.stop_propagation();
572    KeyOutcome::Pass
573}
574
575fn cut<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
576    if this.line_masked() || this.line_read_only() {
577        cx.stop_propagation();
578        return KeyOutcome::Pass;
579    }
580    let Some(text) = this.edit().selected_text() else {
581        cx.stop_propagation();
582        return KeyOutcome::Pass;
583    };
584    cx.write_to_clipboard(ClipboardItem::new_string(text));
585    this.edit_mut().delete_selection();
586    this.line_mut().wake();
587    cx.stop_propagation();
588    KeyOutcome::Edited
589}
590
591fn paste<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
592    if this.line_read_only() {
593        cx.stop_propagation();
594        return KeyOutcome::Pass;
595    }
596    let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
597        cx.stop_propagation();
598        return KeyOutcome::Pass;
599    };
600    let text = this.line_filter(flatten(&text));
601    let text = match this.line_max_length() {
602        Some(max) => {
603            let selected = this.edit().selection().map_or(0, |(s, e)| e - s);
604            let room = max.saturating_sub(this.edit().len() - selected);
605            text.chars().take(room).collect()
606        }
607        None => text,
608    };
609    this.edit_mut().break_undo();
610    this.edit_mut().insert(&text);
611    this.edit_mut().break_undo();
612    this.line_mut().wake();
613    cx.stop_propagation();
614    KeyOutcome::Edited
615}
616
617fn history<V: LineEditor>(this: &mut V, undo: bool, cx: &mut Context<V>) -> KeyOutcome {
618    if this.line_read_only() {
619        cx.stop_propagation();
620        return KeyOutcome::Pass;
621    }
622    let changed = if undo {
623        this.edit_mut().undo()
624    } else {
625        this.edit_mut().redo()
626    };
627    this.line_mut().wake();
628    cx.stop_propagation();
629    if changed {
630        KeyOutcome::Edited
631    } else {
632        KeyOutcome::Pass
633    }
634}
635
636// --- the element ------------------------------------------------------------
637
638/// The text, caret, and selection of a single-line field.
639///
640/// Give it the entity that owns the buffer and the colors already resolved
641/// from the theme; it handles the rest. Put it inside the field's chrome —
642/// it fills the width it is given and is one line tall.
643pub struct Line<V: LineEditor> {
644    field: Entity<V>,
645    placeholder: SharedString,
646    /// Only the placeholder's color varies between fields — a picker that has
647    /// a value shows it in the text color rather than dimmed. The rest is the
648    /// theme's, resolved at paint.
649    placeholder_color: Option<Hsla>,
650}
651
652impl<V: LineEditor> Line<V> {
653    pub fn new(field: Entity<V>) -> Self {
654        Line {
655            field,
656            placeholder: SharedString::default(),
657            placeholder_color: None,
658        }
659    }
660
661    /// Text to show while the field is empty, and the color to show it in.
662    pub fn placeholder(mut self, placeholder: impl Into<SharedString>, color: Hsla) -> Self {
663        self.placeholder = placeholder.into();
664        self.placeholder_color = Some(color);
665        self
666    }
667}
668
669/// What `prepaint` worked out for `paint` to draw.
670pub struct LinePrepaint {
671    shaped: Option<ShapedLine>,
672    caret: Option<PaintQuad>,
673    selection: Option<PaintQuad>,
674    scroll: Pixels,
675}
676
677impl<V: LineEditor> IntoElement for Line<V> {
678    type Element = Self;
679
680    fn into_element(self) -> Self::Element {
681        self
682    }
683}
684
685impl<V: LineEditor> Element for Line<V> {
686    type RequestLayoutState = ();
687    type PrepaintState = LinePrepaint;
688
689    fn id(&self) -> Option<gpui::ElementId> {
690        None
691    }
692
693    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
694        None
695    }
696
697    fn request_layout(
698        &mut self,
699        _id: Option<&GlobalElementId>,
700        _inspector: Option<&gpui::InspectorElementId>,
701        window: &mut Window,
702        cx: &mut App,
703    ) -> (LayoutId, ()) {
704        let mut style = Style::default();
705        style.size.width = gpui::relative(1.0).into();
706        style.size.height = window.line_height().into();
707        (window.request_layout(style, [], cx), ())
708    }
709
710    fn prepaint(
711        &mut self,
712        _id: Option<&GlobalElementId>,
713        _inspector: Option<&gpui::InspectorElementId>,
714        bounds: Bounds<Pixels>,
715        _layout: &mut (),
716        window: &mut Window,
717        cx: &mut App,
718    ) -> LinePrepaint {
719        // The field's visuals are the theme's, so they are read here rather
720        // than threaded through `Line::new` by seven callers that all passed
721        // the same three values.
722        let t = theme(cx);
723        let text_color = t.text().hsla();
724        let caret_color = t.primary().hsla();
725        let selection_color = t.selection();
726        let dimmed = t.dimmed().hsla();
727
728        let focused = self.field.read(cx).line_focus().is_focused(window);
729        let field = self.field.read(cx);
730        let masked = field.line_masked();
731        let empty = field.edit().is_empty();
732        let cursor = field.edit().cursor();
733        let selection = field.edit().selection();
734        let marked = field.line().marked.clone();
735        let chars = field.edit().len();
736        let caret_on = field.line().caret_on;
737
738        // Built per branch rather than up front: a masked field would otherwise
739        // allocate and copy the whole cleartext buffer every frame only to
740        // throw it away, and an empty one would build an empty `String`.
741        let display: SharedString = if empty {
742            self.placeholder.clone()
743        } else if masked {
744            SharedString::from(MASK_STR.repeat(chars))
745        } else {
746            SharedString::from(field.edit().text())
747        };
748
749        let style = window.text_style();
750        let font_size = style.font_size.to_pixels(window.rem_size());
751        let color = if empty {
752            self.placeholder_color.unwrap_or(dimmed)
753        } else {
754            text_color
755        };
756        let run = TextRun {
757            len: display.len(),
758            font: style.font(),
759            color,
760            background_color: None,
761            underline: None,
762            strikethrough: None,
763        };
764
765        // The IME underlines what it is still composing, so the user can see
766        // which characters are provisional.
767        let runs = match marked.filter(|_| !empty) {
768            Some(marked) => {
769                // Runs have to cover the shaped string exactly — the text
770                // system sums their lengths and slices the string by the
771                // total, so an over-long run is a panic, not a mis-draw. The
772                // marked range is the IME's view of the text and can outlive
773                // an edit that shortened it, so clamp rather than trust it.
774                let limit = display.len();
775                let byte = |index: usize| {
776                    if masked {
777                        index.saturating_mul(MASK.len_utf8())
778                    } else {
779                        byte_of(&display, index)
780                    }
781                    .min(limit)
782                };
783                let start = byte(marked.start);
784                let end = byte(marked.end).max(start);
785                vec![
786                    TextRun {
787                        len: start,
788                        ..run.clone()
789                    },
790                    TextRun {
791                        len: end.saturating_sub(start),
792                        underline: Some(UnderlineStyle {
793                            color: Some(color),
794                            thickness: px(1.0),
795                            wavy: false,
796                        }),
797                        ..run.clone()
798                    },
799                    TextRun {
800                        len: display.len().saturating_sub(end),
801                        ..run
802                    },
803                ]
804                .into_iter()
805                .filter(|run| run.len > 0)
806                .collect()
807            }
808            None => vec![run],
809        };
810
811        // Cloning a `SharedString` is a refcount bump, so the shaped copy and
812        // the one the offsets are measured against are the same allocation.
813        let shaped = window
814            .text_system()
815            .shape_line(display.clone(), font_size, &runs, None);
816
817        // Scroll so the caret stays inside the field. Anchoring on the caret
818        // rather than the text means a long value slides under the border on
819        // whichever side the user is not looking at.
820        // Measured against the shaped string, which is the value itself when
821        // it isn't masked — so no second copy of it has to be kept alive.
822        let byte = |index: usize| {
823            if masked {
824                index.saturating_mul(MASK.len_utf8())
825            } else {
826                byte_of(&display, index)
827            }
828        };
829        let caret_x = if empty {
830            px(0.0)
831        } else {
832            shaped.x_for_index(byte(cursor))
833        };
834        let width = bounds.size.width;
835        let pad = px(SCROLL_PAD);
836        let mut scroll = self.field.read(cx).line().scroll;
837        // Never scroll past the end: shrinking the value should pull the text
838        // back rather than leave the field looking empty.
839        scroll = scroll.min((shaped.width - width + pad).max(px(0.0)));
840        if caret_x - scroll > width - pad {
841            scroll = caret_x - width + pad;
842        }
843        if caret_x - scroll < px(0.0) {
844            scroll = caret_x;
845        }
846        scroll = scroll.max(px(0.0));
847
848        let quads = if focused && !empty {
849            match selection {
850                Some((start, end)) => (
851                    None,
852                    Some(fill(
853                        Bounds::from_corners(
854                            point(
855                                bounds.left() + shaped.x_for_index(byte(start)) - scroll,
856                                bounds.top(),
857                            ),
858                            point(
859                                bounds.left() + shaped.x_for_index(byte(end)) - scroll,
860                                bounds.bottom(),
861                            ),
862                        ),
863                        selection_color,
864                    )),
865                ),
866                None => (caret_quad(bounds, caret_x - scroll, caret_color), None),
867            }
868        } else if focused {
869            (caret_quad(bounds, caret_x - scroll, caret_color), None)
870        } else {
871            (None, None)
872        };
873
874        self.field.update(cx, |field, cx| {
875            let state = field.line_mut();
876            state.scroll = scroll;
877            state.masked = masked;
878            state.empty = empty;
879            if state.focused != focused {
880                state.focused = focused;
881                state.caret_on = true;
882            }
883            // One blink task per focused field, started the first frame it
884            // holds focus and stopped by the task itself when it loses it.
885            if focused && !state.blinking {
886                state.blinking = true;
887                blink(cx);
888            }
889        });
890
891        LinePrepaint {
892            shaped: Some(shaped),
893            caret: quads.0.filter(|_| caret_on),
894            selection: quads.1,
895            scroll,
896        }
897    }
898
899    fn paint(
900        &mut self,
901        _id: Option<&GlobalElementId>,
902        _inspector: Option<&gpui::InspectorElementId>,
903        bounds: Bounds<Pixels>,
904        _layout: &mut (),
905        prepaint: &mut LinePrepaint,
906        window: &mut Window,
907        cx: &mut App,
908    ) {
909        let focus = self.field.read(cx).line_focus().clone();
910        window.handle_input(
911            &focus,
912            ElementInputHandler::new(bounds, self.field.clone()),
913            cx,
914        );
915
916        let shaped = prepaint.shaped.take().unwrap_or_default();
917        let origin = point(bounds.origin.x - prepaint.scroll, bounds.origin.y);
918        // Clip to the field: scrolled text would otherwise paint over the
919        // border and whatever sits beside it.
920        window.with_content_mask(Some(gpui::ContentMask { bounds }), |window| {
921            if let Some(selection) = prepaint.selection.take() {
922                window.paint_quad(selection);
923            }
924            shaped.paint(origin, window.line_height(), window, cx).ok();
925            if let Some(caret) = prepaint.caret.take() {
926                window.paint_quad(caret);
927            }
928        });
929
930        self.field.update(cx, |field, _| {
931            let state = field.line_mut();
932            state.shaped = Some(shaped);
933            state.bounds = Some(bounds);
934        });
935    }
936}
937
938fn caret_quad(bounds: Bounds<Pixels>, x: Pixels, color: Hsla) -> Option<PaintQuad> {
939    Some(fill(
940        Bounds::new(
941            point(bounds.left() + x, bounds.top()),
942            size(px(1.0), bounds.size.height),
943        ),
944        color,
945    ))
946}
947
948/// Byte offset of a char index into `text`.
949fn byte_of(text: &str, index: usize) -> usize {
950    text.char_indices()
951        .nth(index)
952        .map(|(byte, _)| byte)
953        .unwrap_or(text.len())
954}
955
956/// Toggle the caret while the field holds focus. The task ends itself the
957/// first tick after focus is lost, so nothing has to be cancelled.
958fn blink<V: LineEditor>(cx: &mut Context<V>) {
959    cx.spawn(async move |field, cx| loop {
960        cx.background_executor().timer(BLINK).await;
961        let running = field
962            .update(cx, |field, cx| {
963                let state = field.line_mut();
964                if !state.focused {
965                    state.blinking = false;
966                    state.caret_on = true;
967                    cx.notify();
968                    return false;
969                }
970                state.caret_on = !state.caret_on;
971                cx.notify();
972                true
973            })
974            .unwrap_or(false);
975        if !running {
976            break;
977        }
978    })
979    .detach();
980}