Skip to main content

gpui_kit/display/
badge.rs

1use gpui::{
2    App, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, px,
3};
4use gpui_kit_semantics::{NodeSpec, Role, Semantic};
5use gpui_kit_theme::{ActiveTheme, Theme, TypeScale};
6
7use crate::foundation::{Ident, StyledExt};
8
9/// The severity a status surface claims.
10///
11/// A tone is a statement about the state, not a decoration: Success on an idle
12/// thing says the thing succeeded, which is a different and untrue sentence.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum Tone {
15    #[default]
16    Neutral,
17    Accent,
18    Success,
19    Warning,
20    Danger,
21    Info,
22}
23
24impl Tone {
25    /// The name a semantic node publishes, so a test can assert the severity
26    /// a surface reported rather than the color it painted.
27    pub fn name(self) -> &'static str {
28        match self {
29            Self::Neutral => "neutral",
30            Self::Accent => "accent",
31            Self::Success => "success",
32            Self::Warning => "warning",
33            Self::Danger => "danger",
34            Self::Info => "info",
35        }
36    }
37
38    pub(crate) fn color(self, theme: &Theme) -> Hsla {
39        match self {
40            Self::Neutral => theme.colors.text_faint,
41            Self::Accent => theme.colors.accent,
42            Self::Success => theme.colors.success,
43            Self::Warning => theme.colors.warning,
44            Self::Danger => theme.colors.danger,
45            Self::Info => theme.colors.info,
46        }
47    }
48}
49
50/// A compact status label.
51///
52/// A badge only carries a semantic node when the caller gives it an id, so
53/// decorative badges do not add noise to assertion snapshots.
54#[derive(Debug, IntoElement)]
55pub struct Badge {
56    ident: Option<Ident>,
57    label: SharedString,
58    tone: Tone,
59}
60
61impl Badge {
62    pub fn new(label: impl Into<SharedString>) -> Self {
63        Self {
64            ident: None,
65            label: label.into(),
66            tone: Tone::default(),
67        }
68    }
69
70    pub fn id(mut self, ident: impl Into<Ident>) -> Self {
71        self.ident = Some(ident.into());
72        self
73    }
74
75    pub fn tone(mut self, tone: Tone) -> Self {
76        self.tone = tone;
77        self
78    }
79
80    pub fn neutral(self) -> Self {
81        self.tone(Tone::Neutral)
82    }
83
84    pub fn accent(self) -> Self {
85        self.tone(Tone::Accent)
86    }
87
88    pub fn success(self) -> Self {
89        self.tone(Tone::Success)
90    }
91
92    pub fn warning(self) -> Self {
93        self.tone(Tone::Warning)
94    }
95
96    pub fn danger(self) -> Self {
97        self.tone(Tone::Danger)
98    }
99
100    pub fn info(self) -> Self {
101        self.tone(Tone::Info)
102    }
103}
104
105impl RenderOnce for Badge {
106    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
107        let theme = cx.theme().clone();
108        // A badge is the tone itself, as a block. The tint is carried far
109        // enough that the block reads on any surface the badge can land on,
110        // which is what lets the outline go: an outline around a shape that
111        // is already a colour was only ever compensating for a tint too weak
112        // to see.
113        let (foreground, background) = match self.tone {
114            Tone::Neutral => (theme.colors.text_muted, theme.colors.raised),
115            tone => {
116                let color = tone.color(&theme);
117                (color, color.opacity(0.18))
118            }
119        };
120
121        let element = div()
122            .flex_none()
123            .px_token(&theme, gpui_kit_theme::Space::Sm)
124            .py(px(2.0))
125            .rounded_full()
126            .bg(background)
127            .type_scale(&theme, TypeScale::Caption)
128            .text_color(foreground)
129            .child(self.label.clone());
130        match self.ident {
131            Some(ident) => element
132                .semantic_in(
133                    cx,
134                    NodeSpec::new(ident.semantic_id(), Role::Status).text(self.label.clone()),
135                )
136                .into_any_element(),
137            None => element.into_any_element(),
138        }
139    }
140}