Something went wrong. Try again.
Something went wrong. Try again.
5.6 kB · 214 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215use iced::{ Background, Border, Element, Length, Padding, alignment::{Horizontal, Vertical}, widget::{Column, Space, button, container, text},};
use crate::ui::{ components::icon::Icon, style::{color, radius, spacing, typography},};
const CONTENT_SPACING: f32 = spacing::UNIT;const ICON_SIZE: f32 = 28.0;const SECTION_GAP: f32 = spacing::SPACE_6;
pub struct EmptyState<'a, M> { action: Option<(&'a str, M)>, icon: Option<Icon>, subtitle: Option<&'a str>, title: &'a str,}
impl<'a, M: Clone + 'static> EmptyState<'a, M> { pub fn action(mut self, label: &'a str, on_press: M) -> Self { self.action = Some((label, on_press)); self }
#[cfg(test)] pub fn icon(mut self, icon: Icon) -> Self { self.icon = Some(icon); self }
pub fn render(self) -> Element<'a, M> { let mut children: Vec<Element<'a, M>> = Vec::new();
if let Some(icon) = self.icon { children.push(icon.color(color::text::tertiary()).size(ICON_SIZE).render()); children.push(Space::new().height(Length::Fixed(CONTENT_SPACING)).into()); }
children.push( text(self.title.to_owned()) .font(typography::body::MEDIUM) .size(typography::size::MD) .style(|_| text::Style { color: Some(color::text::secondary()), }) .into(), );
if let Some(subtitle) = self.subtitle { children.push(Space::new().height(Length::Fixed(CONTENT_SPACING)).into()); children.push( text(subtitle.to_owned()) .font(typography::body::REGULAR) .size(typography::size::SM) .style(|_| text::Style { color: Some(color::text::tertiary()), }) .into(), ); }
if let Some((label, on_press)) = self.action { children.push(Space::new().height(Length::Fixed(SECTION_GAP)).into()); children.push(action_button(label, on_press)); }
container(Column::with_children(children).align_x(Horizontal::Center)) .width(Length::Fill) .height(Length::Fill) .align_x(Horizontal::Center) .align_y(Vertical::Center) .padding(SECTION_GAP) .into() }
pub fn subtitle(mut self, subtitle: &'a str) -> Self { self.subtitle = Some(subtitle); self }}
pub enum LoadStateView<'a, M> { Empty(EmptyState<'a, M>), Error(&'a str), Loading(&'a str),}
pub fn empty_state<M>(title: &str) -> EmptyState<'_, M> { EmptyState { action: None, icon: None, subtitle: None, title, }}
pub fn load_state_view<M: Clone + 'static>(view: LoadStateView<'_, M>) -> Element<'_, M> { match view { LoadStateView::Empty(empty) => empty.render(), LoadStateView::Error(message) => placeholder(message, color::status::DANGER), LoadStateView::Loading(message) => placeholder(message, color::text::secondary()), }}
fn action_button<'a, M: Clone + 'a>(label: &'a str, on_press: M) -> Element<'a, M> { button( text(label.to_owned()) .font(typography::body::MEDIUM) .size(typography::size::SM) .style(|_| text::Style { color: Some(color::accent()), }), ) .padding(Padding { top: 7.0, bottom: 7.0, left: spacing::SPACE_3, right: spacing::SPACE_3, }) .on_press(on_press) .style(|_, status| { let hover = matches!(status, button::Status::Hovered | button::Status::Pressed); button::Style { background: Some(Background::Color(if hover { color::with_alpha(color::accent(), 0.12) } else { iced::Color::TRANSPARENT })), border: Border { color: color::accent_muted(), radius: radius::CONTROL.into(), width: 1.0, }, text_color: color::accent(), ..button::Style::default() } }) .into()}
fn placeholder<'a, M: 'a>(message: &'a str, text_color: iced::Color) -> Element<'a, M> { container( text(message.to_owned()) .font(typography::body::REGULAR) .size(typography::size::MD) .style(move |_| text::Style { color: Some(text_color), }), ) .width(Length::Fill) .height(Length::Fill) .align_x(Horizontal::Center) .align_y(Vertical::Center) .padding(SECTION_GAP) .into()}
#[cfg(test)]mod tests { use super::*;
#[derive(Clone, Debug, Eq, PartialEq)] enum Message { ClearFilters, }
mod empty_state { use super::*;
#[test] fn it_renders_a_bare_title() { let _el: Element<'_, Message> = super::super::empty_state("No assets").render(); }
#[test] fn it_renders_with_a_subtitle_but_no_icon_or_action() { let _el: Element<'_, Message> = super::super::empty_state("Nothing here") .subtitle("Sync to populate this view.") .render(); }
#[test] fn it_renders_with_an_icon_subtitle_and_action() { let _el: Element<'_, Message> = super::super::empty_state("No matches") .icon(Icon::filter()) .subtitle("Try widening your filters.") .action("Clear filters", Message::ClearFilters) .render(); } }
mod load_state_view { use super::*;
#[test] fn it_renders_the_error_branch() { let _el: Element<'_, Message> = load_state_view(LoadStateView::Error("Couldn\u{2019}t load")); }
#[test] fn it_renders_the_loaded_empty_branch() { let _el: Element<'_, Message> = load_state_view(LoadStateView::Empty(super::super::empty_state("No rows"))); }
#[test] fn it_renders_the_loading_branch() { let _el: Element<'_, Message> = load_state_view(LoadStateView::Loading("Loading\u{2026}")); } }}