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, PaintQuad, Pixels, TextRun, Window,
7    WrappedLineLayout, fill, outline, 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
31impl CaretShape {
32    /// Adapts a renderer's existing bar, centred in a line `line_height` tall,
33    /// to `width`, which a bar ignores. A block fills the line's height, solid
34    /// or, when `hollow`, outlined.
35    pub fn quad(
36        self,
37        mut bar: Bounds<Pixels>,
38        line_height: Pixels,
39        width: Pixels,
40        color: Hsla,
41        hollow: bool,
42    ) -> PaintQuad {
43        if self != Self::Bar {
44            bar.size.width = width;
45        }
46        match self {
47            Self::Bar => fill(bar, color),
48            Self::Block => {
49                bar.origin.y -= (line_height - bar.size.height) / 2.0;
50                bar.size.height = line_height;
51                match hollow {
52                    true => outline(bar, color, BorderStyle::Solid),
53                    false => fill(bar, color),
54                }
55            }
56            Self::Underline => {
57                let thickness = px(2.0).min(bar.size.height);
58                bar.origin.y += bar.size.height - thickness;
59                bar.size.height = thickness;
60                fill(bar, color)
61            }
62        }
63    }
64
65    /// Whether the glyph under the caret is painted in the background colour,
66    /// over the caret.
67    pub fn cuts_out(self, hollow: bool) -> bool {
68        self == Self::Block && !hollow
69    }
70}
71
72/// The shaped width of `0` in `font`: a wide caret's width where no character
73/// follows it.
74pub fn zero_width(font: Font, font_size: Pixels, window: &Window) -> Pixels {
75    let run = TextRun {
76        len: 1,
77        font,
78        color: Hsla::default(),
79        background_color: None,
80        underline: None,
81        strikethrough: None,
82    };
83    window
84        .text_system()
85        .shape_line("0".into(), font_size, &[run], None)
86        .width
87}
88
89/// `runs` cut at `range`'s edges, the pieces inside it in `color`.
90pub fn recoloured(runs: Vec<TextRun>, range: &Range<usize>, color: Hsla) -> Vec<TextRun> {
91    let mut out = Vec::with_capacity(runs.len() + 2);
92    let mut at = 0;
93    for run in runs {
94        let end = at + run.len;
95        for (start, stop, inside) in [
96            (at, end.min(range.start), false),
97            (at.max(range.start), end.min(range.end), true),
98            (at.max(range.end), end, false),
99        ] {
100            if stop <= start {
101                continue;
102            }
103            out.push(TextRun {
104                len: stop - start,
105                color: if inside { color } else { run.color },
106                ..run.clone()
107            });
108        }
109        at = end;
110    }
111    out
112}
113
114/// The next grapheme's shaped advance, or no character at a hard/soft line end.
115pub fn character_advance(line: &WrappedLineLayout, text: &str, offset: usize) -> Option<Pixels> {
116    if offset >= text.len() || !text.is_char_boundary(offset) {
117        return None;
118    }
119    if line
120        .wrap_boundaries()
121        .iter()
122        .any(|boundary| line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index == offset)
123    {
124        return None;
125    }
126    let end = super::next_boundary(text, offset);
127    Some((line.unwrapped_layout.x_for_index(end) - line.unwrapped_layout.x_for_index(offset)).abs())
128}