1use iced::advanced::text::Wrapping;
2use iced::alignment::{Horizontal, Vertical};
3use iced::border::Border;
4use iced::font::Weight;
5use iced::widget::{column, container, row, text};
6use iced::{Alignment, Background, Color, Element, Font, Length};
7
8use crate::theme::Theme;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
11pub enum EmptyMediaVariant {
12 #[default]
13 Default,
14 Icon,
15}
16
17#[derive(Clone, Debug)]
18pub struct EmptyProps<'a> {
19 pub title: &'a str,
20 pub description: Option<&'a str>,
21 pub icon: Option<&'a str>,
22}
23
24impl<'a> EmptyProps<'a> {
25 pub fn new(title: &'a str) -> Self {
26 Self {
27 title,
28 description: None,
29 icon: None,
30 }
31 }
32
33 pub fn description(mut self, description: &'a str) -> Self {
34 self.description = Some(description);
35 self
36 }
37
38 pub fn icon(mut self, icon: &'a str) -> Self {
39 self.icon = Some(icon);
40 self
41 }
42}
43
44#[derive(Clone, Copy, Debug)]
45pub struct EmptyRootProps {
46 pub gap: f32,
47 pub padding: f32,
48 pub max_width: f32,
49 pub min_height: f32,
50 pub bordered: bool,
51 pub dashed: bool,
52 pub background: Option<Color>,
53}
54
55impl Default for EmptyRootProps {
56 fn default() -> Self {
57 Self {
58 gap: 0.0,
59 padding: 0.0,
60 max_width: 0.0,
61 min_height: 0.0,
62 bordered: false,
63 dashed: true,
64 background: None,
65 }
66 }
67}
68
69impl EmptyRootProps {
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn gap(mut self, gap: f32) -> Self {
75 self.gap = gap;
76 self
77 }
78
79 pub fn padding(mut self, padding: f32) -> Self {
80 self.padding = padding;
81 self
82 }
83
84 pub fn max_width(mut self, max_width: f32) -> Self {
85 self.max_width = max_width;
86 self
87 }
88
89 pub fn min_height(mut self, min_height: f32) -> Self {
90 self.min_height = min_height;
91 self
92 }
93
94 pub fn bordered(mut self, bordered: bool) -> Self {
95 self.bordered = bordered;
96 self
97 }
98
99 pub fn dashed(mut self, dashed: bool) -> Self {
100 self.dashed = dashed;
101 self
102 }
103
104 pub fn background(mut self, background: Color) -> Self {
105 self.background = Some(background);
106 self
107 }
108}
109
110#[derive(Clone, Copy, Debug)]
111pub struct EmptyHeaderProps {
112 pub gap: f32,
113 pub max_width: f32,
114}
115
116impl Default for EmptyHeaderProps {
117 fn default() -> Self {
118 Self {
119 gap: 0.0,
120 max_width: 0.0,
121 }
122 }
123}
124
125impl EmptyHeaderProps {
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 pub fn gap(mut self, gap: f32) -> Self {
131 self.gap = gap;
132 self
133 }
134
135 pub fn max_width(mut self, max_width: f32) -> Self {
136 self.max_width = max_width;
137 self
138 }
139}
140
141#[derive(Clone, Copy, Debug)]
142pub struct EmptyMediaProps {
143 pub variant: EmptyMediaVariant,
144 pub size: f32,
145 pub icon_size: f32,
146}
147
148impl Default for EmptyMediaProps {
149 fn default() -> Self {
150 Self {
151 variant: EmptyMediaVariant::Default,
152 size: 0.0,
153 icon_size: 0.0,
154 }
155 }
156}
157
158impl EmptyMediaProps {
159 pub fn new() -> Self {
160 Self::default()
161 }
162
163 pub fn variant(mut self, variant: EmptyMediaVariant) -> Self {
164 self.variant = variant;
165 self
166 }
167
168 pub fn size(mut self, size: f32) -> Self {
169 self.size = size;
170 self
171 }
172
173 pub fn icon_size(mut self, icon_size: f32) -> Self {
174 self.icon_size = icon_size;
175 self
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
180pub struct EmptyTitleProps {
181 pub size: f32,
182}
183
184impl Default for EmptyTitleProps {
185 fn default() -> Self {
186 Self { size: 0.0 }
187 }
188}
189
190impl EmptyTitleProps {
191 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn size(mut self, size: f32) -> Self {
196 self.size = size;
197 self
198 }
199}
200
201#[derive(Clone, Copy, Debug)]
202pub struct EmptyDescriptionProps {
203 pub size: f32,
204 pub max_width: f32,
205}
206
207impl Default for EmptyDescriptionProps {
208 fn default() -> Self {
209 Self {
210 size: 0.0,
211 max_width: 0.0,
212 }
213 }
214}
215
216impl EmptyDescriptionProps {
217 pub fn new() -> Self {
218 Self::default()
219 }
220
221 pub fn size(mut self, size: f32) -> Self {
222 self.size = size;
223 self
224 }
225
226 pub fn max_width(mut self, max_width: f32) -> Self {
227 self.max_width = max_width;
228 self
229 }
230}
231
232#[derive(Clone, Copy, Debug)]
233pub struct EmptyContentProps {
234 pub gap: f32,
235 pub max_width: f32,
236}
237
238impl Default for EmptyContentProps {
239 fn default() -> Self {
240 Self {
241 gap: 0.0,
242 max_width: 0.0,
243 }
244 }
245}
246
247impl EmptyContentProps {
248 pub fn new() -> Self {
249 Self::default()
250 }
251
252 pub fn gap(mut self, gap: f32) -> Self {
253 self.gap = gap;
254 self
255 }
256
257 pub fn max_width(mut self, max_width: f32) -> Self {
258 self.max_width = max_width;
259 self
260 }
261}
262
263pub fn empty_root<'a, Message: 'a>(
264 content: impl Into<Element<'a, Message>>,
265 props: EmptyRootProps,
266 theme: &Theme,
267) -> Element<'a, Message> {
268 let gap = if props.gap > 0.0 {
269 props.gap
270 } else {
271 theme.styles.empty.root_gap
272 };
273 let padding = if props.padding > 0.0 {
274 props.padding
275 } else {
276 theme.styles.empty.root_padding
277 };
278 let max_width = if props.max_width > 0.0 {
279 props.max_width
280 } else {
281 theme.styles.empty.root_max_width
282 };
283 let border_color = if props.dashed {
284 apply_opacity(theme.palette.border, 0.9)
285 } else {
286 theme.palette.border
287 };
288 let background = props.background;
289 let radius = theme.radius.lg;
290 let min_height = if props.min_height > 0.0 {
291 props.min_height
292 } else {
293 theme.styles.empty.root_min_height
294 };
295
296 container(column![content.into()].spacing(gap).width(Length::Fill))
297 .padding(padding)
298 .width(Length::Fill)
299 .max_width(max_width)
300 .center_x(Length::Fill)
301 .style(move |_t| iced::widget::container::Style {
302 background: background.map(Background::Color),
303 border: Border {
304 color: if props.bordered {
305 border_color
306 } else {
307 Color::TRANSPARENT
308 },
309 width: if props.bordered { 1.0 } else { 0.0 },
310 radius: radius.into(),
311 },
312 ..Default::default()
313 })
314 .height(if min_height > 0.0 {
315 Length::Fixed(min_height)
316 } else {
317 Length::Shrink
318 })
319 .into()
320}
321
322pub fn empty_header<'a, Message: 'a>(
323 items: Vec<Element<'a, Message>>,
324 props: EmptyHeaderProps,
325 theme: &Theme,
326) -> Element<'a, Message> {
327 let gap = if props.gap > 0.0 {
328 props.gap
329 } else {
330 theme.styles.empty.header_gap
331 };
332 let max_width = if props.max_width > 0.0 {
333 props.max_width
334 } else {
335 theme.styles.empty.header_max_width
336 };
337
338 container(
339 column(items)
340 .spacing(gap)
341 .align_x(Alignment::Center)
342 .width(Length::Shrink),
343 )
344 .width(Length::Fill)
345 .center_x(Length::Fill)
346 .max_width(max_width)
347 .into()
348}
349
350pub fn empty_media<'a, Message: 'a>(
351 content: impl Into<Element<'a, Message>>,
352 props: EmptyMediaProps,
353 theme: &Theme,
354) -> Element<'a, Message> {
355 let size = if props.size > 0.0 {
356 props.size
357 } else {
358 theme.styles.empty.media_size
359 };
360 let icon_size = if props.icon_size > 0.0 {
361 props.icon_size
362 } else {
363 theme.styles.empty.media_icon_size
364 };
365 let background = match props.variant {
366 EmptyMediaVariant::Default => None,
367 EmptyMediaVariant::Icon => Some(Background::Color(theme.palette.muted)),
368 };
369 let text_color = match props.variant {
370 EmptyMediaVariant::Default => None,
371 EmptyMediaVariant::Icon => Some(theme.palette.foreground),
372 };
373 let radius = theme.radius.md;
374
375 container(content)
376 .padding(match props.variant {
377 EmptyMediaVariant::Default => 0.0,
378 EmptyMediaVariant::Icon => ((size - icon_size) / 2.0).max(theme.spacing.sm),
379 })
380 .width(Length::Fixed(size))
381 .height(Length::Fixed(size))
382 .align_x(Horizontal::Center)
383 .align_y(Vertical::Center)
384 .style(move |_t| iced::widget::container::Style {
385 background,
386 text_color,
387 border: Border {
388 color: Color::TRANSPARENT,
389 width: 0.0,
390 radius: radius.into(),
391 },
392 ..Default::default()
393 })
394 .into()
395}
396
397pub fn empty_title<'a, Message: 'a>(
398 value: impl Into<String>,
399 props: EmptyTitleProps,
400 theme: &'a Theme,
401) -> Element<'a, Message> {
402 let size = if props.size > 0.0 {
403 props.size
404 } else {
405 theme.styles.empty.title_size
406 };
407 text(value.into())
408 .size(size)
409 .font(Font {
410 weight: Weight::Medium,
411 ..Font::DEFAULT
412 })
413 .style(move |_t| iced::widget::text::Style {
414 color: Some(theme.palette.foreground),
415 })
416 .align_x(iced::alignment::Horizontal::Center)
417 .into()
418}
419
420pub fn empty_description<'a, Message: 'a>(
421 value: impl Into<String>,
422 props: EmptyDescriptionProps,
423 theme: &'a Theme,
424) -> Element<'a, Message> {
425 let size = if props.size > 0.0 {
426 props.size
427 } else {
428 theme.styles.empty.description_size
429 };
430 let max_width = if props.max_width > 0.0 {
431 props.max_width
432 } else {
433 theme.styles.empty.description_max_width
434 };
435 container(
436 text(value.into())
437 .size(size)
438 .wrapping(Wrapping::WordOrGlyph)
439 .style(move |_t| iced::widget::text::Style {
440 color: Some(theme.palette.muted_foreground),
441 })
442 .align_x(iced::alignment::Horizontal::Center),
443 )
444 .max_width(max_width)
445 .into()
446}
447
448pub fn empty_content<'a, Message: 'a>(
449 items: Vec<Element<'a, Message>>,
450 props: EmptyContentProps,
451 theme: &Theme,
452) -> Element<'a, Message> {
453 let gap = if props.gap > 0.0 {
454 props.gap
455 } else {
456 theme.styles.empty.content_gap
457 };
458 let max_width = if props.max_width > 0.0 {
459 props.max_width
460 } else {
461 theme.styles.empty.content_max_width
462 };
463
464 container(
465 column(items)
466 .spacing(gap)
467 .align_x(Alignment::Center)
468 .width(Length::Fill),
469 )
470 .width(Length::Fill)
471 .center_x(Length::Fill)
472 .max_width(max_width)
473 .into()
474}
475
476pub fn empty<'a, Message: 'a>(props: EmptyProps<'a>, theme: &'a Theme) -> Element<'a, Message> {
477 let mut header_items = Vec::new();
478
479 if let Some(icon) = props.icon {
480 header_items.push(empty_media(
481 text(icon)
482 .size(theme.styles.empty.media_icon_size)
483 .font(Font::with_name("lucide"))
484 .style(move |_t| iced::widget::text::Style {
485 color: Some(theme.palette.foreground),
486 }),
487 EmptyMediaProps::new().variant(EmptyMediaVariant::Icon),
488 theme,
489 ));
490 }
491
492 header_items.push(empty_title(props.title, EmptyTitleProps::new(), theme));
493
494 if let Some(description) = props.description {
495 header_items.push(empty_description(
496 description,
497 EmptyDescriptionProps::new(),
498 theme,
499 ));
500 }
501
502 empty_root(
503 row![empty_header(header_items, EmptyHeaderProps::new(), theme)]
504 .width(Length::Fill)
505 .align_y(Alignment::Center),
506 EmptyRootProps::new(),
507 theme,
508 )
509}
510
511fn apply_opacity(color: Color, opacity: f32) -> Color {
512 Color {
513 a: color.a * opacity,
514 ..color
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn empty_props_builder() {
524 let props = EmptyProps::new("No results")
525 .description("Try adjusting your search.")
526 .icon("x");
527
528 assert_eq!(props.title, "No results");
529 assert_eq!(props.description, Some("Try adjusting your search."));
530 assert_eq!(props.icon, Some("x"));
531 }
532
533 #[test]
534 fn empty_root_props_builder() {
535 let props = EmptyRootProps::new()
536 .bordered(true)
537 .dashed(false)
538 .padding(32.0)
539 .gap(12.0)
540 .max_width(420.0)
541 .min_height(240.0);
542
543 assert!(props.bordered);
544 assert!(!props.dashed);
545 assert_eq!(props.padding, 32.0);
546 assert_eq!(props.gap, 12.0);
547 assert_eq!(props.max_width, 420.0);
548 assert_eq!(props.min_height, 240.0);
549 }
550
551 #[test]
552 fn empty_media_props_builder() {
553 let props = EmptyMediaProps::new()
554 .variant(EmptyMediaVariant::Icon)
555 .size(48.0)
556 .icon_size(20.0);
557
558 assert_eq!(props.variant, EmptyMediaVariant::Icon);
559 assert_eq!(props.size, 48.0);
560 assert_eq!(props.icon_size, 20.0);
561 }
562}