1use std::marker::PhantomData;
2
3use gpui3::{Hsla, WindowContext};
4use smallvec::SmallVec;
5
6use crate::prelude::*;
7use crate::theme::theme;
8
9#[derive(Default, PartialEq, Copy, Clone)]
10pub enum LabelColor {
11 #[default]
12 Default,
13 Muted,
14 Created,
15 Modified,
16 Deleted,
17 Disabled,
18 Hidden,
19 Placeholder,
20 Accent,
21}
22
23impl LabelColor {
24 pub fn hsla(&self, cx: &WindowContext) -> Hsla {
25 let theme = theme(cx);
26
27 match self {
28 Self::Default => theme.middle.base.default.foreground,
29 Self::Muted => theme.middle.variant.default.foreground,
30 Self::Created => theme.middle.positive.default.foreground,
31 Self::Modified => theme.middle.warning.default.foreground,
32 Self::Deleted => theme.middle.negative.default.foreground,
33 Self::Disabled => theme.middle.base.disabled.foreground,
34 Self::Hidden => theme.middle.variant.default.foreground,
35 Self::Placeholder => theme.middle.base.disabled.foreground,
36 Self::Accent => theme.middle.accent.default.foreground,
37 }
38 }
39}
40
41#[derive(Default, PartialEq, Copy, Clone)]
42pub enum LabelSize {
43 #[default]
44 Default,
45 Small,
46}
47
48#[derive(Element, Clone)]
49pub struct Label<S: 'static + Send + Sync + Clone> {
50 state_type: PhantomData<S>,
51 label: SharedString,
52 color: LabelColor,
53 size: LabelSize,
54 strikethrough: bool,
55}
56
57impl<S: 'static + Send + Sync + Clone> Label<S> {
58 pub fn new(label: impl Into<SharedString>) -> Self {
59 Self {
60 state_type: PhantomData,
61 label: label.into(),
62 color: LabelColor::Default,
63 size: LabelSize::Default,
64 strikethrough: false,
65 }
66 }
67
68 pub fn color(mut self, color: LabelColor) -> Self {
69 self.color = color;
70 self
71 }
72
73 pub fn size(mut self, size: LabelSize) -> Self {
74 self.size = size;
75 self
76 }
77
78 pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
79 self.strikethrough = strikethrough;
80 self
81 }
82
83 fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
84 let theme = theme(cx);
85
86 div()
87 .when(self.strikethrough, |this| {
88 this.relative().child(
89 div()
90 .absolute()
91 .top_px()
92 .my_auto()
93 .w_full()
94 .h_px()
95 .bg(LabelColor::Hidden.hsla(cx)),
96 )
97 })
98 .text_color(self.color.hsla(cx))
99 .child(self.label.clone())
100 }
101}
102
103#[derive(Element, Clone)]
104pub struct HighlightedLabel<S: 'static + Send + Sync + Clone> {
105 state_type: PhantomData<S>,
106 label: SharedString,
107 color: LabelColor,
108 size: LabelSize,
109 highlight_indices: Vec<usize>,
110 strikethrough: bool,
111}
112
113impl<S: 'static + Send + Sync + Clone> HighlightedLabel<S> {
114 pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
115 Self {
116 state_type: PhantomData,
117 label: label.into(),
118 color: LabelColor::Default,
119 size: LabelSize::Default,
120 highlight_indices,
121 strikethrough: false,
122 }
123 }
124
125 pub fn color(mut self, color: LabelColor) -> Self {
126 self.color = color;
127 self
128 }
129
130 pub fn size(mut self, size: LabelSize) -> Self {
131 self.size = size;
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(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
141 let theme = theme(cx);
142
143 let highlight_color = theme.lowest.accent.default.foreground;
144
145 let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
146
147 let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
148
149 for (char_ix, char) in self.label.char_indices() {
150 let mut color = self.color.hsla(cx);
151
152 if let Some(highlight_ix) = highlight_indices.peek() {
153 if char_ix == *highlight_ix {
154 color = highlight_color;
155
156 highlight_indices.next();
157 }
158 }
159
160 let last_run = runs.last_mut();
161
162 let start_new_run = if let Some(last_run) = last_run {
163 if color == last_run.color {
164 last_run.text.push(char);
165 false
166 } else {
167 true
168 }
169 } else {
170 true
171 };
172
173 if start_new_run {
174 runs.push(Run {
175 text: char.to_string(),
176 color,
177 });
178 }
179 }
180
181 div()
182 .flex()
183 .when(self.strikethrough, |this| {
184 this.relative().child(
185 div()
186 .absolute()
187 .top_px()
188 .my_auto()
189 .w_full()
190 .h_px()
191 .bg(LabelColor::Hidden.hsla(cx)),
192 )
193 })
194 .children(runs.into_iter().map(|run| {
195 let mut div = div();
196
197 if self.size == LabelSize::Small {
198 div = div.text_xs();
199 } else {
200 div = div.text_sm();
201 }
202
203 div.text_color(run.color).child(run.text)
204 }))
205 }
206}
207
208/// A run of text that receives the same style.
209struct Run {
210 pub text: String,
211 pub color: Hsla,
212}
213
214#[cfg(feature = "stories")]
215pub use stories::*;
216
217#[cfg(feature = "stories")]
218mod stories {
219 use crate::Story;
220
221 use super::*;
222
223 #[derive(Element)]
224 pub struct LabelStory<S: 'static + Send + Sync + Clone> {
225 state_type: PhantomData<S>,
226 }
227
228 impl<S: 'static + Send + Sync + Clone> LabelStory<S> {
229 pub fn new() -> Self {
230 Self {
231 state_type: PhantomData,
232 }
233 }
234
235 fn render(
236 &mut self,
237 _view: &mut S,
238 cx: &mut ViewContext<S>,
239 ) -> impl Element<ViewState = S> {
240 Story::container(cx)
241 .child(Story::title_for::<_, Label<S>>(cx))
242 .child(Story::label(cx, "Default"))
243 .child(Label::new("Hello, world!"))
244 .child(Story::label(cx, "Highlighted"))
245 .child(HighlightedLabel::new(
246 "Hello, world!",
247 vec![0, 1, 2, 7, 8, 12],
248 ))
249 }
250 }
251}