Skip to main content

llimphi_widget_empty/
lib.rs

1//! `llimphi-widget-empty` — empty state con icono, título y descripción.
2//!
3//! Patrón para reemplazar pantallas en blanco con orientación: cuando
4//! una lista no tiene items, un editor no tiene archivo abierto, una
5//! búsqueda no arrojó resultados — en vez de fondo plano, mostrar
6//! un icono grande apagado + título + descripción corta + (opcional)
7//! botón de acción primaria.
8
9#![forbid(unsafe_code)]
10
11use llimphi_ui::llimphi_layout::taffy::{
12    prelude::{length, percent, FlexDirection, Size, Style},
13    AlignItems, JustifyContent,
14};
15use llimphi_ui::llimphi_raster::peniko::Color;
16use llimphi_ui::llimphi_text::Alignment;
17use llimphi_ui::View;
18use llimphi_icons::{icon_view, Icon};
19use llimphi_theme::{alpha, Theme};
20
21/// Paleta del empty state — colores apagados para no competir con la
22/// UI principal.
23#[derive(Debug, Clone, Copy)]
24pub struct EmptyPalette {
25    pub fg_icon: Color,
26    pub fg_title: Color,
27    pub fg_desc: Color,
28}
29
30impl EmptyPalette {
31    pub fn from_theme(t: &Theme) -> Self {
32        Self {
33            fg_icon: with_alpha8(t.fg_muted, alpha::HINT),
34            fg_title: t.fg_muted,
35            fg_desc: with_alpha8(t.fg_muted, alpha::DISABLED),
36        }
37    }
38}
39
40fn with_alpha8(c: Color, a: u8) -> Color {
41    let [r, g, b, _] = c.components;
42    use llimphi_ui::llimphi_raster::peniko::color::AlphaColor;
43    AlphaColor::new([r, g, b, a as f32 / 255.0])
44}
45
46/// Construye el empty state. La app llama desde su `view()` cuando
47/// detecta el caso vacío:
48///
49/// ```ignore
50/// if model.items.is_empty() {
51///     return empty_view(Icon::File, "Sin archivos abiertos",
52///                       Some("Abrí uno con Ctrl+O para empezar."),
53///                       &palette);
54/// }
55/// ```
56pub fn empty_view<Msg: Clone + 'static>(
57    icon: Icon,
58    title: impl Into<String>,
59    description: Option<&str>,
60    palette: &EmptyPalette,
61) -> View<Msg> {
62    let icon_cell = View::new(Style {
63        size: Size {
64            width: length(72.0_f32),
65            height: length(72.0_f32),
66        },
67        flex_shrink: 0.0,
68        ..Default::default()
69    })
70    .children(vec![icon_view(icon, palette.fg_icon, 1.4)]);
71
72    let title_view = View::new(Style {
73        size: Size {
74            width: percent(1.0_f32),
75            height: length(28.0_f32),
76        },
77        flex_shrink: 0.0,
78        ..Default::default()
79    })
80    .text_aligned(title.into(), 15.5, palette.fg_title, Alignment::Center);
81
82    let mut children = vec![icon_cell, title_view];
83    if let Some(desc) = description {
84        children.push(
85            View::new(Style {
86                size: Size {
87                    width: length(360.0_f32),
88                    height: length(40.0_f32),
89                },
90                flex_shrink: 0.0,
91                ..Default::default()
92            })
93            .text_aligned(desc.to_string(), 12.0, palette.fg_desc, Alignment::Center),
94        );
95    }
96
97    View::new(Style {
98        flex_direction: FlexDirection::Column,
99        size: Size {
100            width: percent(1.0_f32),
101            height: percent(1.0_f32),
102        },
103        align_items: Some(AlignItems::Center),
104        justify_content: Some(JustifyContent::Center),
105        gap: Size {
106            width: length(0.0_f32),
107            height: length(14.0_f32),
108        },
109        ..Default::default()
110    })
111    .children(children)
112}