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}