gpui_kit/display/
empty.rs1use gpui::{
8 AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
9 prelude::FluentBuilder, px,
10};
11use gpui_kit_assets::{Icon, icon};
12use gpui_kit_semantics::{NodeSpec, Role, Semantic};
13use gpui_kit_theme::{ActiveTheme, Space};
14
15use crate::foundation::Ident;
16use crate::motion;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum EmptyKind {
21 #[default]
23 Empty,
24 Unstarted,
26 Unavailable,
28 Failed,
30}
31
32#[derive(IntoElement)]
34pub struct EmptyState {
35 ident: Ident,
36 kind: EmptyKind,
37 title: SharedString,
38 detail: Option<SharedString>,
39 action: Option<AnyElement>,
40}
41
42impl std::fmt::Debug for EmptyState {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 formatter
45 .debug_struct("EmptyState")
46 .field("ident", &self.ident)
47 .field("kind", &self.kind)
48 .field("title", &self.title)
49 .finish()
50 }
51}
52
53impl EmptyState {
54 pub fn new(ident: impl Into<Ident>, title: impl Into<SharedString>) -> Self {
55 Self {
56 ident: ident.into(),
57 kind: EmptyKind::default(),
58 title: title.into(),
59 detail: None,
60 action: None,
61 }
62 }
63
64 pub fn kind(mut self, kind: EmptyKind) -> Self {
65 self.kind = kind;
66 self
67 }
68
69 pub fn detail(mut self, detail: impl Into<SharedString>) -> Self {
72 self.detail = Some(detail.into());
73 self
74 }
75
76 pub fn action(mut self, action: impl IntoElement) -> Self {
78 self.action = Some(action.into_any_element());
79 self
80 }
81}
82
83impl RenderOnce for EmptyState {
84 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
85 let theme = cx.theme().clone();
86 let (glyph, tint) = match self.kind {
87 EmptyKind::Empty => (Icon::Checklist, theme.colors.text_faint),
88 EmptyKind::Unstarted => (Icon::Document, theme.colors.text_faint),
89 EmptyKind::Unavailable => (Icon::CloseCircle, theme.colors.warning),
90 EmptyKind::Failed => (Icon::Danger, theme.colors.danger),
91 };
92
93 let content = div()
94 .flex()
95 .flex_col()
96 .items_center()
97 .justify_center()
98 .gap(px(theme.space(Space::Sm)))
99 .w_full()
100 .text_align(gpui::TextAlign::Center)
101 .child(icon(glyph).size(px(20.0)).text_color(tint))
102 .child(
103 div()
104 .text_size(px(theme.typography.body.size))
105 .text_color(theme.colors.text)
106 .child(self.title.clone()),
107 )
108 .when_some(self.detail.clone(), |element, detail| {
109 element.child(
110 div()
111 .max_w(px(360.0))
112 .text_size(px(theme.typography.caption.size))
113 .text_color(theme.colors.text_muted)
114 .child(detail),
115 )
116 })
117 .children(self.action);
118
119 div()
122 .flex()
123 .flex_col()
124 .items_center()
125 .justify_center()
126 .p(px(theme.space(Space::Lg)))
127 .w_full()
128 .child(motion::content_in(
129 self.ident.child("in").element_id(),
130 &theme,
131 content,
132 ))
133 .semantic_in(
134 cx,
135 NodeSpec::new(self.ident.semantic_id(), Role::Status)
136 .text(self.title.clone())
137 .value(match self.kind {
138 EmptyKind::Empty => "empty",
139 EmptyKind::Unstarted => "unstarted",
140 EmptyKind::Unavailable => "unavailable",
141 EmptyKind::Failed => "failed",
142 }),
143 )
144 }
145}
146
147#[derive(Debug, IntoElement)]
149pub struct Divider {
150 ident: Option<Ident>,
151 label: Option<SharedString>,
152}
153
154impl Default for Divider {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl Divider {
161 pub fn new() -> Self {
162 Self {
163 ident: None,
164 label: None,
165 }
166 }
167
168 pub fn id(mut self, ident: impl Into<Ident>) -> Self {
169 self.ident = Some(ident.into());
170 self
171 }
172
173 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
175 self.label = Some(label.into());
176 self
177 }
178}
179
180impl RenderOnce for Divider {
181 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
182 let theme = cx.theme().clone();
183 let rule = || {
184 div()
185 .h(px(theme.borders.hairline))
186 .flex_1()
187 .bg(theme.colors.hairline)
188 };
189 let spec = self.ident.as_ref().map(|ident| {
190 let mut spec = NodeSpec::new(ident.semantic_id(), Role::Separator);
191 if let Some(label) = self.label.clone() {
192 spec = spec.text(label);
193 }
194 spec
195 });
196
197 let element = div()
198 .flex()
199 .flex_row()
200 .items_center()
201 .w_full()
202 .gap(px(theme.space(Space::Sm)))
203 .child(rule())
204 .when_some(self.label.clone(), |element, label| {
205 element.child(
206 div()
207 .flex_none()
208 .text_size(px(theme.typography.caption.size))
209 .text_color(theme.colors.text_faint)
210 .child(label),
211 )
212 })
213 .when(self.label.is_some(), |element| element.child(rule()));
214 match spec {
215 Some(spec) => element.semantic_in(cx, spec).into_any_element(),
216 None => element.into_any_element(),
217 }
218 }
219}