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, UnderlineStyle,
36  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 this module's `Line` element.
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 the crate's `line_input_handler!` macro.
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(&mut self, _window: &mut ::gpui::Window, _cx: &mut ::gpui::Context<Self>) {
212        self.line_mut().marked = None;
213      }
214
215      fn replace_text_in_range(
216        &mut self,
217        range_utf16: Option<::std::ops::Range<usize>>,
218        text: &str,
219        _window: &mut ::gpui::Window,
220        cx: &mut ::gpui::Context<Self>,
221      ) {
222        $crate::input::line::replace(self, range_utf16, text, None, cx);
223      }
224
225      fn replace_and_mark_text_in_range(
226        &mut self,
227        range_utf16: Option<::std::ops::Range<usize>>,
228        text: &str,
229        selected_utf16: Option<::std::ops::Range<usize>>,
230        _window: &mut ::gpui::Window,
231        cx: &mut ::gpui::Context<Self>,
232      ) {
233        $crate::input::line::replace(self, range_utf16, text, Some(selected_utf16), cx);
234      }
235
236      fn bounds_for_range(
237        &mut self,
238        range_utf16: ::std::ops::Range<usize>,
239        bounds: ::gpui::Bounds<::gpui::Pixels>,
240        _window: &mut ::gpui::Window,
241        _cx: &mut ::gpui::Context<Self>,
242      ) -> Option<::gpui::Bounds<::gpui::Pixels>> {
243        $crate::input::line::range_bounds(self, range_utf16, bounds)
244      }
245
246      fn character_index_for_point(
247        &mut self,
248        point: ::gpui::Point<::gpui::Pixels>,
249        _window: &mut ::gpui::Window,
250        _cx: &mut ::gpui::Context<Self>,
251      ) -> Option<usize> {
252        let index = self.line().index_at(self.edit(), point)?;
253        Some($crate::input::line::to_utf16(self.edit(), &(0..index)).end)
254      }
255    }
256  };
257}
258
259pub(crate) use line_input_handler;
260
261// --- UTF-16 translation -----------------------------------------------------
262//
263// The platform addresses text in UTF-16 code units; the model counts chars.
264// These four helpers are the only place that conversion happens.
265
266pub(crate) fn to_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
267  // Over `chars()` rather than `text()`: the platform calls these several
268  // times per keystroke, and `text()` builds a fresh `String` each time.
269  let buffer = edit.chars();
270  let units = |chars: usize| {
271    buffer[..chars.min(buffer.len())]
272      .iter()
273      .map(|c| c.len_utf16())
274      .sum::<usize>()
275  };
276  units(range.start)..units(range.end)
277}
278
279pub(crate) fn from_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
280  let buffer = edit.chars();
281  let chars = |units: usize| {
282    let mut seen = 0;
283    for (index, c) in buffer.iter().enumerate() {
284      if seen >= units {
285        return index;
286      }
287      seen += c.len_utf16();
288    }
289    buffer.len()
290  };
291  chars(range.start)..chars(range.end)
292}
293
294pub(crate) fn slice(edit: &TextEdit, range: &Range<usize>) -> String {
295  let buffer = edit.chars();
296  let start = range.start.min(buffer.len());
297  let end = range.end.clamp(start, buffer.len());
298  buffer[start..end].iter().collect()
299}
300
301pub(crate) fn utf16_selection(edit: &TextEdit) -> gpui::UTF16Selection {
302  let (start, end) = edit.selection().unwrap_or((edit.cursor(), edit.cursor()));
303  let reversed = edit.cursor() == start && start != end;
304  gpui::UTF16Selection {
305    range: to_utf16(edit, &(start..end)),
306    reversed,
307  }
308}
309
310/// A single-line field never holds a line break, exactly like `<input>`, which
311/// flattens them out of anything pasted or dropped into it. Other control
312/// characters would render as nothing useful, so they go too — and this is
313/// also what makes it impossible for a stray `\t` to be typed.
314fn flatten(text: &str) -> String {
315  text
316    .chars()
317    .map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
318    .filter(|c| !c.is_control())
319    .collect()
320}
321
322/// The shared body of `replace_text_in_range` and its marked-text sibling.
323/// `marking` is `Some(selection)` when the IME is mid-composition.
324pub(crate) fn replace<V: LineEditor>(
325  this: &mut V,
326  range_utf16: Option<Range<usize>>,
327  text: &str,
328  marking: Option<Option<Range<usize>>>,
329  cx: &mut Context<V>,
330) {
331  if this.line_read_only() {
332    return;
333  }
334  let text = this.line_filter(flatten(text));
335
336  let range = range_utf16
337    .map(|r| from_utf16(this.edit(), &r))
338    .or_else(|| this.line().marked.clone())
339    .unwrap_or_else(|| {
340      this
341        .edit()
342        .selection()
343        .map(|(s, e)| s..e)
344        .unwrap_or_else(|| this.edit().cursor()..this.edit().cursor())
345    });
346
347  // `maxlength` counts what would remain, so a replacement that swaps a
348  // selection for something the same size always fits.
349  let text = match this.line_max_length() {
350    Some(max) => {
351      let kept = this.edit().len() - range.len().min(this.edit().len());
352      text.chars().take(max.saturating_sub(kept)).collect()
353    }
354    None => text,
355  };
356
357  let start = range.start;
358  this.edit_mut().replace_range(range, &text);
359  match marking {
360    Some(selection) => {
361      let end = start + text.chars().count();
362      this.line_mut().marked = (!text.is_empty()).then_some(start..end);
363      if let Some(selection) = selection {
364        let selection = from_utf16(this.edit(), &selection);
365        this
366          .edit_mut()
367          .set_selection(start + selection.start, start + selection.end);
368      }
369    }
370    None => this.line_mut().marked = None,
371  }
372  this.line_mut().wake();
373  this.line_changed(cx);
374}
375
376/// Where a char range sits on screen, for the IME's candidate window.
377pub(crate) fn range_bounds<V: LineEditor>(
378  this: &V,
379  range_utf16: Range<usize>,
380  bounds: Bounds<Pixels>,
381) -> Option<Bounds<Pixels>> {
382  let shaped = this.line().shaped.as_ref()?;
383  let range = from_utf16(this.edit(), &range_utf16);
384  let x = |index: usize| {
385    bounds.left() + shaped.x_for_index(this.line().shaped_byte(this.edit(), index))
386      - this.line().scroll
387  };
388  Some(Bounds::from_corners(
389    point(x(range.start), bounds.top()),
390    point(x(range.end), bounds.bottom()),
391  ))
392}
393
394// --- mouse ------------------------------------------------------------------
395
396/// Focus the field and place (or extend) the selection where the user clicked.
397/// Click counts follow the platform convention a browser also uses: one places
398/// the caret, two takes the word, three takes the whole value.
399pub(crate) fn mouse_down<V: LineEditor>(
400  this: &mut V,
401  event: &MouseDownEvent,
402  window: &mut Window,
403  cx: &mut Context<V>,
404) {
405  window.focus(this.line_focus());
406  let Some(index) = this.line().index_at(this.edit(), event.position) else {
407    cx.notify();
408    return;
409  };
410  match event.click_count {
411    1 if event.modifiers.shift => this.edit_mut().extend_to(index),
412    1 => this.edit_mut().set_cursor(index),
413    2 => {
414      let (start, end) = this.edit().word_at(index);
415      this.edit_mut().set_selection(start, end);
416    }
417    _ => this.edit_mut().select_all(),
418  }
419  this.line_mut().selecting = true;
420  this.line_mut().wake();
421  cx.notify();
422}
423
424/// Extend the selection while the button is held.
425pub(crate) fn mouse_move<V: LineEditor>(
426  this: &mut V,
427  event: &MouseMoveEvent,
428  _window: &mut Window,
429  cx: &mut Context<V>,
430) {
431  if !this.line().selecting {
432    return;
433  }
434  if let Some(index) = this.line().index_at(this.edit(), event.position) {
435    this.edit_mut().extend_to(index);
436    cx.notify();
437  }
438}
439
440pub(crate) fn mouse_up<V: LineEditor>(
441  this: &mut V,
442  _event: &MouseUpEvent,
443  _window: &mut Window,
444  cx: &mut Context<V>,
445) {
446  if this.line().selecting {
447    this.line_mut().selecting = false;
448    cx.notify();
449  }
450}
451
452/// Attach the mouse handling every field shares, and mark it a text surface.
453///
454/// The four handlers have to travel together — a selection drag that only
455/// registers `on_mouse_up` and not `on_mouse_up_out` never ends when the
456/// pointer leaves the field — so they are applied in one place rather than
457/// copied into each field's `render`. Fields that need more (a picker that
458/// also opens its list) add their own handler after; gpui runs both.
459pub(crate) fn wire<V: LineEditor>(
460  element: gpui::Stateful<gpui::Div>,
461  focus: &FocusHandle,
462  cx: &mut Context<V>,
463) -> gpui::Stateful<gpui::Div> {
464  element
465    .track_focus(focus)
466    .cursor(gpui::CursorStyle::IBeam)
467    .on_mouse_down(gpui::MouseButton::Left, cx.listener(mouse_down))
468    .on_mouse_move(cx.listener(mouse_move))
469    .on_mouse_up(gpui::MouseButton::Left, cx.listener(mouse_up))
470    .on_mouse_up_out(gpui::MouseButton::Left, cx.listener(mouse_up))
471}
472
473/// Give a field the Tab-order and focus accessors every form control needs.
474///
475/// These were copy-pasted into three fields and simply absent from the other
476/// four, which left half the inputs in the Tab ring with no way for a host to
477/// order them or focus them on open.
478macro_rules! line_focus_builders {
479  ($ty:ty) => {
480    impl $ty {
481      /// Where this field sits in the window's Tab order. Fields default
482      /// to 0, which walks them in render order; set this only to
483      /// override that.
484      pub fn tab_index(mut self, index: isize) -> Self {
485        self.focus = self.focus.clone().tab_index(index);
486        self
487      }
488
489      /// Leave the field out of the Tab order without disabling it.
490      pub fn tab_stop(mut self, tab_stop: bool) -> Self {
491        self.focus = self.focus.clone().tab_stop(tab_stop);
492        self
493      }
494
495      /// The field's focus handle, so a host can focus it on open.
496      pub fn focus_handle(&self) -> ::gpui::FocusHandle {
497        self.focus.clone()
498      }
499    }
500  };
501}
502
503pub(crate) use line_focus_builders;
504
505// --- keyboard ---------------------------------------------------------------
506
507/// The keys a single-line field handles itself: the clipboard, undo, focus
508/// movement, then navigation and deletion via
509/// [`apply_nav`](super::apply_nav).
510///
511/// Printable input is deliberately absent. Returning [`KeyOutcome::Pass`] for
512/// it lets the platform hand the key to the input handler instead, which is
513/// what makes dead keys, IME composition, and press-and-hold work.
514pub(crate) fn keys<V: LineEditor>(
515  this: &mut V,
516  event: &KeyDownEvent,
517  window: &mut Window,
518  cx: &mut Context<V>,
519) -> KeyOutcome {
520  let ks = &event.keystroke;
521  let m = &ks.modifiers;
522
523  // Tab is focus movement, never a character. Without this the platform
524  // reports a `\t` for it and the field would type one, which is the one
525  // behaviour every HTML form gets right for free.
526  if ks.key == "tab" && !m.platform && !m.control {
527    if m.shift {
528      window.focus_prev();
529    } else {
530      window.focus_next();
531    }
532    cx.stop_propagation();
533    return KeyOutcome::Edited;
534  }
535
536  if m.platform && !m.alt && !m.control {
537    match ks.key.as_str() {
538      "c" => return copy(this, cx),
539      "x" => return cut(this, cx),
540      "v" => return paste(this, cx),
541      "z" if m.shift => return history(this, false, cx),
542      "z" => return history(this, true, cx),
543      "y" => return history(this, false, cx),
544      _ => {}
545    }
546  }
547
548  if this.line_read_only() && mutates(ks.key.as_str()) {
549    return KeyOutcome::Pass;
550  }
551
552  let outcome = apply_nav(this.edit_mut(), ks);
553  if outcome == KeyOutcome::Edited {
554    this.line_mut().wake();
555  }
556  outcome
557}
558
559/// Whether a key would change the text, for the read-only check.
560fn mutates(key: &str) -> bool {
561  matches!(key, "backspace" | "delete" | "k")
562}
563
564fn copy<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
565  if !this.line_masked() {
566    if let Some(text) = this.edit().selected_text() {
567      cx.write_to_clipboard(ClipboardItem::new_string(text));
568    }
569  }
570  cx.stop_propagation();
571  KeyOutcome::Pass
572}
573
574fn cut<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
575  if this.line_masked() || this.line_read_only() {
576    cx.stop_propagation();
577    return KeyOutcome::Pass;
578  }
579  let Some(text) = this.edit().selected_text() else {
580    cx.stop_propagation();
581    return KeyOutcome::Pass;
582  };
583  cx.write_to_clipboard(ClipboardItem::new_string(text));
584  this.edit_mut().delete_selection();
585  this.line_mut().wake();
586  cx.stop_propagation();
587  KeyOutcome::Edited
588}
589
590fn paste<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
591  if this.line_read_only() {
592    cx.stop_propagation();
593    return KeyOutcome::Pass;
594  }
595  let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
596    cx.stop_propagation();
597    return KeyOutcome::Pass;
598  };
599  let text = this.line_filter(flatten(&text));
600  let text = match this.line_max_length() {
601    Some(max) => {
602      let selected = this.edit().selection().map_or(0, |(s, e)| e - s);
603      let room = max.saturating_sub(this.edit().len() - selected);
604      text.chars().take(room).collect()
605    }
606    None => text,
607  };
608  this.edit_mut().break_undo();
609  this.edit_mut().insert(&text);
610  this.edit_mut().break_undo();
611  this.line_mut().wake();
612  cx.stop_propagation();
613  KeyOutcome::Edited
614}
615
616fn history<V: LineEditor>(this: &mut V, undo: bool, cx: &mut Context<V>) -> KeyOutcome {
617  if this.line_read_only() {
618    cx.stop_propagation();
619    return KeyOutcome::Pass;
620  }
621  let changed = if undo {
622    this.edit_mut().undo()
623  } else {
624    this.edit_mut().redo()
625  };
626  this.line_mut().wake();
627  cx.stop_propagation();
628  if changed {
629    KeyOutcome::Edited
630  } else {
631    KeyOutcome::Pass
632  }
633}
634
635// --- the element ------------------------------------------------------------
636
637/// The text, caret, and selection of a single-line field.
638///
639/// Give it the entity that owns the buffer and the colors already resolved
640/// from the theme; it handles the rest. Put it inside the field's chrome —
641/// it fills the width it is given and is one line tall.
642pub struct Line<V: LineEditor> {
643  field: Entity<V>,
644  placeholder: SharedString,
645  /// Only the placeholder's color varies between fields — a picker that has
646  /// a value shows it in the text color rather than dimmed. The rest is the
647  /// theme's, resolved at paint.
648  placeholder_color: Option<Hsla>,
649}
650
651impl<V: LineEditor> Line<V> {
652  pub fn new(field: Entity<V>) -> Self {
653    Line {
654      field,
655      placeholder: SharedString::default(),
656      placeholder_color: None,
657    }
658  }
659
660  /// Text to show while the field is empty, and the color to show it in.
661  pub fn placeholder(mut self, placeholder: impl Into<SharedString>, color: Hsla) -> Self {
662    self.placeholder = placeholder.into();
663    self.placeholder_color = Some(color);
664    self
665  }
666}
667
668/// What `prepaint` worked out for `paint` to draw.
669pub struct LinePrepaint {
670  shaped: Option<ShapedLine>,
671  caret: Option<PaintQuad>,
672  selection: Option<PaintQuad>,
673  scroll: Pixels,
674}
675
676impl<V: LineEditor> IntoElement for Line<V> {
677  type Element = Self;
678
679  fn into_element(self) -> Self::Element {
680    self
681  }
682}
683
684impl<V: LineEditor> Element for Line<V> {
685  type RequestLayoutState = ();
686  type PrepaintState = LinePrepaint;
687
688  fn id(&self) -> Option<gpui::ElementId> {
689    None
690  }
691
692  fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
693    None
694  }
695
696  fn request_layout(
697    &mut self,
698    _id: Option<&GlobalElementId>,
699    _inspector: Option<&gpui::InspectorElementId>,
700    window: &mut Window,
701    cx: &mut App,
702  ) -> (LayoutId, ()) {
703    let mut style = Style::default();
704    style.size.width = gpui::relative(1.0).into();
705    style.size.height = window.line_height().into();
706    (window.request_layout(style, [], cx), ())
707  }
708
709  fn prepaint(
710    &mut self,
711    _id: Option<&GlobalElementId>,
712    _inspector: Option<&gpui::InspectorElementId>,
713    bounds: Bounds<Pixels>,
714    _layout: &mut (),
715    window: &mut Window,
716    cx: &mut App,
717  ) -> LinePrepaint {
718    // The field's visuals are the theme's, so they are read here rather
719    // than threaded through `Line::new` by seven callers that all passed
720    // the same three values.
721    let t = theme(cx);
722    let text_color = t.text().hsla();
723    let caret_color = t.primary().hsla();
724    let selection_color = t.selection();
725    let dimmed = t.dimmed().hsla();
726
727    let focused = self.field.read(cx).line_focus().is_focused(window);
728    let field = self.field.read(cx);
729    let masked = field.line_masked();
730    let empty = field.edit().is_empty();
731    let cursor = field.edit().cursor();
732    let selection = field.edit().selection();
733    let marked = field.line().marked.clone();
734    let chars = field.edit().len();
735    let caret_on = field.line().caret_on;
736
737    // Built per branch rather than up front: a masked field would otherwise
738    // allocate and copy the whole cleartext buffer every frame only to
739    // throw it away, and an empty one would build an empty `String`.
740    let display: SharedString = if empty {
741      self.placeholder.clone()
742    } else if masked {
743      SharedString::from(MASK_STR.repeat(chars))
744    } else {
745      SharedString::from(field.edit().text())
746    };
747
748    let style = window.text_style();
749    let font_size = style.font_size.to_pixels(window.rem_size());
750    let color = if empty {
751      self.placeholder_color.unwrap_or(dimmed)
752    } else {
753      text_color
754    };
755    let run = TextRun {
756      len: display.len(),
757      font: style.font(),
758      color,
759      background_color: None,
760      underline: None,
761      strikethrough: None,
762    };
763
764    // The IME underlines what it is still composing, so the user can see
765    // which characters are provisional.
766    let runs = match marked.filter(|_| !empty) {
767      Some(marked) => {
768        // Runs have to cover the shaped string exactly — the text
769        // system sums their lengths and slices the string by the
770        // total, so an over-long run is a panic, not a mis-draw. The
771        // marked range is the IME's view of the text and can outlive
772        // an edit that shortened it, so clamp rather than trust it.
773        let limit = display.len();
774        let byte = |index: usize| {
775          if masked {
776            index.saturating_mul(MASK.len_utf8())
777          } else {
778            byte_of(&display, index)
779          }
780          .min(limit)
781        };
782        let start = byte(marked.start);
783        let end = byte(marked.end).max(start);
784        vec![
785          TextRun {
786            len: start,
787            ..run.clone()
788          },
789          TextRun {
790            len: end.saturating_sub(start),
791            underline: Some(UnderlineStyle {
792              color: Some(color),
793              thickness: px(1.0),
794              wavy: false,
795            }),
796            ..run.clone()
797          },
798          TextRun {
799            len: display.len().saturating_sub(end),
800            ..run
801          },
802        ]
803        .into_iter()
804        .filter(|run| run.len > 0)
805        .collect()
806      }
807      None => vec![run],
808    };
809
810    // Cloning a `SharedString` is a refcount bump, so the shaped copy and
811    // the one the offsets are measured against are the same allocation.
812    let shaped = window
813      .text_system()
814      .shape_line(display.clone(), font_size, &runs, None);
815
816    // Scroll so the caret stays inside the field. Anchoring on the caret
817    // rather than the text means a long value slides under the border on
818    // whichever side the user is not looking at.
819    // Measured against the shaped string, which is the value itself when
820    // it isn't masked — so no second copy of it has to be kept alive.
821    let byte = |index: usize| {
822      if masked {
823        index.saturating_mul(MASK.len_utf8())
824      } else {
825        byte_of(&display, index)
826      }
827    };
828    let caret_x = if empty {
829      px(0.0)
830    } else {
831      shaped.x_for_index(byte(cursor))
832    };
833    let width = bounds.size.width;
834    let pad = px(SCROLL_PAD);
835    let mut scroll = self.field.read(cx).line().scroll;
836    // Never scroll past the end: shrinking the value should pull the text
837    // back rather than leave the field looking empty.
838    scroll = scroll.min((shaped.width - width + pad).max(px(0.0)));
839    if caret_x - scroll > width - pad {
840      scroll = caret_x - width + pad;
841    }
842    if caret_x - scroll < px(0.0) {
843      scroll = caret_x;
844    }
845    scroll = scroll.max(px(0.0));
846
847    let quads = if focused && !empty {
848      match selection {
849        Some((start, end)) => (
850          None,
851          Some(fill(
852            Bounds::from_corners(
853              point(
854                bounds.left() + shaped.x_for_index(byte(start)) - scroll,
855                bounds.top(),
856              ),
857              point(
858                bounds.left() + shaped.x_for_index(byte(end)) - scroll,
859                bounds.bottom(),
860              ),
861            ),
862            selection_color,
863          )),
864        ),
865        None => (caret_quad(bounds, caret_x - scroll, caret_color), None),
866      }
867    } else if focused {
868      (caret_quad(bounds, caret_x - scroll, caret_color), None)
869    } else {
870      (None, None)
871    };
872
873    self.field.update(cx, |field, cx| {
874      let state = field.line_mut();
875      state.scroll = scroll;
876      state.masked = masked;
877      state.empty = empty;
878      if state.focused != focused {
879        state.focused = focused;
880        state.caret_on = true;
881      }
882      // One blink task per focused field, started the first frame it
883      // holds focus and stopped by the task itself when it loses it.
884      if focused && !state.blinking {
885        state.blinking = true;
886        blink(cx);
887      }
888    });
889
890    LinePrepaint {
891      shaped: Some(shaped),
892      caret: quads.0.filter(|_| caret_on),
893      selection: quads.1,
894      scroll,
895    }
896  }
897
898  fn paint(
899    &mut self,
900    _id: Option<&GlobalElementId>,
901    _inspector: Option<&gpui::InspectorElementId>,
902    bounds: Bounds<Pixels>,
903    _layout: &mut (),
904    prepaint: &mut LinePrepaint,
905    window: &mut Window,
906    cx: &mut App,
907  ) {
908    let focus = self.field.read(cx).line_focus().clone();
909    window.handle_input(
910      &focus,
911      ElementInputHandler::new(bounds, self.field.clone()),
912      cx,
913    );
914
915    let shaped = prepaint.shaped.take().unwrap_or_default();
916    let origin = point(bounds.origin.x - prepaint.scroll, bounds.origin.y);
917    // Keep the horizontal viewport tight without using the line box as a
918    // vertical mask. Fallback glyphs, accents, and emoji can paint outside
919    // that box even when their advance metrics fit it; the surrounding
920    // control (or window) already supplies the correct vertical clip.
921    let parent_mask = window.content_mask().bounds;
922    let mask = Bounds::from_corners(
923      point(bounds.left(), parent_mask.top()),
924      point(bounds.right(), parent_mask.bottom()),
925    );
926    window.with_content_mask(Some(gpui::ContentMask { bounds: mask }), |window| {
927      if let Some(selection) = prepaint.selection.take() {
928        window.paint_quad(selection);
929      }
930      shaped.paint(origin, window.line_height(), window, cx).ok();
931      if let Some(caret) = prepaint.caret.take() {
932        window.paint_quad(caret);
933      }
934    });
935
936    self.field.update(cx, |field, _| {
937      let state = field.line_mut();
938      state.shaped = Some(shaped);
939      state.bounds = Some(bounds);
940    });
941  }
942}
943
944fn caret_quad(bounds: Bounds<Pixels>, x: Pixels, color: Hsla) -> Option<PaintQuad> {
945  Some(fill(
946    Bounds::new(
947      point(bounds.left() + x, bounds.top()),
948      size(px(1.0), bounds.size.height),
949    ),
950    color,
951  ))
952}
953
954/// Byte offset of a char index into `text`.
955fn byte_of(text: &str, index: usize) -> usize {
956  text
957    .char_indices()
958    .nth(index)
959    .map(|(byte, _)| byte)
960    .unwrap_or(text.len())
961}
962
963/// Toggle the caret while the field holds focus. The task ends itself the
964/// first tick after focus is lost, so nothing has to be cancelled.
965fn blink<V: LineEditor>(cx: &mut Context<V>) {
966  cx.spawn(async move |field, cx| loop {
967    cx.background_executor().timer(BLINK).await;
968    let running = field
969      .update(cx, |field, cx| {
970        let state = field.line_mut();
971        if !state.focused {
972          state.blinking = false;
973          state.caret_on = true;
974          cx.notify();
975          return false;
976        }
977        state.caret_on = !state.caret_on;
978        cx.notify();
979        true
980      })
981      .unwrap_or(false);
982    if !running {
983      break;
984    }
985  })
986  .detach();
987}