1use gpui::{relative, Hsla, WindowContext};
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5use crate::styled_ext::StyledExt;
6
7#[derive(Default, PartialEq, Copy, Clone)]
8pub enum LabelColor {
9 #[default]
10 Default,
11 Muted,
12 Created,
13 Modified,
14 Deleted,
15 Disabled,
16 Hidden,
17 Placeholder,
18 Accent,
19}
20
21impl LabelColor {
22 pub fn hsla(&self, cx: &WindowContext) -> Hsla {
23 match self {
24 Self::Default => cx.theme().colors().text,
25 Self::Muted => cx.theme().colors().text_muted,
26 Self::Created => cx.theme().status().created,
27 Self::Modified => cx.theme().status().modified,
28 Self::Deleted => cx.theme().status().deleted,
29 Self::Disabled => cx.theme().colors().text_disabled,
30 Self::Hidden => cx.theme().status().hidden,
31 Self::Placeholder => cx.theme().colors().text_placeholder,
32 Self::Accent => cx.theme().colors().text_accent,
33 }
34 }
35}
36
37#[derive(Default, PartialEq, Copy, Clone)]
38pub enum LineHeightStyle {
39 #[default]
40 TextLabel,
41 /// Sets the line height to 1
42 UILabel,
43}
44
45#[derive(Component)]
46pub struct Label {
47 label: SharedString,
48 line_height_style: LineHeightStyle,
49 color: LabelColor,
50 strikethrough: bool,
51}
52
53impl Label {
54 pub fn new(label: impl Into<SharedString>) -> Self {
55 Self {
56 label: label.into(),
57 line_height_style: LineHeightStyle::default(),
58 color: LabelColor::Default,
59 strikethrough: false,
60 }
61 }
62
63 pub fn color(mut self, color: LabelColor) -> Self {
64 self.color = color;
65 self
66 }
67
68 pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
69 self.line_height_style = line_height_style;
70 self
71 }
72
73 pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
74 self.strikethrough = strikethrough;
75 self
76 }
77
78 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
79 div()
80 .when(self.strikethrough, |this| {
81 this.relative().child(
82 div()
83 .absolute()
84 .top_1_2()
85 .w_full()
86 .h_px()
87 .bg(LabelColor::Hidden.hsla(cx)),
88 )
89 })
90 .text_ui()
91 .when(self.line_height_style == LineHeightStyle::UILabel, |this| {
92 this.line_height(relative(1.))
93 })
94 .text_color(self.color.hsla(cx))
95 .child(self.label.clone())
96 }
97}
98
99#[derive(Component)]
100pub struct HighlightedLabel {
101 label: SharedString,
102 color: LabelColor,
103 highlight_indices: Vec<usize>,
104 strikethrough: bool,
105}
106
107impl HighlightedLabel {
108 pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
109 Self {
110 label: label.into(),
111 color: LabelColor::Default,
112 highlight_indices,
113 strikethrough: false,
114 }
115 }
116
117 pub fn color(mut self, color: LabelColor) -> Self {
118 self.color = color;
119 self
120 }
121
122 pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
123 self.strikethrough = strikethrough;
124 self
125 }
126
127 fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
128 let highlight_color = cx.theme().colors().text_accent;
129
130 let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
131
132 let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
133
134 for (char_ix, char) in self.label.char_indices() {
135 let mut color = self.color.hsla(cx);
136
137 if let Some(highlight_ix) = highlight_indices.peek() {
138 if char_ix == *highlight_ix {
139 color = highlight_color;
140
141 highlight_indices.next();
142 }
143 }
144
145 let last_run = runs.last_mut();
146
147 let start_new_run = if let Some(last_run) = last_run {
148 if color == last_run.color {
149 last_run.text.push(char);
150 false
151 } else {
152 true
153 }
154 } else {
155 true
156 };
157
158 if start_new_run {
159 runs.push(Run {
160 text: char.to_string(),
161 color,
162 });
163 }
164 }
165
166 div()
167 .flex()
168 .when(self.strikethrough, |this| {
169 this.relative().child(
170 div()
171 .absolute()
172 .top_px()
173 .my_auto()
174 .w_full()
175 .h_px()
176 .bg(LabelColor::Hidden.hsla(cx)),
177 )
178 })
179 .children(
180 runs.into_iter()
181 .map(|run| div().text_color(run.color).child(run.text)),
182 )
183 }
184}
185
186/// A run of text that receives the same style.
187struct Run {
188 pub text: String,
189 pub color: Hsla,
190}
191
192#[cfg(feature = "stories")]
193pub use stories::*;
194
195#[cfg(feature = "stories")]
196mod stories {
197 use super::*;
198 use crate::Story;
199 use gpui::{Div, Render};
200
201 pub struct LabelStory;
202
203 impl Render for LabelStory {
204 type Element = Div<Self>;
205
206 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
207 Story::container(cx)
208 .child(Story::title_for::<_, Label>(cx))
209 .child(Story::label(cx, "Default"))
210 .child(Label::new("Hello, world!"))
211 .child(Story::label(cx, "Highlighted"))
212 .child(HighlightedLabel::new(
213 "Hello, world!",
214 vec![0, 1, 2, 7, 8, 12],
215 ))
216 }
217 }
218}