label.rs

  1use std::ops::Range;
  2
  3use crate::{LabelLike, prelude::*};
  4use gpui::{HighlightStyle, StyleRefinement, StyledText};
  5
  6/// A struct representing a label element in the UI.
  7///
  8/// The `Label` struct stores the label text and common properties for a label element.
  9/// It provides methods for modifying these properties.
 10///
 11/// # Examples
 12///
 13/// ```
 14/// use ui::prelude::*;
 15///
 16/// Label::new("Hello, World!");
 17/// ```
 18///
 19/// **A colored label**, for example labeling a dangerous action:
 20///
 21/// ```
 22/// use ui::prelude::*;
 23///
 24/// let my_label = Label::new("Delete").color(Color::Error);
 25/// ```
 26///
 27/// **A label with a strikethrough**, for example labeling something that has been deleted:
 28///
 29/// ```
 30/// use ui::prelude::*;
 31///
 32/// let my_label = Label::new("Deleted").strikethrough();
 33/// ```
 34#[derive(IntoElement, RegisterComponent)]
 35pub struct Label {
 36    base: LabelLike,
 37    label: SharedString,
 38    render_code_spans: bool,
 39}
 40
 41impl Label {
 42    /// Creates a new [`Label`] with the given text.
 43    ///
 44    /// # Examples
 45    ///
 46    /// ```
 47    /// use ui::prelude::*;
 48    ///
 49    /// let my_label = Label::new("Hello, World!");
 50    /// ```
 51    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    /// When enabled, text wrapped in backticks (e.g. `` `code` ``) will be
 60    /// rendered in the buffer (monospace) font.
 61    pub fn render_code_spans(mut self) -> Self {
 62        self.render_code_spans = true;
 63        self
 64    }
 65
 66    /// Sets the text of the [`Label`].
 67    pub fn set_text(&mut self, text: impl Into<SharedString>) {
 68        self.label = text.into();
 69    }
 70
 71    /// Truncates the label from the start, keeping the end visible.
 72    pub fn truncate_start(mut self) -> Self {
 73        self.base = self.base.truncate_start();
 74        self
 75    }
 76}
 77
 78// Style methods.
 79impl 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    /// Sets the size of the label using a [`LabelSize`].
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use ui::prelude::*;
124    ///
125    /// let my_label = Label::new("Hello, World!").size(LabelSize::Small);
126    /// ```
127    fn size(mut self, size: LabelSize) -> Self {
128        self.base = self.base.size(size);
129        self
130    }
131
132    /// Sets the weight of the label using a [`FontWeight`].
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use gpui::FontWeight;
138    /// use ui::prelude::*;
139    ///
140    /// let my_label = Label::new("Hello, World!").weight(FontWeight::BOLD);
141    /// ```
142    fn weight(mut self, weight: gpui::FontWeight) -> Self {
143        self.base = self.base.weight(weight);
144        self
145    }
146
147    /// Sets the line height style of the label using a [`LineHeightStyle`].
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use ui::prelude::*;
153    ///
154    /// let my_label = Label::new("Hello, World!").line_height_style(LineHeightStyle::UiLabel);
155    /// ```
156    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    /// Sets the color of the label using a [`Color`].
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use ui::prelude::*;
167    ///
168    /// let my_label = Label::new("Hello, World!").color(Color::Accent);
169    /// ```
170    fn color(mut self, color: Color) -> Self {
171        self.base = self.base.color(color);
172        self
173    }
174
175    /// Sets the strikethrough property of the label.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use ui::prelude::*;
181    ///
182    /// let my_label = Label::new("Hello, World!").strikethrough();
183    /// ```
184    fn strikethrough(mut self) -> Self {
185        self.base = self.base.strikethrough();
186        self
187    }
188
189    /// Sets the italic property of the label.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use ui::prelude::*;
195    ///
196    /// let my_label = Label::new("Hello, World!").italic();
197    /// ```
198    fn italic(mut self) -> Self {
199        self.base = self.base.italic();
200        self
201    }
202
203    /// Sets the alpha property of the color of label.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use ui::prelude::*;
209    ///
210    /// let my_label = Label::new("Hello, World!").alpha(0.5);
211    /// ```
212    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    /// Truncates overflowing text with an ellipsis (`…`) if needed.
223    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    /// Styles the label to look like inline code.
240    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
278/// Parses backtick-delimited code spans from a string.
279///
280/// Returns `None` if there are no matched backtick pairs.
281/// Otherwise returns the text with backticks stripped and the byte ranges
282/// of the code spans in the stripped string.
283fn 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 `zed` to open").unwrap();
325        assert_eq!(text.as_ref(), "use zed 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        // A trailing unmatched backtick should not produce a code range
339        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 to Zed").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("zed/crates/ui/src/components/label/truncate/label/label.rs").truncate_start()).into_any_element()),
404                        ],
405                    ),
406                ])
407                .into_any_element()
408        )
409    }
410}