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
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.
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/// `runs` cut at `range`'s edges, the pieces inside it in `color`.
145pub fn recoloured(runs: Vec<TextRun>, range: &Range<usize>, color: Hsla) -> Vec<TextRun> {
146    let mut out = Vec::with_capacity(runs.len() + 2);
147    let mut at = 0;
148    for run in runs {
149        let end = at + run.len;
150        for (start, stop, inside) in [
151            (at, end.min(range.start), false),
152            (at.max(range.start), end.min(range.end), true),
153            (at.max(range.end), end, false),
154        ] {
155            if stop <= start {
156                continue;
157            }
158            out.push(TextRun {
159                len: stop - start,
160                color: if inside { color } else { run.color },
161                ..run.clone()
162            });
163        }
164        at = end;
165    }
166    out
167}
168
169/// The next grapheme's shaped advance, or no character at a hard/soft line end.
170pub fn character_advance(line: &WrappedLineLayout, text: &str, offset: usize) -> Option<Pixels> {
171    if offset >= text.len() || !text.is_char_boundary(offset) {
172        return None;
173    }
174    if line
175        .wrap_boundaries()
176        .iter()
177        .any(|boundary| line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index == offset)
178    {
179        return None;
180    }
181    let end = super::next_boundary(text, offset);
182    Some((line.unwrapped_layout.x_for_index(end) - line.unwrapped_layout.x_for_index(offset)).abs())
183}