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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum TagVariant {
14 #[default]
16 Primary,
17 Secondary,
19 Danger,
21 Success,
23 Warning,
25 Info,
27}
28
29impl TagVariant {
30 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#[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 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 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
96 self.label = Some(label.into());
97 self
98 }
99
100 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
102 self.icon = Some(icon.into());
103 self
104 }
105
106 pub fn variant(mut self, variant: TagVariant) -> Self {
108 self.variant = variant;
109 self
110 }
111
112 pub fn secondary(self) -> Self {
114 self.variant(TagVariant::Secondary)
115 }
116
117 pub fn danger(self) -> Self {
119 self.variant(TagVariant::Danger)
120 }
121
122 pub fn success(self) -> Self {
124 self.variant(TagVariant::Success)
125 }
126
127 pub fn warning(self) -> Self {
129 self.variant(TagVariant::Warning)
130 }
131
132 pub fn info(self) -> Self {
134 self.variant(TagVariant::Info)
135 }
136
137 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}