1use std::ops::Range;
2
3use gpui::{FontWeight, HighlightStyle, StyleRefinement, StyledText};
4
5use crate::{LabelCommon, LabelLike, LabelSize, LineHeightStyle, prelude::*};
6
7#[derive(IntoElement, RegisterComponent)]
8pub struct HighlightedLabel {
9 base: LabelLike,
10 label: SharedString,
11 highlight_indices: Vec<usize>,
12}
13
14impl HighlightedLabel {
15 /// Constructs a label with the given characters highlighted.
16 /// Characters are identified by UTF-8 byte position.
17 pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
18 let label = label.into();
19 for &run in &highlight_indices {
20 assert!(
21 label.is_char_boundary(run),
22 "highlight index {run} is not a valid UTF-8 boundary"
23 );
24 }
25 Self {
26 base: LabelLike::new(),
27 label,
28 highlight_indices,
29 }
30 }
31
32 /// Constructs a label with the given byte ranges highlighted.
33 /// Assumes that the highlight ranges are valid UTF-8 byte positions.
34 pub fn from_ranges(
35 label: impl Into<SharedString>,
36 highlight_ranges: Vec<Range<usize>>,
37 ) -> Self {
38 let label = label.into();
39 let highlight_indices = highlight_ranges
40 .iter()
41 .flat_map(|range| {
42 let mut indices = Vec::new();
43 let mut index = range.start;
44 while index < range.end {
45 indices.push(index);
46 index += label[index..].chars().next().map_or(0, |c| c.len_utf8());
47 }
48 indices
49 })
50 .collect();
51
52 Self {
53 base: LabelLike::new(),
54 label,
55 highlight_indices,
56 }
57 }
58
59 pub fn text(&self) -> &str {
60 self.label.as_str()
61 }
62
63 pub fn highlight_indices(&self) -> &[usize] {
64 &self.highlight_indices
65 }
66}
67
68impl HighlightedLabel {
69 fn style(&mut self) -> &mut StyleRefinement {
70 self.base.base.style()
71 }
72
73 pub fn flex_1(mut self) -> Self {
74 self.style().flex_grow = Some(1.);
75 self.style().flex_shrink = Some(1.);
76 self.style().flex_basis = Some(gpui::relative(0.).into());
77 self
78 }
79
80 pub fn flex_none(mut self) -> Self {
81 self.style().flex_grow = Some(0.);
82 self.style().flex_shrink = Some(0.);
83 self
84 }
85
86 pub fn flex_grow(mut self) -> Self {
87 self.style().flex_grow = Some(1.);
88 self
89 }
90
91 pub fn flex_shrink(mut self) -> Self {
92 self.style().flex_shrink = Some(1.);
93 self
94 }
95
96 pub fn flex_shrink_0(mut self) -> Self {
97 self.style().flex_shrink = Some(0.);
98 self
99 }
100}
101
102impl LabelCommon for HighlightedLabel {
103 fn size(mut self, size: LabelSize) -> Self {
104 self.base = self.base.size(size);
105 self
106 }
107
108 fn weight(mut self, weight: FontWeight) -> Self {
109 self.base = self.base.weight(weight);
110 self
111 }
112
113 fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
114 self.base = self.base.line_height_style(line_height_style);
115 self
116 }
117
118 fn color(mut self, color: Color) -> Self {
119 self.base = self.base.color(color);
120 self
121 }
122
123 fn strikethrough(mut self) -> Self {
124 self.base = self.base.strikethrough();
125 self
126 }
127
128 fn italic(mut self) -> Self {
129 self.base = self.base.italic();
130 self
131 }
132
133 fn alpha(mut self, alpha: f32) -> Self {
134 self.base = self.base.alpha(alpha);
135 self
136 }
137
138 fn underline(mut self) -> Self {
139 self.base = self.base.underline();
140 self
141 }
142
143 fn truncate(mut self) -> Self {
144 self.base = self.base.truncate();
145 self
146 }
147
148 fn single_line(mut self) -> Self {
149 self.base = self.base.single_line();
150 self
151 }
152
153 fn buffer_font(mut self, cx: &App) -> Self {
154 self.base = self.base.buffer_font(cx);
155 self
156 }
157
158 fn inline_code(mut self, cx: &App) -> Self {
159 self.base = self.base.inline_code(cx);
160 self
161 }
162}
163
164pub fn highlight_ranges(
165 text: &str,
166 indices: &[usize],
167 style: HighlightStyle,
168) -> Vec<(Range<usize>, HighlightStyle)> {
169 let mut highlight_indices = indices.iter().copied().peekable();
170 let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
171
172 while let Some(start_ix) = highlight_indices.next() {
173 let mut end_ix = start_ix;
174
175 loop {
176 end_ix += text[end_ix..].chars().next().map_or(0, |c| c.len_utf8());
177 if highlight_indices.next_if(|&ix| ix == end_ix).is_none() {
178 break;
179 }
180 }
181
182 highlights.push((start_ix..end_ix, style));
183 }
184
185 highlights
186}
187
188impl RenderOnce for HighlightedLabel {
189 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
190 let highlight_color = cx.theme().colors().text_accent;
191
192 let highlights = highlight_ranges(
193 &self.label,
194 &self.highlight_indices,
195 HighlightStyle {
196 color: Some(highlight_color),
197 ..Default::default()
198 },
199 );
200
201 let mut text_style = window.text_style();
202 text_style.color = self.base.color.color(cx);
203
204 self.base
205 .child(StyledText::new(self.label).with_default_highlights(&text_style, highlights))
206 }
207}
208
209impl Component for HighlightedLabel {
210 fn scope() -> ComponentScope {
211 ComponentScope::Typography
212 }
213
214 fn name() -> &'static str {
215 "HighlightedLabel"
216 }
217
218 fn description() -> Option<&'static str> {
219 Some("A label with highlighted characters based on specified indices.")
220 }
221
222 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
223 Some(
224 v_flex()
225 .gap_6()
226 .children(vec![
227 example_group_with_title(
228 "Basic Usage",
229 vec![
230 single_example(
231 "Default",
232 HighlightedLabel::new("Highlighted Text", vec![0, 1, 2, 3]).into_any_element(),
233 ),
234 single_example(
235 "Custom Color",
236 HighlightedLabel::new("Colored Highlight", vec![0, 1, 7, 8, 9])
237 .color(Color::Accent)
238 .into_any_element(),
239 ),
240 ],
241 ),
242 example_group_with_title(
243 "Styles",
244 vec![
245 single_example(
246 "Bold",
247 HighlightedLabel::new("Bold Highlight", vec![0, 1, 2, 3])
248 .weight(FontWeight::BOLD)
249 .into_any_element(),
250 ),
251 single_example(
252 "Italic",
253 HighlightedLabel::new("Italic Highlight", vec![0, 1, 6, 7, 8])
254 .italic()
255 .into_any_element(),
256 ),
257 single_example(
258 "Underline",
259 HighlightedLabel::new("Underlined Highlight", vec![0, 1, 10, 11, 12])
260 .underline()
261 .into_any_element(),
262 ),
263 ],
264 ),
265 example_group_with_title(
266 "Sizes",
267 vec![
268 single_example(
269 "Small",
270 HighlightedLabel::new("Small Highlight", vec![0, 1, 5, 6, 7])
271 .size(LabelSize::Small)
272 .into_any_element(),
273 ),
274 single_example(
275 "Large",
276 HighlightedLabel::new("Large Highlight", vec![0, 1, 5, 6, 7])
277 .size(LabelSize::Large)
278 .into_any_element(),
279 ),
280 ],
281 ),
282 example_group_with_title(
283 "Special Cases",
284 vec![
285 single_example(
286 "Single Line",
287 HighlightedLabel::new("Single Line Highlight\nWith Newline", vec![0, 1, 7, 8, 9])
288 .single_line()
289 .into_any_element(),
290 ),
291 single_example(
292 "Truncate",
293 HighlightedLabel::new("This is a very long text that should be truncated with highlights", vec![0, 1, 2, 3, 4, 5])
294 .truncate()
295 .into_any_element(),
296 ),
297 ],
298 ),
299 ])
300 .into_any_element()
301 )
302 }
303}