use gpui::{
AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::FluentBuilder, px,
};
use gpui_kit_assets::{Icon, icon};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, Space};
use crate::foundation::Ident;
use crate::motion;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EmptyKind {
#[default]
Empty,
Unstarted,
Unavailable,
Failed,
}
#[derive(IntoElement)]
pub struct EmptyState {
ident: Ident,
kind: EmptyKind,
title: SharedString,
detail: Option<SharedString>,
action: Option<AnyElement>,
}
impl std::fmt::Debug for EmptyState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("EmptyState")
.field("ident", &self.ident)
.field("kind", &self.kind)
.field("title", &self.title)
.finish()
}
}
impl EmptyState {
pub fn new(ident: impl Into<Ident>, title: impl Into<SharedString>) -> Self {
Self {
ident: ident.into(),
kind: EmptyKind::default(),
title: title.into(),
detail: None,
action: None,
}
}
pub fn kind(mut self, kind: EmptyKind) -> Self {
self.kind = kind;
self
}
pub fn detail(mut self, detail: impl Into<SharedString>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn action(mut self, action: impl IntoElement) -> Self {
self.action = Some(action.into_any_element());
self
}
}
impl RenderOnce for EmptyState {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let (glyph, tint) = match self.kind {
EmptyKind::Empty => (Icon::Checklist, theme.colors.text_faint),
EmptyKind::Unstarted => (Icon::Document, theme.colors.text_faint),
EmptyKind::Unavailable => (Icon::CloseCircle, theme.colors.warning),
EmptyKind::Failed => (Icon::Danger, theme.colors.danger),
};
let content = div()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap(px(theme.space(Space::Sm)))
.w_full()
.text_align(gpui::TextAlign::Center)
.child(icon(glyph).size(px(20.0)).text_color(tint))
.child(
div()
.text_size(px(theme.typography.body.size))
.text_color(theme.colors.text)
.child(self.title.clone()),
)
.when_some(self.detail.clone(), |element, detail| {
element.child(
div()
.max_w(px(360.0))
.text_size(px(theme.typography.caption.size))
.text_color(theme.colors.text_muted)
.child(detail),
)
})
.children(self.action);
div()
.flex()
.flex_col()
.items_center()
.justify_center()
.p(px(theme.space(Space::Lg)))
.w_full()
.child(motion::content_in(
self.ident.child("in").element_id(),
&theme,
content,
))
.semantic_in(
cx,
NodeSpec::new(self.ident.semantic_id(), Role::Status)
.text(self.title.clone())
.value(match self.kind {
EmptyKind::Empty => "empty",
EmptyKind::Unstarted => "unstarted",
EmptyKind::Unavailable => "unavailable",
EmptyKind::Failed => "failed",
}),
)
}
}
#[derive(Debug, IntoElement)]
pub struct Divider {
ident: Option<Ident>,
label: Option<SharedString>,
}
impl Default for Divider {
fn default() -> Self {
Self::new()
}
}
impl Divider {
pub fn new() -> Self {
Self {
ident: None,
label: None,
}
}
pub fn id(mut self, ident: impl Into<Ident>) -> Self {
self.ident = Some(ident.into());
self
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
}
impl RenderOnce for Divider {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let rule = || {
div()
.h(px(theme.borders.hairline))
.flex_1()
.bg(theme.colors.hairline)
};
let spec = self.ident.as_ref().map(|ident| {
let mut spec = NodeSpec::new(ident.semantic_id(), Role::Separator);
if let Some(label) = self.label.clone() {
spec = spec.text(label);
}
spec
});
let element = div()
.flex()
.flex_row()
.items_center()
.w_full()
.gap(px(theme.space(Space::Sm)))
.child(rule())
.when_some(self.label.clone(), |element, label| {
element.child(
div()
.flex_none()
.text_size(px(theme.typography.caption.size))
.text_color(theme.colors.text_faint)
.child(label),
)
})
.when(self.label.is_some(), |element| element.child(rule()));
match spec {
Some(spec) => element.semantic_in(cx, spec).into_any_element(),
None => element.into_any_element(),
}
}
}