Skip to main content

ui/input/
element.rs

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