Skip to main content

ashlar_ui/content/
empty_state.rs

1//! What a view shows when it has nothing to show.
2
3use egui::{Response, RichText, Sense, Ui, vec2};
4
5use crate::automation::{self, Details, Role};
6use crate::controls::{Button, IconPainter, Variant};
7use crate::tokens;
8
9/// A centred icon, title, explanation and optional action: an empty list, a search with no
10/// results, a document that failed to load.
11pub struct EmptyState<'a> {
12    id: &'a str,
13    title: &'a str,
14    body: Option<&'a str>,
15    action: Option<&'a str>,
16    icon: Option<IconPainter<'a>>,
17}
18
19/// What [`EmptyState::show`] reports.
20pub struct EmptyStateOutput {
21    /// The whole block.
22    pub response: Response,
23    /// The action button was clicked.
24    pub action: bool,
25}
26
27impl<'a> EmptyState<'a> {
28    pub fn new(id: &'a str, title: &'a str) -> Self {
29        EmptyState { id, title, body: None, action: None, icon: None }
30    }
31    /// A sentence or two under the title.
32    pub fn body(mut self, text: &'a str) -> Self {
33        self.body = Some(text);
34        self
35    }
36    /// A primary button under the text (registered as `<id>.action`).
37    pub fn action(mut self, label: &'a str) -> Self {
38        self.action = Some(label);
39        self
40    }
41    /// An icon above the title, painted in the secondary text colour.
42    pub fn icon(mut self, paint: IconPainter<'a>) -> Self {
43        self.icon = Some(paint);
44        self
45    }
46
47    pub fn show(self, ui: &mut Ui) -> EmptyStateOutput {
48        let t = tokens::current(ui.ctx());
49        let mut action = false;
50        let inner = ui.vertical_centered(|ui| {
51            let title = self.title;
52            ui.ctx().accesskit_node_builder(ui.unique_id(), |node| {
53                node.set_role(egui::accesskit::Role::Group);
54                node.set_label(title);
55            });
56            ui.add_space(t.space.xl);
57            if let Some(paint) = self.icon {
58                let side = t.control_height * 1.5;
59                let (rect, _) = ui.allocate_exact_size(vec2(side, side), Sense::hover());
60                paint(ui.painter(), rect, t.text.secondary);
61                ui.add_space(t.space.md);
62            }
63            ui.label(RichText::new(self.title).size(t.type_scale.heading).strong().color(t.text.primary));
64            if let Some(body) = self.body {
65                ui.add_space(t.space.xs);
66                ui.label(RichText::new(body).color(t.text.secondary));
67            }
68            if let Some(label) = self.action {
69                ui.add_space(t.space.lg);
70                action = Button::new(self.id, label).part("action").variant(Variant::Primary).show(ui).clicked();
71            }
72            ui.add_space(t.space.xl);
73        });
74        let response = inner.response;
75        automation::register_with(ui, &response, self.id, self.title, Role::Group, Details { value: self.body, selected: None });
76        EmptyStateOutput { response, action }
77    }
78}