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