use crate::button::Button;
use crate::cloud_image::CloudImage;
use crate::text::{Heading, Text};
use fluidattacks_designs_domain::button::Variant;
use fluidattacks_designs_domain::empty_state::{
Size, BUTTON_GAP, BUTTON_ROW_MARGIN_BOTTOM, CANCEL_VARIANT, CONFIRM_VARIANT, DEFAULT_IMAGE,
IMAGE_MARGIN_BOTTOM, IMAGE_SIZE, MAX_WIDTH, PADDING, TEXT_MARGIN_BOTTOM, TITLE_MARGIN_BOTTOM,
};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, AnyElement, App, ClickEvent, ElementId, IntoElement, ParentElement as _, RenderOnce,
SharedString, Styled as _, Window,
};
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
struct Action {
id: ElementId,
label: SharedString,
handler: ClickHandler,
}
#[derive(IntoElement)]
pub struct EmptyState {
title: SharedString,
description: SharedString,
size: Size,
padding: f32,
image: Option<AnyElement>,
confirm: Option<Action>,
confirm_variant: Variant,
cancel: Option<Action>,
cancel_variant: Variant,
}
impl EmptyState {
pub fn new(title: impl Into<SharedString>, description: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
description: description.into(),
size: Size::default(),
padding: PADDING,
image: Some(
CloudImage::new(DEFAULT_IMAGE)
.width(IMAGE_SIZE)
.into_any_element(),
),
confirm: None,
confirm_variant: CONFIRM_VARIANT,
cancel: None,
cancel_variant: CANCEL_VARIANT,
}
}
#[must_use]
pub const fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
#[must_use]
pub const fn padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
#[must_use]
pub fn image(mut self, image: impl IntoElement) -> Self {
self.image = Some(image.into_any_element());
self
}
#[must_use]
pub fn image_id(mut self, public_id: impl Into<gpui::SharedString>) -> Self {
self.image = Some(
CloudImage::new(public_id)
.width(IMAGE_SIZE)
.into_any_element(),
);
self
}
#[must_use]
pub fn confirm(
mut self,
id: impl Into<ElementId>,
label: impl Into<SharedString>,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.confirm = Some(Action {
id: id.into(),
label: label.into(),
handler: Box::new(handler),
});
self
}
#[must_use]
pub const fn confirm_variant(mut self, variant: Variant) -> Self {
self.confirm_variant = variant;
self
}
#[must_use]
pub fn cancel(
mut self,
id: impl Into<ElementId>,
label: impl Into<SharedString>,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.cancel = Some(Action {
id: id.into(),
label: label.into(),
handler: Box::new(handler),
});
self
}
#[must_use]
pub const fn cancel_variant(mut self, variant: Variant) -> Self {
self.cancel_variant = variant;
self
}
}
fn button_row(actions: [(Option<Action>, Variant); 2]) -> gpui::Div {
let mut row = div()
.flex()
.gap(px(BUTTON_GAP))
.mb(px(BUTTON_ROW_MARGIN_BOTTOM));
for (action, variant) in actions {
let Some(action) = action else { continue };
row = row.child(
Button::new(action.id, action.label)
.variant(variant)
.on_click(action.handler),
);
}
row
}
impl RenderOnce for EmptyState {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let has_buttons = self.confirm.is_some() || self.cancel.is_some();
let buttons = has_buttons.then(|| {
button_row([
(self.cancel, self.cancel_variant),
(self.confirm, self.confirm_variant),
])
});
div()
.flex()
.flex_col()
.items_center()
.p(px(self.padding))
.max_w(px(MAX_WIDTH))
.mx_auto()
.when_some(self.image, |el, image| {
el.child(
div()
.flex()
.items_center()
.justify_center()
.size(px(IMAGE_SIZE))
.mb(px(IMAGE_MARGIN_BOTTOM))
.child(image),
)
})
.child(
div()
.mb(px(TITLE_MARGIN_BOTTOM))
.text_center()
.child(Heading::new(self.title).size(self.size.title())),
)
.child(
div()
.when(has_buttons, |el| el.mb(px(TEXT_MARGIN_BOTTOM)))
.text_center()
.child(Text::new(self.description).size(self.size.description())),
)
.when_some(buttons, gpui::ParentElement::child)
}
}