Skip to main content

glassy_ui/
badge.rs

1//! Paper hex+alpha is grouped as `RRGGBB_AA`.
2#![allow(clippy::unusual_byte_groupings)]
3
4use crate::motion::StyledSlot;
5use crate::theme::{ActiveTheme, Theme};
6use gpui::{
7    div, prelude::*, px, App, FontWeight, IntoElement, RenderOnce, SharedString, StyleRefinement,
8    Styled, Window,
9};
10
11use crate::button::ButtonVariant;
12use crate::chrome::{box_shadow, button_chrome, ButtonChrome};
13
14const BADGE_HEIGHT: f32 = 22.0;
15const BADGE_RADIUS: f32 = 6.0;
16const BADGE_PAD_X: f32 = 8.0;
17
18/// Visual treatment from Paper `Glassy UI` → Badges.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum BadgeVariant {
21    #[default]
22    Default,
23    Muted,
24    Destructive,
25}
26
27impl BadgeVariant {
28    fn button_variant(self) -> ButtonVariant {
29        match self {
30            Self::Default => ButtonVariant::Primary,
31            Self::Muted => ButtonVariant::Ghost,
32            Self::Destructive => ButtonVariant::Destructive,
33        }
34    }
35}
36
37fn badge_chrome(theme: Theme, variant: BadgeVariant) -> ButtonChrome {
38    let mut chrome = button_chrome(theme, variant.button_variant());
39    if variant == BadgeVariant::Muted {
40        chrome.fg = theme.muted_fg();
41    }
42    chrome
43}
44
45/// A compact, non-interactive status or count chip.
46#[derive(IntoElement)]
47pub struct Badge {
48    label: SharedString,
49    variant: BadgeVariant,
50    style: StyleRefinement,
51}
52
53impl Badge {
54    pub fn new(label: impl Into<SharedString>) -> Self {
55        Self {
56            label: label.into(),
57            variant: BadgeVariant::Default,
58            style: StyleRefinement::default(),
59        }
60    }
61
62    pub fn variant(mut self, variant: BadgeVariant) -> Self {
63        self.variant = variant;
64        self
65    }
66}
67
68impl Styled for Badge {
69    fn style(&mut self) -> &mut StyleRefinement {
70        &mut self.style
71    }
72}
73
74impl RenderOnce for Badge {
75    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
76        let theme = cx.theme();
77        let chrome = badge_chrome(theme, self.variant);
78        let mut shadows = vec![box_shadow(0., 1., chrome.inset, 0., 0.)];
79        if chrome.shadow_blur > 0.0 {
80            shadows.push(box_shadow(
81                0.,
82                chrome.shadow_y,
83                chrome.shadow,
84                chrome.shadow_blur,
85                0.,
86            ));
87        }
88
89        div()
90            .flex()
91            .items_center()
92            .justify_center()
93            .h(px(BADGE_HEIGHT))
94            .flex_shrink_0()
95            .px(px(BADGE_PAD_X))
96            .rounded(px(BADGE_RADIUS))
97            .border_1()
98            .border_color(chrome.border)
99            .bg(chrome.bg)
100            .shadow(shadows)
101            .cursor_default()
102            .refine_style(&self.style)
103            .child(
104                div()
105                    .font_family(theme.font_family)
106                    .font_weight(FontWeight::MEDIUM)
107                    .text_size(px(12.))
108                    .line_height(px(16.))
109                    .text_color(chrome.fg)
110                    .child(self.label),
111            )
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::theme::{paint, rgb};
119
120    #[test]
121    fn metrics_match_paper() {
122        assert_eq!(BADGE_HEIGHT, 22.0);
123        assert_eq!(BADGE_RADIUS, 6.0);
124        assert_eq!(BADGE_PAD_X, 8.0);
125    }
126
127    #[test]
128    fn light_variants_match_paper() {
129        let theme = Theme::light();
130        let default = badge_chrome(theme, BadgeVariant::Default);
131        assert_eq!(default.bg, paint(0x18181B_B8));
132        assert_eq!(default.border, paint(0xFFFFFF_29));
133        assert_eq!(default.fg, rgb(0xFAFAFA));
134
135        let muted = badge_chrome(theme, BadgeVariant::Muted);
136        assert_eq!(muted.bg, paint(0xFFFFFF_29));
137        assert_eq!(muted.border, paint(0xFFFFFF_47));
138        assert_eq!(muted.fg, rgb(0xA1A1AA));
139
140        let destructive = badge_chrome(theme, BadgeVariant::Destructive);
141        assert_eq!(destructive.bg, paint(0xDC2626_C7));
142        assert_eq!(destructive.border, paint(0xFFFFFF_47));
143        assert_eq!(destructive.fg, rgb(0xFFFFFF));
144    }
145
146    #[test]
147    fn dark_variants_match_paper() {
148        let theme = Theme::dark();
149        let default = badge_chrome(theme, BadgeVariant::Default);
150        assert_eq!(default.bg, paint(0xFFFFFF_29));
151        assert_eq!(default.border, paint(0xFFFFFF_38));
152        assert_eq!(default.fg, rgb(0xFAFAFA));
153
154        let muted = badge_chrome(theme, BadgeVariant::Muted);
155        assert_eq!(muted.bg, paint(0xFFFFFF_08));
156        assert_eq!(muted.border, paint(0xFFFFFF_0F));
157        assert_eq!(muted.fg, rgb(0x71717A));
158
159        let destructive = badge_chrome(theme, BadgeVariant::Destructive);
160        assert_eq!(destructive.bg, paint(0xDC2626_9E));
161        assert_eq!(destructive.border, paint(0xFFFFFF_29));
162        assert_eq!(destructive.fg, rgb(0xFAFAFA));
163    }
164}