label.rs

  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}