Skip to main content

ui/input/
caret.rs

1//! Shared caret geometry for text fields and document renderers.
2
3use std::ops::Range;
4
5use gpui::{
6    App, BorderStyle, Bounds, Font, Global, Hsla, LineLayout, PaintQuad, Pixels, Point, TextRun,
7    Window, WrappedLineLayout, fill, outline, point, px,
8};
9
10/// The app-wide caret shape. Terminal cursors follow their terminal instead.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum CaretShape {
13    #[default]
14    Bar,
15    Block,
16    Underline,
17}
18
19impl Global for CaretShape {}
20
21pub(crate) fn caret_shape(cx: &App) -> CaretShape {
22    cx.try_global::<CaretShape>().copied().unwrap_or_default()
23}
24
25/// Changes the shape and repaints open windows.
26pub(crate) fn set_caret_shape(shape: CaretShape, cx: &mut App) {
27    cx.set_global(shape);
28    cx.refresh_windows();
29}
30
31/// How tall the app-wide block caret stands.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum CaretHeight {
34    /// The line's height, leading included.
35    #[default]
36    Line,
37    /// The bar's height: the text's font size.
38    Text,
39}
40
41impl Global for CaretHeight {}
42
43pub(crate) fn caret_height(cx: &App) -> CaretHeight {
44    cx.try_global::<CaretHeight>().copied().unwrap_or_default()
45}
46
47/// Changes the height and repaints open windows.
48pub(crate) fn set_caret_height(height: CaretHeight, cx: &mut App) {
49    cx.set_global(height);
50    cx.refresh_windows();
51}
52
53/// What the app-wide block caret paints as while its window is inactive.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum InactiveCaret {
56    /// Outlined.
57    #[default]
58    Hollow,
59    /// Nothing.
60    Hidden,
61}
62
63impl Global for InactiveCaret {}
64
65pub(crate) fn inactive_caret(cx: &App) -> InactiveCaret {
66    cx.try_global::<InactiveCaret>()
67        .copied()
68        .unwrap_or_default()
69}
70
71/// Changes it and repaints open windows.
72pub(crate) fn set_inactive_caret(inactive: InactiveCaret, cx: &mut App) {
73    cx.set_global(inactive);
74    cx.refresh_windows();
75}
76
77impl CaretShape {
78    /// Whether a caret of this shape paints at all, given whether its window
79    /// is active. Only a block changes with the window.
80    pub fn shown(self, window_active: bool, inactive: InactiveCaret) -> bool {
81        window_active || self != Self::Block || inactive == InactiveCaret::Hollow
82    }
83
84    /// Adapts a renderer's existing bar, centred in a line `line_height` tall,
85    /// to `width`, which a bar ignores. A block stands as tall as `height`
86    /// says, solid or, when `hollow`, outlined.
87    pub fn quad(
88        self,
89        mut bar: Bounds<Pixels>,
90        line_height: Pixels,
91        height: CaretHeight,
92        width: Pixels,
93        color: Hsla,
94        hollow: bool,
95    ) -> PaintQuad {
96        if self != Self::Bar {
97            bar.size.width = width;
98        }
99        match self {
100            Self::Bar => fill(bar, color),
101            Self::Block => {
102                if height == CaretHeight::Line {
103                    bar.origin.y -= (line_height - bar.size.height) / 2.0;
104                    bar.size.height = line_height;
105                }
106                match hollow {
107                    true => outline(bar, color, BorderStyle::Solid),
108                    false => fill(bar, color),
109                }
110            }
111            Self::Underline => {
112                let thickness = px(2.0).min(bar.size.height);
113                bar.origin.y += bar.size.height - thickness;
114                bar.size.height = thickness;
115                fill(bar, color)
116            }
117        }
118    }
119
120    /// Whether the glyph under the caret is painted in the background colour,
121    /// over the caret, by [`paint_covered`].
122    pub fn cuts_out(self, hollow: bool) -> bool {
123        self == Self::Block && !hollow
124    }
125}
126
127/// The shaped width of `0` in `font`: a wide caret's width where no character
128/// follows it.
129pub fn zero_width(font: Font, font_size: Pixels, window: &Window) -> Pixels {
130    let run = TextRun {
131        len: 1,
132        font,
133        color: Hsla::default(),
134        background_color: None,
135        underline: None,
136        strikethrough: None,
137    };
138    window
139        .text_system()
140        .shape_line("0".into(), font_size, &[run], None)
141        .width
142}
143
144/// Paints the glyphs `layout` shaped for `range` again, in `color`: what a
145/// solid block covers, painted over the block and the line. `at` is the top
146/// left of the first one's slot, on a row `line_height` tall.
147///
148/// gpui shapes each colour of a line apart, so recolouring the covered glyph
149/// in the line's runs moves the glyphs around it.
150pub fn paint_covered(
151    layout: &LineLayout,
152    range: &Range<usize>,
153    at: Point<Pixels>,
154    line_height: Pixels,
155    color: Hsla,
156    window: &mut Window,
157) {
158    let mut glyphs = layout
159        .runs
160        .iter()
161        .flat_map(|run| run.glyphs.iter().map(move |glyph| (run.font_id, glyph)))
162        .filter(|(_, glyph)| range.contains(&glyph.index))
163        .peekable();
164    let Some(left) = glyphs.peek().map(|(_, glyph)| glyph.position.x) else {
165        return;
166    };
167    let baseline = (line_height - layout.ascent - layout.descent) / 2. + layout.ascent;
168    for (font_id, glyph) in glyphs {
169        let origin = point(
170            at.x + glyph.position.x - left,
171            at.y + baseline + glyph.position.y,
172        );
173        let _ = match glyph.is_emoji {
174            true => window.paint_emoji(origin, font_id, glyph.id, layout.font_size),
175            false => window.paint_glyph(origin, font_id, glyph.id, layout.font_size, color),
176        };
177    }
178}
179
180/// `runs` cut at `range`'s edges, the pieces inside it in `color`.
181pub fn recoloured(runs: Vec<TextRun>, range: &Range<usize>, color: Hsla) -> Vec<TextRun> {
182    let mut out = Vec::with_capacity(runs.len() + 2);
183    let mut at = 0;
184    for run in runs {
185        let end = at + run.len;
186        for (start, stop, inside) in [
187            (at, end.min(range.start), false),
188            (at.max(range.start), end.min(range.end), true),
189            (at.max(range.end), end, false),
190        ] {
191            if stop <= start {
192                continue;
193            }
194            out.push(TextRun {
195                len: stop - start,
196                color: if inside { color } else { run.color },
197                ..run.clone()
198            });
199        }
200        at = end;
201    }
202    out
203}
204
205/// The next grapheme's shaped advance, or no character at a hard/soft line end.
206pub fn character_advance(line: &WrappedLineLayout, text: &str, offset: usize) -> Option<Pixels> {
207    if offset >= text.len() || !text.is_char_boundary(offset) {
208        return None;
209    }
210    if line
211        .wrap_boundaries()
212        .iter()
213        .any(|boundary| line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index == offset)
214    {
215        return None;
216    }
217    let end = super::next_boundary(text, offset);
218    Some((line.unwrapped_layout.x_for_index(end) - line.unwrapped_layout.x_for_index(offset)).abs())
219}