1use 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#[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 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
169pub 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}