1use std::marker::PhantomData;
2
3use gpui2::{relative, 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 color = ThemeColor::new(cx);
26 // TODO: Remove
27 let theme = theme(cx);
28
29 match self {
30 Self::Default => color.text,
31 Self::Muted => color.text_muted,
32 Self::Created => theme.middle.positive.default.foreground,
33 Self::Modified => theme.middle.warning.default.foreground,
34 Self::Deleted => theme.middle.negative.default.foreground,
35 Self::Disabled => color.text_disabled,
36 Self::Hidden => theme.middle.variant.default.foreground,
37 Self::Placeholder => color.text_placeholder,
38 Self::Accent => 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 let color = ThemeColor::new(cx);
88
89 div()
90 .when(self.strikethrough, |this| {
91 this.relative().child(
92 div()
93 .absolute()
94 .top_px()
95 .my_auto()
96 .w_full()
97 .h_px()
98 .bg(LabelColor::Hidden.hsla(cx)),
99 )
100 })
101 .text_size(ui_size(cx, 1.))
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(Element)]
111pub struct HighlightedLabel<S: 'static + Send + Sync> {
112 state_type: PhantomData<S>,
113 label: SharedString,
114 color: LabelColor,
115 highlight_indices: Vec<usize>,
116 strikethrough: bool,
117}
118
119impl<S: 'static + Send + Sync> HighlightedLabel<S> {
120 pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
121 Self {
122 state_type: PhantomData,
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(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
141 let color = ThemeColor::new(cx);
142
143 let highlight_color = color.text_accent;
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(
195 runs.into_iter()
196 .map(|run| div().text_color(run.color).child(run.text)),
197 )
198 }
199}
200
201/// A run of text that receives the same style.
202struct Run {
203 pub text: String,
204 pub color: Hsla,
205}
206
207#[cfg(feature = "stories")]
208pub use stories::*;
209
210#[cfg(feature = "stories")]
211mod stories {
212 use crate::Story;
213
214 use super::*;
215
216 #[derive(Element)]
217 pub struct LabelStory<S: 'static + Send + Sync> {
218 state_type: PhantomData<S>,
219 }
220
221 impl<S: 'static + Send + Sync> LabelStory<S> {
222 pub fn new() -> Self {
223 Self {
224 state_type: PhantomData,
225 }
226 }
227
228 fn render(
229 &mut self,
230 _view: &mut S,
231 cx: &mut ViewContext<S>,
232 ) -> impl Element<ViewState = S> {
233 Story::container(cx)
234 .child(Story::title_for::<_, Label<S>>(cx))
235 .child(Story::label(cx, "Default"))
236 .child(Label::new("Hello, world!"))
237 .child(Story::label(cx, "Highlighted"))
238 .child(HighlightedLabel::new(
239 "Hello, world!",
240 vec![0, 1, 2, 7, 8, 12],
241 ))
242 }
243 }
244}