1use iced::widget::{container, row, text};
4use iced::{Alignment, Background, Border, Color, Element, Padding, Shadow};
5
6use crate::icon::{Glyph, tinted};
7use crate::theme::{Tokens, on, radius, text_size};
8
9const ICON_SIZE: f32 = 12.0;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum Variant {
14 #[default]
15 Default,
16 Secondary,
17 Outline,
18 Destructive,
19 Success,
20 Warning,
21}
22
23impl Variant {
24 pub const ALL: [Variant; 6] = [
25 Variant::Default,
26 Variant::Secondary,
27 Variant::Outline,
28 Variant::Destructive,
29 Variant::Success,
30 Variant::Warning,
31 ];
32}
33
34#[derive(Debug)]
36pub struct Badge<'a> {
37 label: text::Fragment<'a>,
38 icon: Option<Glyph>,
39 variant: Variant,
40}
41
42pub fn badge<'a>(label: impl text::IntoFragment<'a>) -> Badge<'a> {
44 Badge {
45 label: label.into_fragment(),
46 icon: None,
47 variant: Variant::default(),
48 }
49}
50
51impl Badge<'_> {
52 pub fn variant(mut self, variant: Variant) -> Self {
53 self.variant = variant;
54 self
55 }
56
57 pub fn icon(mut self, glyph: Glyph) -> Self {
59 self.icon = Some(glyph);
60 self
61 }
62}
63
64impl<'a, Message: 'a> From<Badge<'a>> for Element<'a, Message> {
65 fn from(badge: Badge<'a>) -> Self {
66 let variant = badge.variant;
67
68 let mut content = row![].spacing(4).align_y(Alignment::Center);
69 if let Some(glyph) = badge.icon {
70 content = content.push(tinted(glyph, ICON_SIZE, None).style(move |theme, _| {
71 iced::widget::svg::Style {
72 color: Some(colours(&Tokens::of(theme), variant).foreground),
73 }
74 }));
75 }
76 content = content.push(text(badge.label).size(text_size::XS));
77
78 container(content)
79 .padding(Padding::from([2.0, 10.0]))
80 .style(move |theme| style(&Tokens::of(theme), variant))
81 .into()
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct Colours {
88 pub background: Option<Color>,
89 pub foreground: Color,
90 pub border: Option<Color>,
91}
92
93pub fn colours(tokens: &Tokens, variant: Variant) -> Colours {
95 let filled = |background: Color, foreground: Color| Colours {
96 background: Some(background),
97 foreground,
98 border: None,
99 };
100
101 match variant {
102 Variant::Default => filled(tokens.primary, tokens.primary_foreground),
103 Variant::Secondary => filled(tokens.secondary, tokens.secondary_foreground),
104 Variant::Outline => Colours {
105 background: None,
106 foreground: tokens.foreground,
107 border: Some(tokens.border),
108 },
109 Variant::Destructive => filled(tokens.destructive, tokens.destructive_foreground),
110 Variant::Success => filled(tokens.success, on(tokens.success)),
111 Variant::Warning => filled(tokens.warning, on(tokens.warning)),
112 }
113}
114
115pub fn style(tokens: &Tokens, variant: Variant) -> container::Style {
117 let colours = colours(tokens, variant);
118 container::Style {
119 text_color: Some(colours.foreground),
120 background: colours.background.map(Background::Color),
121 border: Border {
122 color: colours.border.unwrap_or(Color::TRANSPARENT),
123 width: if colours.border.is_some() { 1.0 } else { 0.0 },
124 radius: radius::FULL.into(),
125 },
126 shadow: Shadow::default(),
127 snap: true,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::theme::{dark, light};
135
136 #[test]
137 fn default_builder_is_default_variant_without_icon() {
138 let b = badge("New");
139 assert_eq!(b.variant, Variant::Default);
140 assert!(b.icon.is_none());
141 }
142
143 #[test]
144 fn icon_and_variant_are_kept() {
145 let b = badge("Live")
146 .icon(crate::lucide!(CircleDot))
147 .variant(Variant::Success);
148 assert_eq!(b.variant, Variant::Success);
149 assert_eq!(b.icon.map(Glyph::name), Some("circle-dot"));
150 }
151
152 #[test]
153 fn only_outline_has_a_border_and_no_fill() {
154 for theme in [light(), dark()] {
155 let tokens = Tokens::of(&theme);
156 for variant in Variant::ALL {
157 let style = style(&tokens, variant);
158 let outline = variant == Variant::Outline;
159 assert_eq!(style.border.width > 0.0, outline, "{variant:?}");
160 assert_eq!(style.background.is_none(), outline, "{variant:?}");
161 }
162 }
163 }
164
165 #[test]
166 fn foreground_differs_from_background_in_every_variant() {
167 for theme in [light(), dark()] {
168 let tokens = Tokens::of(&theme);
169 for variant in Variant::ALL {
170 let colours = colours(&tokens, variant);
171 let behind = colours.background.unwrap_or(tokens.background);
172 assert_ne!(colours.foreground, behind, "{variant:?}");
173 }
174 }
175 }
176
177 #[test]
178 fn status_variants_use_status_tokens() {
179 for theme in [light(), dark()] {
180 let tokens = Tokens::of(&theme);
181 assert_eq!(
182 colours(&tokens, Variant::Success).background,
183 Some(tokens.success)
184 );
185 assert_eq!(
186 colours(&tokens, Variant::Warning).background,
187 Some(tokens.warning)
188 );
189 assert_eq!(
190 colours(&tokens, Variant::Destructive).background,
191 Some(tokens.destructive)
192 );
193 }
194 }
195
196 #[test]
197 fn badges_are_pills() {
198 let tokens = Tokens::of(&light());
199 for variant in Variant::ALL {
200 assert_eq!(style(&tokens, variant).border.radius, radius::FULL.into());
201 }
202 }
203}