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