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