label.rs

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