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