Skip to main content

gpui_cn/
tag.rs

1use gpui_kit::base::StyledExt as _;
2use gpui_kit::{
3    AnyElement, App, ElementId, FontWeight, Hsla, InteractiveElement as _, IntoElement,
4    ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, TestSupportExt as _, Window,
5    div, prelude::FluentBuilder as _,
6};
7
8use crate::{ActiveTheme as _, Icon, theme::ThemeTokens};
9
10/// The color of a [`Tag`], after gpui-kit's tag variants.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum TagVariant {
14    /// The primary fill with the primary foreground, shadcn's default badge.
15    #[default]
16    Primary,
17    /// The selected step with the foreground, the fill an avatar uses.
18    Secondary,
19    /// The destructive color.
20    Danger,
21    /// The success color.
22    Success,
23    /// The warning color.
24    Warning,
25    /// The info color.
26    Info,
27}
28
29impl TagVariant {
30    /// The fill, the text color, and the border.
31    ///
32    /// A status color paints on its theme tint with the color as text. Outline drops the fill: a neutral tag
33    /// takes the input hairline, a status tag its own color.
34    fn colors(self, outline: bool, theme: &ThemeTokens) -> (Hsla, Hsla, Hsla) {
35        let clear = gpui_kit::transparent_black();
36        let status = match self {
37            Self::Primary | Self::Secondary => None,
38            Self::Danger => Some((theme.destructive(), theme.destructive_tint)),
39            Self::Success => Some((theme.success, theme.success_tint)),
40            Self::Warning => Some((theme.warning, theme.warning_tint)),
41            Self::Info => Some((theme.info, theme.info_tint)),
42        };
43        match (status, outline) {
44            (Some((color, _)), true) => (clear, color, color),
45            (Some((color, tint)), false) => (tint, color, clear),
46            (None, true) => (clear, theme.foreground(), theme.input()),
47            (None, false) if self == Self::Primary => {
48                (theme.primary(), theme.primary_foreground(), clear)
49            }
50            (None, false) => (theme.selected, theme.foreground(), clear),
51        }
52    }
53}
54
55/// A small pill with a label and an optional icon, for a status or a
56/// category. gpui-kit's `Tag`, drawn as shadcn's badge.
57///
58/// It is 20px tall on the rem scale with `px_2` at the sides, `text-xs`
59/// at medium weight, and a 12px icon. It is not interactive; put it in a
60/// button to make it one. For a count or a dot on another element, use
61/// [`Badge`](crate::Badge).
62///
63/// ```
64/// use gpui_cn::{Tag, gpui_kit::assets::IconName};
65///
66/// let _ = Tag::new("status").label("Verified").icon(IconName::CircleCheck).success();
67/// let _ = Tag::new("draft").label("Draft").secondary().outline();
68/// ```
69#[derive(IntoElement)]
70pub struct Tag {
71    id: ElementId,
72    style: StyleRefinement,
73    variant: TagVariant,
74    outline: bool,
75    icon: Option<Icon>,
76    label: Option<SharedString>,
77    children: Vec<AnyElement>,
78}
79
80impl Tag {
81    /// A tag with a stable id.
82    pub fn new(id: impl Into<ElementId>) -> Self {
83        Self {
84            id: id.into(),
85            style: StyleRefinement::default(),
86            variant: TagVariant::default(),
87            outline: false,
88            icon: None,
89            label: None,
90            children: Vec::new(),
91        }
92    }
93
94    /// The text.
95    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
96        self.label = Some(label.into());
97        self
98    }
99
100    /// The icon before the text.
101    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
102        self.icon = Some(icon.into());
103        self
104    }
105
106    /// The color.
107    pub fn variant(mut self, variant: TagVariant) -> Self {
108        self.variant = variant;
109        self
110    }
111
112    /// [`TagVariant::Secondary`].
113    pub fn secondary(self) -> Self {
114        self.variant(TagVariant::Secondary)
115    }
116
117    /// [`TagVariant::Danger`].
118    pub fn danger(self) -> Self {
119        self.variant(TagVariant::Danger)
120    }
121
122    /// [`TagVariant::Success`].
123    pub fn success(self) -> Self {
124        self.variant(TagVariant::Success)
125    }
126
127    /// [`TagVariant::Warning`].
128    pub fn warning(self) -> Self {
129        self.variant(TagVariant::Warning)
130    }
131
132    /// [`TagVariant::Info`].
133    pub fn info(self) -> Self {
134        self.variant(TagVariant::Info)
135    }
136
137    /// Draws a hairline and no fill.
138    pub fn outline(mut self) -> Self {
139        self.outline = true;
140        self
141    }
142}
143
144impl Styled for Tag {
145    fn style(&mut self) -> &mut StyleRefinement {
146        &mut self.style
147    }
148}
149
150impl ParentElement for Tag {
151    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
152        self.children.extend(elements);
153    }
154}
155
156impl RenderOnce for Tag {
157    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
158        let theme = cx.theme();
159        let (fill, ink, border) = self.variant.colors(self.outline, theme);
160        let (height, icon_size) = (theme.metrics.tag_height, theme.metrics.tag_icon);
161        let text = theme.base.typography.xs;
162
163        div()
164            .id(self.id)
165            .test_support()
166            .flex_none()
167            .flex()
168            .items_center()
169            .justify_center()
170            .gap_1()
171            .h(height)
172            .px_2()
173            .overflow_hidden()
174            .whitespace_nowrap()
175            .rounded_full()
176            .border_1()
177            .border_color(border)
178            .bg(fill)
179            .text_color(ink)
180            .text_size(text.size)
181            .line_height(text.line_height)
182            .font_weight(FontWeight::MEDIUM)
183            .when_some(self.icon, |this, icon| this.child(icon.size(icon_size)))
184            .when_some(self.label, |this, label| this.child(label))
185            .children(self.children)
186            .refine_style(&self.style)
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::theme::{ThemeConfig, to_hex};
194    use gpui_kit::base::ThemeAppearance;
195
196    #[test]
197    fn variants_paint_from_the_theme() {
198        let dark = crate::theme::test_tokens(&ThemeConfig::dark(), ThemeAppearance::Dark);
199        let light = crate::theme::test_tokens(&ThemeConfig::light(), ThemeAppearance::Light);
200        let hex =
201            |(fill, ink, border): (Hsla, Hsla, Hsla)| [to_hex(fill), to_hex(ink), to_hex(border)];
202        assert_eq!(
203            hex(TagVariant::Primary.colors(false, &dark)),
204            ["#dfdfdf", "#2d2d2d", "#00000000"].map(String::from),
205        );
206        assert_eq!(
207            hex(TagVariant::Secondary.colors(false, &light)),
208            ["#e8e8e8", "#1a1c1f", "#00000000"].map(String::from),
209        );
210        assert_eq!(
211            hex(TagVariant::Success.colors(false, &dark)),
212            ["#25372a", "#40c977", "#00000000"].map(String::from),
213        );
214        assert_eq!(
215            hex(TagVariant::Info.colors(true, &light)),
216            ["#00000000", "#339cff", "#339cff"].map(String::from),
217        );
218        assert_eq!(
219            hex(TagVariant::Primary.colors(true, &light)),
220            ["#00000000", "#1a1c1f", "#e5e5e6"].map(String::from),
221        );
222    }
223}