Skip to main content

ui/input/
element.rs

1//! Shaping and painting: runs, rows, selection and caret.
2
3use super::*;
4use crate::AppExt as _;
5
6/// What gets painted, and whether it is the placeholder — which is the only
7/// reason the colour differs.
8pub(super) fn display_text(field: &TextField) -> (SharedString, bool) {
9    if field.content.is_empty() {
10        (field.placeholder.clone(), true)
11    } else {
12        (field.content.clone(), false)
13    }
14}
15
16/// One run per span, the text between them in the field's own colour.
17///
18/// The runs a field paints [`TextField::set_spans`] as, public for the same
19/// reason [`next_boundary`] is: anything shaping the same text with the same
20/// palette wants the same answer, and this is where it is decided.
21///
22/// The spans are whatever the caller last handed over, which may describe text
23/// this frame no longer holds — see [`TextField::set_spans`]. A range that runs
24/// past the end, overlaps the one before it, or cuts a character in half is
25/// dropped rather than shifting the runs after it: shaping needs the lengths to
26/// add up to the text, and a colour that is wrong for one frame is cheaper than
27/// a panic.
28///
29/// `base.len` is ignored: every run is measured off `text`.
30pub fn coloured(
31    text: &str,
32    spans: &[(Range<usize>, HighlightKind)],
33    base: &TextRun,
34    palette: &SyntaxPalette,
35) -> Vec<TextRun> {
36    if spans.is_empty() {
37        return vec![TextRun {
38            len: text.len(),
39            ..base.clone()
40        }];
41    }
42    let mut runs = Vec::with_capacity(spans.len() * 2 + 1);
43    let mut at = 0;
44    for (range, kind) in spans {
45        if range.start < at
46            || range.end <= range.start
47            || range.end > text.len()
48            || !text.is_char_boundary(range.start)
49            || !text.is_char_boundary(range.end)
50        {
51            continue;
52        }
53        if range.start > at {
54            runs.push(TextRun {
55                len: range.start - at,
56                ..base.clone()
57            });
58        }
59        runs.push(TextRun {
60            len: range.end - range.start,
61            color: palette.color(*kind),
62            ..base.clone()
63        });
64        at = range.end;
65    }
66    if at < text.len() {
67        runs.push(TextRun {
68            len: text.len() - at,
69            ..base.clone()
70        });
71    }
72    runs
73}
74
75/// The IME composition range underlined, so the user can see what is still
76/// provisional. Each run is cut at the range's edges and the pieces inside it
77/// take the line, in whatever colour they already had.
78pub fn underlined(runs: Vec<TextRun>, marked: &Range<usize>) -> Vec<TextRun> {
79    let mut out = Vec::with_capacity(runs.len() + 2);
80    let mut at = 0;
81    for run in runs {
82        let end = at + run.len;
83        for (start, stop, mark) in [
84            (at, end.min(marked.start), false),
85            (at.max(marked.start), end.min(marked.end), true),
86            (at.max(marked.end), end, false),
87        ] {
88            if stop <= start {
89                continue;
90            }
91            out.push(TextRun {
92                len: stop - start,
93                underline: mark.then(|| UnderlineStyle {
94                    color: Some(run.color),
95                    thickness: px(1.0),
96                    wavy: false,
97                }),
98                ..run.clone()
99            });
100        }
101        at = end;
102    }
103    out
104}
105
106// ---------------------------------------------------------------------------
107// Line geometry. `shape_text` returns one `WrappedLine` per hard newline, each
108// wrapping into rows of its own; gpui resolves positions *within* a line, so
109// everything here is walking that list and nothing re-implements shaping.
110// ---------------------------------------------------------------------------
111
112/// Each shaped line with the byte offset it starts at. `shape_text` splits on
113/// `\n` and drops the separator, so each line starts one byte past the last.
114pub(super) fn lines_from(lines: &[WrappedLine]) -> impl Iterator<Item = (usize, &WrappedLine)> {
115    lines.iter().scan(0usize, |start, line| {
116        let at = *start;
117        *start = at + line.len() + 1;
118        Some((at, line))
119    })
120}
121
122/// Every visual row: the byte range it covers and its top edge, relative to the
123/// text origin. A wrap boundary resolves to a byte index exactly the way
124/// `WrappedLineLayout::position_for_index` does it internally — that mapping is
125/// not exposed, and selection needs one quad per row.
126pub(super) fn rows(lines: &[WrappedLine], line_height: Pixels) -> Vec<(Range<usize>, Pixels)> {
127    let mut out = Vec::new();
128    let mut top = px(0.);
129    for (start, line) in lines_from(lines) {
130        let mut row_start = start;
131        for boundary in line.wrap_boundaries() {
132            let at = start + line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index;
133            out.push((row_start..at, top));
134            row_start = at;
135            top += line_height;
136        }
137        out.push((row_start..start + line.len(), top));
138        top += line_height;
139    }
140    out
141}
142
143/// Byte offset → position relative to the text origin.
144pub(super) fn position_for_offset(
145    lines: &[WrappedLine],
146    offset: usize,
147    line_height: Pixels,
148) -> Option<Point<Pixels>> {
149    let mut top = px(0.);
150    for (start, line) in lines_from(lines) {
151        if offset <= start + line.len() {
152            let local = line.position_for_index(offset.saturating_sub(start), line_height)?;
153            return Some(gpui::point(local.x, local.y + top));
154        }
155        top += line.size(line_height).height;
156    }
157    None
158}
159
160/// Position relative to the text origin → the closest byte offset.
161pub(super) fn offset_for_position(
162    lines: &[WrappedLine],
163    position: Point<Pixels>,
164    line_height: Pixels,
165) -> usize {
166    let mut top = px(0.);
167    let mut last = 0;
168    for (start, line) in lines_from(lines) {
169        let height = line.size(line_height).height;
170        last = start + line.len();
171        if position.y < top + height {
172            let local = gpui::point(position.x, position.y - top);
173            let (Ok(index) | Err(index)) = line.closest_index_for_position(local, line_height);
174            return start + index;
175        }
176        top += height;
177    }
178    last
179}
180
181/// The char boundary at or before `offset`, clamped to the end of `text`.
182pub(super) fn floor_boundary(text: &str, offset: usize) -> usize {
183    let mut offset = offset.min(text.len());
184    while !text.is_char_boundary(offset) {
185        offset -= 1;
186    }
187    offset
188}
189
190/// The selection as one rect per visual row, relative to the text origin.
191///
192/// A row's left edge is always x=0, so a continuation row is taken from there
193/// rather than by looking the offset up — at a soft wrap the two rows share a
194/// byte offset, and the lookup resolves it to the end of the earlier row.
195pub(super) fn selection_rows(
196    lines: &[WrappedLine],
197    range: &Range<usize>,
198    line_height: Pixels,
199) -> Vec<Bounds<Pixels>> {
200    rows(lines, line_height)
201        .into_iter()
202        .filter(|(row, _)| range.start <= row.end && range.end >= row.start)
203        .filter_map(|(row, top)| {
204            let left = if range.start <= row.start {
205                px(0.)
206            } else {
207                position_for_offset(lines, range.start, line_height)?.x
208            };
209            let right = position_for_offset(lines, range.end.min(row.end), line_height)?.x;
210            (right > left).then(|| {
211                Bounds::from_corners(
212                    gpui::point(left, top),
213                    gpui::point(right, top + line_height),
214                )
215            })
216        })
217        .collect()
218}
219
220impl Render for TextField {
221    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
222        // The only place the blink starts: `caret_moved` drops the task, so the
223        // next render brings it back in phase, solid beat first.
224        if self.focus_handle.is_focused(_window) && cx.caret_blink() {
225            if self.blink.is_none() {
226                self.start_blink(cx);
227            }
228        } else {
229            self.blink = None;
230            self.caret_on = true;
231        }
232        let theme = Theme::of(cx);
233        let mut key_context = gpui::KeyContext::default();
234        key_context.add(KEY_CONTEXT);
235        if self.shape.is_multiline() {
236            key_context.add(MULTILINE_KEY_CONTEXT);
237        }
238        if let Some(extra) = self.key_context.clone() {
239            key_context.add(extra);
240        }
241        div()
242            .key_context(key_context)
243            .track_focus(&self.focus_handle(cx))
244            .cursor(CursorStyle::IBeam)
245            .on_action(cx.listener(Self::backspace))
246            .on_action(cx.listener(Self::delete))
247            .on_action(cx.listener(Self::left))
248            .on_action(cx.listener(Self::right))
249            .on_action(cx.listener(Self::select_left))
250            .on_action(cx.listener(Self::select_right))
251            .on_action(cx.listener(Self::select_all))
252            .on_action(cx.listener(Self::home))
253            .on_action(cx.listener(Self::end))
254            .on_action(cx.listener(Self::select_home))
255            .on_action(cx.listener(Self::select_end))
256            .on_action(cx.listener(Self::word_left))
257            .on_action(cx.listener(Self::word_right))
258            .on_action(cx.listener(Self::select_word_left))
259            .on_action(cx.listener(Self::select_word_right))
260            .on_action(cx.listener(Self::up))
261            .on_action(cx.listener(Self::down))
262            .on_action(cx.listener(Self::select_up))
263            .on_action(cx.listener(Self::select_down))
264            .on_action(cx.listener(Self::insert_newline))
265            .on_action(cx.listener(Self::undo))
266            .on_action(cx.listener(Self::redo))
267            .on_action(cx.listener(Self::delete_word_left))
268            .on_action(cx.listener(Self::delete_word_right))
269            .on_action(cx.listener(Self::delete_to_line_start))
270            .on_action(cx.listener(Self::delete_to_line_end))
271            .on_action(cx.listener(Self::show_character_palette))
272            .on_action(cx.listener(Self::paste))
273            .on_action(cx.listener(Self::cut))
274            .on_action(cx.listener(Self::copy))
275            .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
276            .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
277            .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
278            .on_scroll_wheel(cx.listener(Self::on_scroll_wheel))
279            .w_full()
280            .when(self.frame, |field| {
281                field
282                    .px(px(10.0))
283                    .py(px(7.0))
284                    .rounded(px(Theme::button_radius()))
285                    .bg(theme.input_bg)
286                    .border_1()
287                    .border_color(if self.focus_handle.is_focused(_window) {
288                        theme.ring
289                    } else {
290                        theme.border
291                    })
292            })
293            .text_size(px(self.metrics.size()))
294            .font_weight(self.metrics.weight)
295            .line_height(px(self.metrics.line_height()))
296            .text_color(theme.text)
297            .child(TextFieldElement { field: cx.entity() })
298    }
299}
300
301/// Paints the shaped lines plus selection and caret. A custom element because
302/// all three are geometry derived from the shaped text, which only exists after
303/// layout.
304pub(super) struct TextFieldElement {
305    field: Entity<TextField>,
306}
307
308pub(super) struct FieldPrepaint {
309    lines: Vec<WrappedLine>,
310    /// Top-left of the text, which is the box moved up by the scroll offset.
311    origin: Point<Pixels>,
312    cursor: Option<PaintQuad>,
313    /// One quad per visual row each match covers.
314    matches: Vec<PaintQuad>,
315    /// One quad per visual row the selection covers.
316    selection: Vec<PaintQuad>,
317}
318
319impl IntoElement for TextFieldElement {
320    type Element = Self;
321
322    fn into_element(self) -> Self::Element {
323        self
324    }
325}
326
327impl Element for TextFieldElement {
328    type RequestLayoutState = ();
329    type PrepaintState = FieldPrepaint;
330
331    fn id(&self) -> Option<ElementId> {
332        None
333    }
334
335    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
336        None
337    }
338
339    fn request_layout(
340        &mut self,
341        _id: Option<&GlobalElementId>,
342        _inspector_id: Option<&gpui::InspectorElementId>,
343        window: &mut Window,
344        cx: &mut App,
345    ) -> (LayoutId, ()) {
346        let mut style = Style::default();
347        style.size.width = relative(1.).into();
348        let field = self.field.read(cx);
349        // The field's own, here as everywhere: sizing the box off one height
350        // and hit-testing it at another is how a click lands on the wrong row.
351        let line_height = field.line_height();
352        let shape = field.shape;
353
354        let (min, max) = match shape {
355            Shape::Line => {
356                style.size.height = line_height.into();
357                return (window.request_layout(style, [], cx), ());
358            }
359            Shape::Rows(rows) => {
360                style.size.height = (line_height * rows.max(1) as f32).into();
361                return (window.request_layout(style, [], cx), ());
362            }
363            // Growing needs the row count, and the row count needs shaping at
364            // the width layout is still deciding — which is exactly what a
365            // measured layout is for.
366            Shape::Grow { min, max } => (min.max(1), max.max(min.max(1))),
367        };
368
369        let text = display_text(field).0;
370        let id = window.request_measured_layout(style, move |known, available, window, _cx| {
371            let text_style = window.text_style();
372            let font_size = text_style.font_size.to_pixels(window.rem_size());
373            // Prefer the width layout has already settled on. Taffy also probes
374            // with min/max-content, where there is no width to wrap against —
375            // and counting rows off unwrapped text under-reports them, which
376            // would size the box for fewer lines than it goes on to paint.
377            let wrap_width = known.width.or(match available.width {
378                gpui::AvailableSpace::Definite(width) => Some(width),
379                _ => None,
380            });
381            let run = TextRun {
382                len: text.len(),
383                font: text_style.font(),
384                color: text_style.color,
385                background_color: None,
386                underline: None,
387                strikethrough: None,
388            };
389            let count = window
390                .text_system()
391                .shape_text(text.clone(), font_size, &[run], wrap_width, None)
392                .map(|lines| {
393                    lines
394                        .iter()
395                        .map(|line| line.wrap_boundaries().len() + 1)
396                        .sum::<usize>()
397                })
398                .unwrap_or(1);
399            gpui::size(
400                wrap_width.unwrap_or(px(0.)),
401                line_height * count.clamp(min, max) as f32,
402            )
403        });
404        (id, ())
405    }
406
407    fn prepaint(
408        &mut self,
409        _id: Option<&GlobalElementId>,
410        _inspector_id: Option<&gpui::InspectorElementId>,
411        bounds: Bounds<Pixels>,
412        _request_layout: &mut Self::RequestLayoutState,
413        window: &mut Window,
414        cx: &mut App,
415    ) -> FieldPrepaint {
416        let theme = Theme::of(cx).clone();
417        let field = self.field.read(cx);
418        let selected_range = field.selected_range.clone();
419        let cursor = field.cursor_offset();
420        let shape = field.shape;
421        let marked_range = field.marked_range.clone();
422        let matches: Vec<_> = field
423            .matches
424            .iter()
425            .filter(|range| {
426                range.start <= range.end
427                    && field.content.is_char_boundary(range.start)
428                    && field.content.is_char_boundary(range.end)
429            })
430            .cloned()
431            .collect();
432        let scrolled = field.scroll;
433        let caret_shape = cx.caret_shape();
434        let caret_height = cx.caret_height();
435        let follow_caret = field.follow_caret || caret_shape != field.last_caret_shape;
436        let style = window.text_style();
437
438        let (text, is_placeholder) = display_text(field);
439        let text_color = if is_placeholder {
440            theme.text_faint
441        } else {
442            style.color
443        };
444
445        let run = TextRun {
446            len: text.len(),
447            font: style.font(),
448            color: text_color,
449            background_color: None,
450            underline: None,
451            strikethrough: None,
452        };
453        // Syntax first, then the IME composition range underlined over the top
454        // of it: both cut the text into runs, and the underline has to land on
455        // whatever colour is already there.
456        let runs = if is_placeholder {
457            vec![run]
458        } else {
459            coloured(&text, &field.spans, &run, &theme.syntax)
460        };
461        let runs = match marked_range.as_ref() {
462            Some(marked) => underlined(runs, marked),
463            None => runs,
464        };
465
466        let font_size = style.font_size.to_pixels(window.rem_size());
467        let line_height = field.line_height();
468        // A single line never wraps: it scrolls sideways instead, so shaping it
469        // against the field's width would fold it into rows nothing can reach.
470        let wrap_width = shape.is_multiline().then_some(bounds.size.width);
471        let lines = window
472            .text_system()
473            .shape_text(text.clone(), font_size, &runs, wrap_width, None)
474            .map(|lines| lines.into_vec())
475            .unwrap_or_default();
476
477        // Clamp every frame, not just when scrolling: the content this is
478        // measured against shrinks under it — delete the last line while parked
479        // at the bottom and an unclamped offset leaves the box showing nothing.
480        //
481        // A wide caret needs trailing room even at the end of the longest row.
482        // Keep the bar's existing scroll extent unchanged.
483        let content_height: Pixels = lines.iter().map(|l| l.size(line_height).height).sum();
484        let content_width = lines.iter().map(|l| l.width()).fold(px(0.), Pixels::max);
485        // The slot a wide caret takes where no character follows it — the end
486        // of the text, a soft-wrap boundary: the font's `ch` width.
487        let slot = (caret_shape != CaretShape::Bar)
488            .then(|| caret::zero_width(style.font(), font_size, window));
489        // A placeholder is not text: over it the caret takes the empty slot.
490        let follows = (!is_placeholder)
491            .then(|| {
492                lines_from(&lines)
493                    .find_map(|(start, line)| {
494                        (cursor <= start + line.len()).then(|| {
495                            caret::character_advance(line, &line.text, cursor.saturating_sub(start))
496                        })
497                    })
498                    .flatten()
499            })
500            .flatten();
501        let advance = slot.map(|slot| follows.unwrap_or(slot));
502        let hollow = !window.is_window_active();
503        let lines = if caret_shape.cuts_out(hollow)
504            && follows.is_some()
505            && selected_range.is_empty()
506            && field.caret_on
507            && field.focus_handle.is_focused(window)
508        {
509            let glyph = cursor..next_boundary(&text, cursor);
510            window
511                .text_system()
512                .shape_text(
513                    text.clone(),
514                    font_size,
515                    &caret::recoloured(runs, &glyph, theme.bg),
516                    wrap_width,
517                    None,
518                )
519                .map(|lines| lines.into_vec())
520                .unwrap_or(lines)
521        } else {
522            lines
523        };
524        let caret_width = caret_shape
525            .quad(
526                Bounds::new(
527                    gpui::point(px(0.), px(0.)),
528                    gpui::size(CARET_WIDTH, font_size),
529                ),
530                line_height,
531                caret_height,
532                advance.unwrap_or_default(),
533                theme.caret,
534                hollow,
535            )
536            .bounds
537            .size
538            .width;
539        // A field that wraps never scrolls sideways.
540        let max = gpui::point(
541            match shape.is_multiline() {
542                true => px(0.),
543                false => (content_width + slot.unwrap_or_default() - bounds.size.width).max(px(0.)),
544            },
545            (content_height - bounds.size.height).max(px(0.)),
546        );
547        let mut scroll = gpui::point(
548            scrolled.x.clamp(px(0.), max.x),
549            scrolled.y.clamp(px(0.), max.y),
550        );
551        if follow_caret && let Some(at) = position_for_offset(&lines, cursor, line_height) {
552            if at.y < scroll.y {
553                scroll.y = at.y;
554            } else if at.y + line_height > scroll.y + bounds.size.height {
555                scroll.y = at.y + line_height - bounds.size.height;
556            }
557            // The caret is the thing being kept in view, so it is its own width
558            // that has to clear the right edge — not the character before it.
559            if at.x < scroll.x {
560                scroll.x = at.x;
561            } else if at.x + caret_width > scroll.x + bounds.size.width {
562                scroll.x = at.x + caret_width - bounds.size.width;
563            }
564            scroll.x = scroll.x.clamp(px(0.), max.x);
565            scroll.y = scroll.y.clamp(px(0.), max.y);
566        }
567        self.field.update(cx, |field, _| {
568            field.scroll = scroll;
569            field.follow_caret = false;
570            field.last_caret_shape = caret_shape;
571        });
572        let origin = bounds.origin - scroll;
573
574        let matches = if is_placeholder {
575            Vec::new()
576        } else {
577            matches
578                .iter()
579                .flat_map(|range| selection_rows(&lines, range, line_height))
580                .map(|rect| {
581                    fill(
582                        Bounds::new(origin + rect.origin, rect.size),
583                        theme.warning.opacity(0.25),
584                    )
585                })
586                .collect()
587        };
588
589        let (selection, cursor) = if selected_range.is_empty() {
590            let at = position_for_offset(&lines, cursor, line_height).unwrap_or_default();
591            (
592                Vec::new(),
593                Some(caret_shape.quad(
594                    // The font's size rather than the line's: leading is not
595                    // a bar's or an underline's to fill. A block fills it.
596                    Bounds::new(
597                        origin + at + gpui::point(px(0.), (line_height - font_size) / 2.),
598                        gpui::size(CARET_WIDTH, font_size),
599                    ),
600                    line_height,
601                    caret_height,
602                    advance.unwrap_or_default(),
603                    theme.caret,
604                    hollow,
605                )),
606            )
607        } else {
608            (
609                selection_rows(&lines, &selected_range, line_height)
610                    .into_iter()
611                    .map(|rect| {
612                        fill(
613                            Bounds::new(origin + rect.origin, rect.size),
614                            theme.selection,
615                        )
616                    })
617                    .collect(),
618                None,
619            )
620        };
621
622        FieldPrepaint {
623            lines,
624            origin,
625            cursor,
626            matches,
627            selection,
628        }
629    }
630
631    fn paint(
632        &mut self,
633        _id: Option<&GlobalElementId>,
634        _inspector_id: Option<&gpui::InspectorElementId>,
635        bounds: Bounds<Pixels>,
636        _request_layout: &mut Self::RequestLayoutState,
637        prepaint: &mut Self::PrepaintState,
638        window: &mut Window,
639        cx: &mut App,
640    ) {
641        let focus_handle = self.field.read(cx).focus_handle.clone();
642        let caret_on = self.field.read(cx).caret_on;
643        window.handle_input(
644            &focus_handle,
645            ElementInputHandler::new(bounds, self.field.clone()),
646            cx,
647        );
648        let line_height = self.field.read(cx).line_height();
649        // A drag that leaves the box is still a drag. `on_mouse_move` on the
650        // field's own div fires only while the box is the thing under the
651        // pointer, so a run dragged past the edge froze at the last character
652        // inside it — and the last line of a full box was unreachable, since
653        // reaching it means passing the edge. This is the window's own move,
654        // which arrives wherever the pointer went.
655        //
656        // Registered every frame because that is the contract: the listener is
657        // cleared with the frame that installed it.
658        let dragged = self.field.clone();
659        window.on_mouse_event(move |event: &MouseMoveEvent, phase, _window, cx| {
660            // The button being down is what makes this a drag. `selecting`
661            // is only cleared on the release, so a press whose release went
662            // somewhere we never heard about would otherwise leave a plain
663            // hover dragging the run around.
664            if phase != DispatchPhase::Bubble || !event.dragging() {
665                return;
666            }
667            dragged.update(cx, |field, cx| {
668                field.drag_to(event.position, line_height, cx);
669            });
670        });
671        let lines = std::mem::take(&mut prepaint.lines);
672        let matches = std::mem::take(&mut prepaint.matches);
673        let selection = std::mem::take(&mut prepaint.selection);
674        let cursor = prepaint.cursor.take();
675        let origin = prepaint.origin;
676
677        // Scrolled text runs past the box in both directions, so everything the
678        // field draws is masked to it — text, selection and caret alike.
679        window.with_content_mask(Some(gpui::ContentMask::new(bounds)), |window| {
680            for quad in matches.into_iter().chain(selection) {
681                window.paint_quad(quad);
682            }
683
684            // The caret only exists while focused — an unfocused field showing
685            // one reads as two cursors on screen. Under the text, so a glyph
686            // cut out of a block lands on top of it.
687            if focus_handle.is_focused(window)
688                && caret_on
689                && let Some(cursor) = cursor
690            {
691                window.paint_quad(cursor);
692            }
693
694            let mut top = origin;
695            for line in &lines {
696                line.paint(top, line_height, gpui::TextAlign::Left, None, window, cx)
697                    .ok();
698                top.y += line.size(line_height).height;
699            }
700        });
701
702        self.field.update(cx, |field, _| {
703            field.last_layout = lines;
704            field.last_bounds = Some(bounds);
705        });
706    }
707}