1use iced::widget::{column, container, row, text};
4use iced::{Background, Border, Color, Element, Length, Shadow};
5
6use crate::icon::{Glyph, themed};
7use crate::theme::{Tokens, mix, radius, space, text_size};
8
9const ICON_SIZE: f32 = 16.0;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum Variant {
14 #[default]
15 Info,
16 Success,
17 Warning,
18 Destructive,
19}
20
21impl Variant {
22 pub const ALL: [Variant; 4] = [
23 Variant::Info,
24 Variant::Success,
25 Variant::Warning,
26 Variant::Destructive,
27 ];
28
29 pub const fn glyph(self) -> Glyph {
31 match self {
32 Variant::Info => crate::lucide!(Info),
33 Variant::Success => crate::lucide!(CircleCheck),
34 Variant::Warning => crate::lucide!(TriangleAlert),
35 Variant::Destructive => crate::lucide!(CircleAlert),
36 }
37 }
38}
39
40#[derive(Debug)]
42pub struct Alert<'a> {
43 title: text::Fragment<'a>,
44 description: Option<text::Fragment<'a>>,
45 icon: Option<Glyph>,
46 variant: Variant,
47 width: Length,
48}
49
50pub fn alert<'a>(title: impl text::IntoFragment<'a>) -> Alert<'a> {
52 Alert {
53 title: title.into_fragment(),
54 description: None,
55 icon: None,
56 variant: Variant::default(),
57 width: Length::Fill,
58 }
59}
60
61impl<'a> Alert<'a> {
62 pub fn variant(mut self, variant: Variant) -> Self {
63 self.variant = variant;
64 self
65 }
66
67 pub fn description(mut self, description: impl text::IntoFragment<'a>) -> Self {
69 self.description = Some(description.into_fragment());
70 self
71 }
72
73 pub fn icon(mut self, glyph: Glyph) -> Self {
75 self.icon = Some(glyph);
76 self
77 }
78
79 pub fn width(mut self, width: impl Into<Length>) -> Self {
81 self.width = width.into();
82 self
83 }
84
85 pub fn glyph(&self) -> Glyph {
87 self.icon.unwrap_or(self.variant.glyph())
88 }
89}
90
91impl<'a, Message: 'a> From<Alert<'a>> for Element<'a, Message> {
92 fn from(alert: Alert<'a>) -> Self {
93 let variant = alert.variant;
94 let glyph = alert.glyph();
95
96 let icon = themed(glyph, ICON_SIZE, 1.0, move |theme| {
97 colours(&Tokens::of(theme), variant).accent
98 });
99
100 let title = text(alert.title)
101 .size(text_size::SM)
102 .style(move |theme| text::Style {
103 color: Some(colours(&Tokens::of(theme), variant).title),
104 });
105
106 let mut body = column![title].spacing(space::XS).width(Length::Fill);
107 if let Some(description) = alert.description {
108 body =
109 body.push(
110 text(description)
111 .size(text_size::SM)
112 .style(move |theme| text::Style {
113 color: Some(colours(&Tokens::of(theme), variant).description),
114 }),
115 );
116 }
117
118 container(row![container(icon).padding([2, 0]), body].spacing(space::MD))
119 .padding([space::MD, space::LG])
120 .width(alert.width)
121 .style(move |theme| style(&Tokens::of(theme), variant))
122 .into()
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq)]
128pub struct Colours {
129 pub background: Color,
130 pub border: Color,
131 pub accent: Color,
133 pub title: Color,
134 pub description: Color,
135}
136
137pub fn colours(tokens: &Tokens, variant: Variant) -> Colours {
139 let accent = match variant {
140 Variant::Info => tokens.foreground,
141 Variant::Success => tokens.success,
142 Variant::Warning => tokens.warning,
143 Variant::Destructive => tokens.destructive,
144 };
145 let tint = if tokens.is_dark { 0.12 } else { 0.06 };
146 let edge = if tokens.is_dark { 0.35 } else { 0.3 };
147 let (background, border) = match variant {
148 Variant::Info => (tokens.background, tokens.border),
149 _ => (
150 mix(tokens.background, accent, tint),
151 mix(tokens.background, accent, edge),
152 ),
153 };
154
155 Colours {
156 background,
157 border,
158 accent,
159 title: mix(accent, tokens.foreground, 0.25),
160 description: match variant {
161 Variant::Info => tokens.muted_foreground,
162 _ => mix(accent, tokens.foreground, 0.55),
163 },
164 }
165}
166
167pub fn style(tokens: &Tokens, variant: Variant) -> container::Style {
169 let colours = colours(tokens, variant);
170 container::Style {
171 text_color: Some(colours.title),
172 background: Some(Background::Color(colours.background)),
173 border: Border {
174 color: colours.border,
175 width: 1.0,
176 radius: radius::LG.into(),
177 },
178 shadow: Shadow::default(),
179 snap: true,
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::theme::{dark, light};
187
188 #[test]
189 fn default_builder_is_info_fill_width_with_variant_icon() {
190 let a = alert("Heads up");
191 assert_eq!(a.variant, Variant::Info);
192 assert_eq!(a.width, Length::Fill);
193 assert!(a.description.is_none());
194 assert_eq!(a.glyph().name(), "info");
195 }
196
197 #[test]
198 fn each_variant_has_its_own_icon() {
199 let mut names: Vec<_> = Variant::ALL.map(|v| v.glyph().name()).to_vec();
200 names.sort_unstable();
201 names.dedup();
202 assert_eq!(names.len(), Variant::ALL.len());
203 }
204
205 #[test]
206 fn custom_icon_overrides_variant_icon() {
207 let a = alert("Saved")
208 .variant(Variant::Success)
209 .icon(crate::lucide!(Save));
210 assert_eq!(a.glyph().name(), "save");
211 }
212
213 #[test]
214 fn status_variants_tint_the_surface() {
215 for theme in [light(), dark()] {
216 let tokens = Tokens::of(&theme);
217 for variant in [Variant::Success, Variant::Warning, Variant::Destructive] {
218 let colours = colours(&tokens, variant);
219 assert_ne!(colours.background, tokens.background, "{variant:?}");
220 assert_ne!(colours.border, tokens.border, "{variant:?}");
221 }
222 }
223 }
224
225 #[test]
226 fn info_uses_neutral_surface() {
227 for theme in [light(), dark()] {
228 let tokens = Tokens::of(&theme);
229 let colours = colours(&tokens, Variant::Info);
230 assert_eq!(colours.background, tokens.background);
231 assert_eq!(colours.accent, tokens.foreground);
232 }
233 }
234
235 #[test]
236 fn text_is_distinct_from_surface_in_every_variant() {
237 for theme in [light(), dark()] {
238 let tokens = Tokens::of(&theme);
239 for variant in Variant::ALL {
240 let colours = colours(&tokens, variant);
241 assert_ne!(colours.title, colours.background, "{variant:?}");
242 assert_ne!(colours.description, colours.background, "{variant:?}");
243 assert_eq!(style(&tokens, variant).border.width, 1.0);
244 }
245 }
246 }
247}