label.rs

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