1use std::ops::Range;
2
3use crate::{LabelLike, prelude::*};
4use gpui::{HighlightStyle, StyleRefinement, StyledText};
5
6#[derive(IntoElement, RegisterComponent)]
35pub struct Label {
36 base: LabelLike,
37 label: SharedString,
38 render_code_spans: bool,
39}
40
41impl Label {
42 pub fn new(label: impl Into<SharedString>) -> Self {
52 Self {
53 base: LabelLike::new(),
54 label: label.into(),
55 render_code_spans: false,
56 }
57 }
58
59 pub fn render_code_spans(mut self) -> Self {
62 self.render_code_spans = true;
63 self
64 }
65
66 pub fn set_text(&mut self, text: impl Into<SharedString>) {
68 self.label = text.into();
69 }
70
71 pub fn truncate_start(mut self) -> Self {
73 self.base = self.base.truncate_start();
74 self
75 }
76}
77
78impl Label {
80 fn style(&mut self) -> &mut StyleRefinement {
81 self.base.base.style()
82 }
83
84 gpui::margin_style_methods!({
85 visibility: pub
86 });
87
88 pub fn flex_1(mut self) -> Self {
89 self.style().flex_grow = Some(1.);
90 self.style().flex_shrink = Some(1.);
91 self.style().flex_basis = Some(gpui::relative(0.).into());
92 self
93 }
94
95 pub fn flex_none(mut self) -> Self {
96 self.style().flex_grow = Some(0.);
97 self.style().flex_shrink = Some(0.);
98 self
99 }
100
101 pub fn flex_grow(mut self) -> Self {
102 self.style().flex_grow = Some(1.);
103 self
104 }
105
106 pub fn flex_shrink(mut self) -> Self {
107 self.style().flex_shrink = Some(1.);
108 self
109 }
110
111 pub fn flex_shrink_0(mut self) -> Self {
112 self.style().flex_shrink = Some(0.);
113 self
114 }
115}
116
117impl LabelCommon for Label {
118 fn size(mut self, size: LabelSize) -> Self {
128 self.base = self.base.size(size);
129 self
130 }
131
132 fn weight(mut self, weight: gpui::FontWeight) -> Self {
143 self.base = self.base.weight(weight);
144 self
145 }
146
147 fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
157 self.base = self.base.line_height_style(line_height_style);
158 self
159 }
160
161 fn color(mut self, color: Color) -> Self {
171 self.base = self.base.color(color);
172 self
173 }
174
175 fn strikethrough(mut self) -> Self {
185 self.base = self.base.strikethrough();
186 self
187 }
188
189 fn italic(mut self) -> Self {
199 self.base = self.base.italic();
200 self
201 }
202
203 fn alpha(mut self, alpha: f32) -> Self {
213 self.base = self.base.alpha(alpha);
214 self
215 }
216
217 fn underline(mut self) -> Self {
218 self.base = self.base.underline();
219 self
220 }
221
222 fn truncate(mut self) -> Self {
224 self.base = self.base.truncate();
225 self
226 }
227
228 fn single_line(mut self) -> Self {
229 self.label = SharedString::from(self.label.replace('\n', "⏎"));
230 self.base = self.base.single_line();
231 self
232 }
233
234 fn buffer_font(mut self, cx: &App) -> Self {
235 self.base = self.base.buffer_font(cx);
236 self
237 }
238
239 fn inline_code(mut self, cx: &App) -> Self {
241 self.base = self.base.inline_code(cx);
242 self
243 }
244}
245
246impl RenderOnce for Label {
247 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
248 if self.render_code_spans {
249 if let Some((stripped, code_ranges)) = parse_backtick_spans(&self.label) {
250 let buffer_font_family = theme::theme_settings(cx).buffer_font(cx).family.clone();
251 let background_color = cx.theme().colors().element_background;
252
253 let highlights = code_ranges.iter().map(|range| {
254 (
255 range.clone(),
256 HighlightStyle {
257 background_color: Some(background_color),
258 ..Default::default()
259 },
260 )
261 });
262
263 let font_overrides = code_ranges
264 .iter()
265 .map(|range| (range.clone(), buffer_font_family.clone()));
266
267 return self.base.child(
268 StyledText::new(stripped)
269 .with_highlights(highlights)
270 .with_font_family_overrides(font_overrides),
271 );
272 }
273 }
274 self.base.child(self.label)
275 }
276}
277
278fn parse_backtick_spans(text: &str) -> Option<(SharedString, Vec<Range<usize>>)> {
284 if !text.contains('`') {
285 return None;
286 }
287
288 let mut stripped = String::with_capacity(text.len());
289 let mut code_ranges = Vec::new();
290 let mut in_code = false;
291 let mut code_start = 0;
292
293 for ch in text.chars() {
294 if ch == '`' {
295 if in_code {
296 code_ranges.push(code_start..stripped.len());
297 } else {
298 code_start = stripped.len();
299 }
300 in_code = !in_code;
301 } else {
302 stripped.push(ch);
303 }
304 }
305
306 if code_ranges.is_empty() {
307 return None;
308 }
309
310 Some((SharedString::from(stripped), code_ranges))
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_parse_backtick_spans_no_backticks() {
319 assert_eq!(parse_backtick_spans("plain text"), None);
320 }
321
322 #[test]
323 fn test_parse_backtick_spans_single_span() {
324 let (text, ranges) = parse_backtick_spans("use `app` to open").unwrap();
325 assert_eq!(text.as_ref(), "use app to open");
326 assert_eq!(ranges, vec![4..7]);
327 }
328
329 #[test]
330 fn test_parse_backtick_spans_multiple_spans() {
331 let (text, ranges) = parse_backtick_spans("flags `-e` or `-n`").unwrap();
332 assert_eq!(text.as_ref(), "flags -e or -n");
333 assert_eq!(ranges, vec![6..8, 12..14]);
334 }
335
336 #[test]
337 fn test_parse_backtick_spans_unmatched_backtick() {
338 assert_eq!(parse_backtick_spans("trailing `backtick"), None);
340 }
341
342 #[test]
343 fn test_parse_backtick_spans_empty_span() {
344 let (text, ranges) = parse_backtick_spans("empty `` span").unwrap();
345 assert_eq!(text.as_ref(), "empty span");
346 assert_eq!(ranges, vec![6..6]);
347 }
348}
349
350impl Component for Label {
351 fn scope() -> ComponentScope {
352 ComponentScope::Typography
353 }
354
355 fn description() -> Option<&'static str> {
356 Some("A text label component that supports various styles, sizes, and formatting options.")
357 }
358
359 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
360 Some(
361 v_flex()
362 .gap_6()
363 .children(vec![
364 example_group_with_title(
365 "Sizes",
366 vec![
367 single_example("Default", Label::new("Project Explorer").into_any_element()),
368 single_example("Small", Label::new("File: main.rs").size(LabelSize::Small).into_any_element()),
369 single_example("Large", Label::new("Welcome").size(LabelSize::Large).into_any_element()),
370 ],
371 ),
372 example_group_with_title(
373 "Colors",
374 vec![
375 single_example("Default", Label::new("Status: Ready").into_any_element()),
376 single_example("Accent", Label::new("New Update Available").color(Color::Accent).into_any_element()),
377 single_example("Error", Label::new("Build Failed").color(Color::Error).into_any_element()),
378 ],
379 ),
380 example_group_with_title(
381 "Styles",
382 vec![
383 single_example("Default", Label::new("Normal Text").into_any_element()),
384 single_example("Bold", Label::new("Important Notice").weight(gpui::FontWeight::BOLD).into_any_element()),
385 single_example("Italic", Label::new("Code Comment").italic().into_any_element()),
386 single_example("Strikethrough", Label::new("Deprecated Feature").strikethrough().into_any_element()),
387 single_example("Underline", Label::new("Clickable Link").underline().into_any_element()),
388 single_example("Inline Code", Label::new("fn main() {}").inline_code(cx).into_any_element()),
389 ],
390 ),
391 example_group_with_title(
392 "Line Height Styles",
393 vec![
394 single_example("Default", Label::new("Multi-line\nText\nExample").into_any_element()),
395 single_example("UI Label", Label::new("Compact\nUI\nLabel").line_height_style(LineHeightStyle::UiLabel).into_any_element()),
396 ],
397 ),
398 example_group_with_title(
399 "Special Cases",
400 vec![
401 single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
402 single_example("Regular Truncation", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
403 single_example("Start Truncation", div().max_w_24().child(Label::new("app/crates/ui/src/components/label/truncate/label/label.rs").truncate_start()).into_any_element()),
404 ],
405 ),
406 ])
407 .into_any_element()
408 )
409 }
410}