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