1use std::ops::Range;
2
3use gpui::{
4 App, HighlightStyle, IntoElement, ParentElement, RenderOnce, SharedString, StyleRefinement,
5 Styled, StyledText, Window, div, prelude::FluentBuilder, rems,
6};
7
8use crate::{ActiveTheme, StyledExt};
9
10const MASKED: &'static str = "•";
11
12#[derive(Clone)]
14pub enum HighlightsMatch {
15 Prefix(SharedString),
16 Full(SharedString),
17}
18
19impl HighlightsMatch {
20 pub fn as_str(&self) -> &str {
21 match self {
22 Self::Prefix(s) => s.as_str(),
23 Self::Full(s) => s.as_str(),
24 }
25 }
26
27 #[inline]
28 pub fn is_prefix(&self) -> bool {
29 matches!(self, Self::Prefix(_))
30 }
31}
32
33impl From<&str> for HighlightsMatch {
34 fn from(value: &str) -> Self {
35 Self::Full(value.to_string().into())
36 }
37}
38
39impl From<String> for HighlightsMatch {
40 fn from(value: String) -> Self {
41 Self::Full(value.into())
42 }
43}
44
45impl From<SharedString> for HighlightsMatch {
46 fn from(value: SharedString) -> Self {
47 Self::Full(value)
48 }
49}
50
51#[derive(IntoElement)]
53pub struct Label {
54 style: StyleRefinement,
55 label: SharedString,
56 secondary: Option<SharedString>,
57 masked: bool,
58 highlights_text: Option<HighlightsMatch>,
59}
60
61impl Label {
62 pub fn new(label: impl Into<SharedString>) -> Self {
64 let label: SharedString = label.into();
65 Self {
66 style: Default::default(),
67 label,
68 secondary: None,
69 masked: false,
70 highlights_text: None,
71 }
72 }
73
74 pub fn secondary(mut self, secondary: impl Into<SharedString>) -> Self {
77 self.secondary = Some(secondary.into());
78 self
79 }
80
81 pub fn masked(mut self, masked: bool) -> Self {
83 self.masked = masked;
84 self
85 }
86
87 pub fn highlights(mut self, text: impl Into<HighlightsMatch>) -> Self {
89 self.highlights_text = Some(text.into());
90 self
91 }
92
93 fn full_text(&self) -> SharedString {
94 match &self.secondary {
95 Some(secondary) => format!("{} {}", self.label, secondary).into(),
96 None => self.label.clone(),
97 }
98 }
99
100 fn highlight_ranges(&self, total_length: usize) -> Vec<Range<usize>> {
101 let mut ranges = Vec::new();
102 let full_text = self.full_text();
103
104 if self.secondary.is_some() {
105 ranges.push(0..self.label.len());
106 ranges.push(self.label.len()..total_length);
107 }
108
109 if let Some(matched) = &self.highlights_text {
110 let matched_str = matched.as_str();
111 if !matched_str.is_empty() {
112 let search_lower = matched_str.to_lowercase();
113 let full_text_lower = full_text.to_lowercase();
114
115 if matched.is_prefix() {
116 if full_text_lower.starts_with(&search_lower) {
118 ranges.push(0..matched_str.len());
119 }
120 } else {
121 let mut search_start = 0;
123 while let Some(pos) = full_text_lower[search_start..].find(&search_lower) {
124 let match_start = search_start + pos;
125 let match_end = match_start + matched_str.len();
126
127 if match_end <= full_text.len() {
128 ranges.push(match_start..match_end);
129 }
130
131 search_start = match_start + 1;
132 while !full_text.is_char_boundary(search_start)
133 && search_start < full_text.len()
134 {
135 search_start += 1;
136 }
137
138 if search_start >= full_text.len() {
139 break;
140 }
141 }
142 }
143 }
144 }
145
146 ranges
147 }
148
149 fn measure_highlights(
150 &self,
151 length: usize,
152 cx: &mut App,
153 ) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
154 if self.masked {
155 return None;
156 }
157
158 let ranges = self.highlight_ranges(length);
159 if ranges.is_empty() {
160 return None;
161 }
162
163 let mut highlights = Vec::new();
164 let mut highlight_ranges_added = 0;
165
166 if self.secondary.is_some() {
167 highlights.push((ranges[0].clone(), HighlightStyle::default()));
168 highlights.push((
169 ranges[1].clone(),
170 HighlightStyle {
171 color: Some(cx.theme().muted_foreground),
172 ..Default::default()
173 },
174 ));
175 highlight_ranges_added = 2;
176 }
177
178 for range in ranges.iter().skip(highlight_ranges_added) {
179 highlights.push((
180 range.clone(),
181 HighlightStyle {
182 color: Some(cx.theme().blue),
183 ..Default::default()
184 },
185 ));
186 }
187
188 Some(gpui::combine_highlights(vec![], highlights).collect())
189 }
190}
191
192impl Styled for Label {
193 fn style(&mut self) -> &mut gpui::StyleRefinement {
194 &mut self.style
195 }
196}
197
198impl RenderOnce for Label {
199 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
200 let mut text = self.full_text();
201 let chars_count = text.chars().count();
202
203 if self.masked {
204 text = SharedString::from(MASKED.repeat(chars_count))
205 };
206
207 let highlights = self.measure_highlights(text.len(), cx);
208
209 div()
210 .line_height(rems(1.25))
211 .text_color(cx.theme().foreground)
212 .refine_style(&self.style)
213 .child(
214 StyledText::new(&text).when_some(highlights, |this, hl| this.with_highlights(hl)),
215 )
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 struct MaskedLabels;
224
225 impl gpui::Render for MaskedLabels {
226 fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
227 div()
228 .child(Label::new("Hello").secondary("World").masked(true))
229 .child(Label::new("Hello").highlights("ell").masked(true))
230 .child(
231 Label::new("é🙂")
232 .secondary("世界")
233 .highlights("🙂 世")
234 .masked(true),
235 )
236 }
237 }
238
239 #[gpui::test]
240 fn masked_secondary_text_and_highlights_render(cx: &mut gpui::TestAppContext) {
241 cx.update(crate::init);
242 cx.update(|cx| {
243 let label = Label::new("é🙂")
244 .secondary("世界")
245 .highlights("🙂 世")
246 .masked(true);
247 assert!(
248 label
249 .measure_highlights(label.full_text().len(), cx)
250 .is_none()
251 );
252 });
253 let (_, cx) = cx.add_window_view(|_, _| MaskedLabels);
254 cx.update(|window, cx| window.draw(cx).clear(cx));
255 }
256
257 #[test]
258 fn test_highlight_ranges() {
259 let label = Label::new("Hello World");
263 let result = label.highlight_ranges("Hello World".len());
264 assert_eq!(result, Vec::<Range<usize>>::new());
265
266 let label = Label::new("Hello").secondary("World");
268 let total_length = "Hello World".len();
269 let result = label.highlight_ranges(total_length);
270 assert_eq!(result.len(), 2);
271 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); let label = Label::new("Hello World").highlights("WORLD");
278 let result = label.highlight_ranges("Hello World".len());
279 assert_eq!(result.len(), 1);
280 assert_eq!(result[0], 6..11); let label = Label::new("Hello Hello Hello").highlights("Hello");
284 let result = label.highlight_ranges("Hello Hello Hello".len());
285 assert_eq!(result.len(), 3);
286 assert_eq!(result[0], 0..5); assert_eq!(result[1], 6..11); assert_eq!(result[2], 12..17); let label = Label::new("Hello World").highlights("xyz");
292 let result = label.highlight_ranges("Hello World".len());
293 assert_eq!(result, Vec::<Range<usize>>::new());
294
295 let label = Label::new("Hello World").highlights("");
296 let result = label.highlight_ranges("Hello World".len());
297 assert_eq!(result, Vec::<Range<usize>>::new());
298
299 let label = Label::new("Hello").secondary("World").highlights("llo");
303 let total_length = "Hello World".len();
304 let result = label.highlight_ranges(total_length);
305 assert_eq!(result.len(), 3);
306 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 2..5); let label = Label::new("Hello").secondary("World").highlights("World");
312 let total_length = "Hello World".len();
313 let result = label.highlight_ranges(total_length);
314 assert_eq!(result.len(), 3);
315 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 6..11); let label = Label::new("Hello").secondary("World").highlights("o W");
321 let total_length = "Hello World".len();
322 let result = label.highlight_ranges(total_length);
323 assert_eq!(result.len(), 3);
324 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 4..7); let label = Label::new("aaaa").highlights("aa");
332 let result = label.highlight_ranges("aaaa".len());
333 assert!(result.len() >= 2);
334 assert_eq!(result[0], 0..2); assert_eq!(result[1], 1..3); let label = Label::new("你好世界,Hello World").highlights("世界");
339 let result = label.highlight_ranges("你好世界,Hello World".len());
340 assert_eq!(result.len(), 1);
341 let text = "你好世界,Hello World";
342 let start = text.find("世界").unwrap();
343 let end = start + "世界".len();
344 assert_eq!(result[0], start..end);
345 }
346
347 #[test]
348 fn test_highlight_ranges_prefix() {
349 let label = Label::new("aaaa").highlights(HighlightsMatch::Prefix("aa".into()));
351 let result = label.highlight_ranges("aaaa".len());
352 assert_eq!(result.len(), 1);
353 assert_eq!(result[0], 0..2); let label_full =
357 Label::new("Hello Hello").highlights(HighlightsMatch::Full("Hello".into()));
358 let result_full = label_full.highlight_ranges("Hello Hello".len());
359 assert_eq!(result_full.len(), 2); let label_prefix =
362 Label::new("Hello Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
363 let result_prefix = label_prefix.highlight_ranges("Hello Hello".len());
364 assert_eq!(result_prefix.len(), 1); assert_eq!(result_prefix[0], 0..5);
366
367 let label =
369 Label::new("Hello hello HELLO").highlights(HighlightsMatch::Prefix("hello".into()));
370 let result = label.highlight_ranges("Hello hello HELLO".len());
371 assert_eq!(result.len(), 1);
372 assert_eq!(result[0], 0..5); let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("xyz".into()));
376 let result = label.highlight_ranges("Hello World".len());
377 assert_eq!(result.len(), 0);
378
379 let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("".into()));
381 let result = label.highlight_ranges("Hello World".len());
382 assert_eq!(result.len(), 0);
383
384 let label = Label::new("Hello")
386 .secondary("Hello World")
387 .highlights(HighlightsMatch::Prefix("Hello".into()));
388 let total_length = "Hello Hello World".len();
389 let result = label.highlight_ranges(total_length);
390 assert_eq!(result.len(), 3); assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..17); assert_eq!(result[2], 0..5); let label = Label::new("abc")
397 .secondary("def abc def")
398 .highlights(HighlightsMatch::Prefix("abc".into()));
399 let total_length = "abc def abc def".len();
400 let result = label.highlight_ranges(total_length);
401 assert_eq!(result.len(), 3); assert_eq!(result[0], 0..3); assert_eq!(result[1], 3..15); assert_eq!(result[2], 0..3); let label = Label::new("你好世界你好").highlights(HighlightsMatch::Prefix("你好".into()));
408 let result = label.highlight_ranges("你好世界你好".len());
409 assert_eq!(result.len(), 1);
410 assert_eq!(result[0], 0..6); let label = Label::new("abababab").highlights(HighlightsMatch::Prefix("abab".into()));
414 let result = label.highlight_ranges("abababab".len());
415 assert_eq!(result.len(), 1);
416 assert_eq!(result[0], 0..4); let label =
420 Label::new("xyz Hello abc Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
421 let result = label.highlight_ranges("xyz Hello abc Hello".len());
422 assert_eq!(result.len(), 0); let prefix_match = HighlightsMatch::Prefix("test".into());
426 let full_match = HighlightsMatch::Full("test".into());
427 assert!(prefix_match.is_prefix());
428 assert!(!full_match.is_prefix());
429
430 let prefix_match = HighlightsMatch::Prefix("test".into());
432 assert_eq!(prefix_match.as_str(), "test");
433 }
434}