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