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