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