1use 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#[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
25pub(crate) fn set_caret_shape(shape: CaretShape, cx: &mut App) {
27 cx.set_global(shape);
28 cx.refresh_windows();
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum CaretHeight {
34 #[default]
36 Line,
37 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
47pub(crate) fn set_caret_height(height: CaretHeight, cx: &mut App) {
49 cx.set_global(height);
50 cx.refresh_windows();
51}
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum InactiveCaret {
56 #[default]
58 Hollow,
59 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
71pub(crate) fn set_inactive_caret(inactive: InactiveCaret, cx: &mut App) {
73 cx.set_global(inactive);
74 cx.refresh_windows();
75}
76
77impl CaretShape {
78 pub fn shown(self, window_active: bool, inactive: InactiveCaret) -> bool {
81 window_active || self != Self::Block || inactive == InactiveCaret::Hollow
82 }
83
84 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 pub fn cuts_out(self, hollow: bool) -> bool {
123 self == Self::Block && !hollow
124 }
125}
126
127pub 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
144pub 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
180pub 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
205pub 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}