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