Skip to main content

egui_charts/ui_kit/empty_state/
mod.rs

1//! Empty state component: a centred message for lists and panels with no content.
2//!
3//! ```
4//! # use egui_charts::ui_kit::EmptyState;
5//! # egui::__run_test_ui(|ui| {
6//! let create = EmptyState::new("No alerts yet")
7//!     .icon(&egui_charts::icons::icons::ALERTS)
8//!     .description("Create one from the toolbar or the chart menu.")
9//!     .action("Create alert")
10//!     .fill_height()
11//!     .show(ui);
12//! # let _ = create;
13//! # });
14//! ```
15
16use crate::localization as l10n;
17use egui::{Frame, Margin, RichText, Ui, Vec2};
18
19use crate::icons::Icon;
20use crate::theme::ThemeExt;
21use crate::ui_kit::buttons::Button;
22
23/// Size of the optional icon above the message.
24const ICON_SIZE: f32 = 32.0;
25
26/// A horizontally centred empty-state placeholder: optional icon, message, optional
27/// description and optional primary action.
28pub struct EmptyState<'a> {
29    message: &'a str,
30    description: Option<&'a str>,
31    icon: Option<&'static Icon>,
32    action: Option<&'a str>,
33    fill_height: bool,
34}
35
36impl<'a> EmptyState<'a> {
37    /// Create an empty state with the given message.
38    pub fn new(message: &'a str) -> Self {
39        Self {
40            message,
41            description: None,
42            icon: None,
43            action: None,
44            fill_height: false,
45        }
46    }
47
48    /// Add a secondary description line.
49    #[must_use]
50    pub fn description(mut self, desc: &'a str) -> Self {
51        self.description = Some(desc);
52        self
53    }
54
55    /// Show an icon above the message.
56    #[must_use]
57    pub fn icon(mut self, icon: &'static Icon) -> Self {
58        self.icon = Some(icon);
59        self
60    }
61
62    /// Add a primary button with this label below the text; [`Self::show`] reports its clicks.
63    #[must_use]
64    pub fn action(mut self, label: &'a str) -> Self {
65        self.action = Some(label);
66        self
67    }
68
69    /// Centre the empty state vertically in the rest of the `Ui`, for a panel or list that
70    /// shows nothing else. Without it, the empty state takes only its own height.
71    #[must_use]
72    pub fn fill_height(mut self) -> Self {
73        self.fill_height = true;
74        self
75    }
76
77    /// Show the empty state. Returns `true` when the action button was clicked.
78    pub fn show(self, ui: &mut Ui) -> bool {
79        let theme = ui.active_theme();
80        // The height of the last pass, to centre this one.
81        let height_id = ui.next_auto_id().with("empty_state_height");
82        let last_height = if self.fill_height {
83            ui.data(|d| d.get_temp::<f32>(height_id))
84        } else {
85            None
86        };
87        if let Some(height) = last_height {
88            ui.add_space(((ui.available_height() - height) / 2.0).max(0.0));
89        }
90
91        let shown = Frame::new()
92            .inner_margin(Margin::symmetric(0, theme.spacing.xl as i8))
93            .show(ui, |ui| {
94                ui.vertical_centered(|ui| {
95                    ui.spacing_mut().item_spacing.y = theme.spacing.lg;
96                    let muted = ui.visuals().weak_text_color();
97                    if let Some(icon) = self.icon {
98                        ui.add(icon.as_image_tinted(Vec2::splat(ICON_SIZE), muted));
99                    }
100                    ui.label(
101                        RichText::new(l10n::text(ui.ctx(), self.message)).size(theme.text.body),
102                    );
103                    if let Some(desc) = self.description {
104                        ui.label(
105                            RichText::new(l10n::text(ui.ctx(), desc))
106                                .size(theme.text.small)
107                                .color(muted),
108                        );
109                    }
110                    self.action
111                        .is_some_and(|label| Button::new(label).primary().show(ui).clicked())
112                })
113                .inner
114            });
115
116        let height = shown.response.rect.height();
117        if self.fill_height && last_height != Some(height) {
118            ui.data_mut(|d| d.insert_temp(height_id, height));
119            ui.ctx().request_discard("EmptyState measured its height");
120        }
121        shown.inner
122    }
123}