Skip to main content

ui/widgets/
content.rs

1//! Content pieces — badges, avatar, tag, breadcrumb, the empty state.
2//!
3//! A catalog trait, like every widget group: import it to unlock
4//! `theme.badge(..)`, `theme.avatar(..)`, `theme.empty_state(..)`.
5
6use gpui::{Div, SharedString, Svg, div, prelude::*, px};
7use theme::{TextStyle, Theme, ThemeExt, Typeset, ink};
8
9pub trait Content: ThemeExt {
10    /// Right-anchored badge pill: `rounded-full border px-2 py-0.5`.
11    fn badge(&self, label: impl Into<SharedString>) -> Div {
12        let theme = self.theme();
13        div()
14            .flex_none()
15            .px(px(8.0))
16            .py(px(2.0))
17            .rounded_full()
18            .border_1()
19            .border_color(theme.border)
20            .text_style(TextStyle::Caption)
21            .text_color(theme.text_muted)
22            .child(label.into())
23    }
24
25    /// Emerald status pill (the Accounts "Active" badge:
26    /// `bg-emerald-400/[0.12] text-emerald-300/90`).
27    fn badge_active(&self, label: impl Into<SharedString>) -> Div {
28        let theme = self.theme();
29        let emerald = theme.success;
30        let emerald_text = theme.success_muted; // emerald-300
31        div()
32            .flex_none()
33            .px(px(8.0))
34            .py(px(2.0))
35            .rounded_full()
36            .bg(emerald.opacity(0.12))
37            .text_style(TextStyle::Caption)
38            .text_color(emerald_text.opacity(0.9))
39            .child(label.into())
40    }
41
42    /// Circular avatar holding one or two initials — the fallback every avatar
43    /// needs, and often the whole thing on a monochrome surface.
44    fn avatar(&self, initials: impl Into<SharedString>) -> Div {
45        let theme = self.theme();
46        div()
47            .flex_none()
48            .size(px(28.0))
49            .rounded_full()
50            .bg(ink(0.12))
51            .flex()
52            .items_center()
53            .justify_center()
54            .text_style(TextStyle::Subheadline)
55            .font_weight(gpui::FontWeight::MEDIUM)
56            .text_color(theme.text_muted)
57            .child(initials.into())
58    }
59
60    /// A removable chip — a token in a filter bar or a recipient field. The
61    /// caller adds the click handler for the ✕.
62    fn tag(&self, label: impl Into<SharedString>) -> Div {
63        let theme = self.theme();
64        div()
65            .self_start()
66            .flex()
67            .flex_row()
68            .items_center()
69            .gap(px(5.0))
70            .pl(px(8.0))
71            .pr(px(5.0))
72            .py(px(3.0))
73            .rounded(px(Theme::control_radius()))
74            .bg(ink(0.07))
75            .border_1()
76            .border_color(theme.border)
77            .text_style(TextStyle::Callout)
78            .text_color(theme.text)
79            .child(label.into())
80            .child(
81                crate::icons::icon(crate::icons::CLOSE)
82                    .size(px(10.0))
83                    .text_color(theme.text_faint),
84            )
85    }
86
87    /// Breadcrumb trail. Items are added by the caller with
88    /// [`Self::breadcrumb_item`], separated by [`Self::breadcrumb_separator`].
89    fn breadcrumb(&self) -> Div {
90        div()
91            .self_start()
92            .flex()
93            .flex_row()
94            .items_center()
95            .gap(px(4.0))
96            .min_w_0()
97    }
98
99    /// One crumb. The last one is `current` and stops looking clickable.
100    fn breadcrumb_item(&self, label: impl Into<SharedString>, current: bool) -> Div {
101        let theme = self.theme();
102        let mut crumb = div()
103            .min_w_0()
104            .truncate()
105            .text_style(TextStyle::Callout)
106            .child(label.into());
107        crumb = if current {
108            crumb.text_color(theme.text)
109        } else {
110            crumb.text_color(theme.text_muted).cursor_pointer()
111        };
112        crumb
113    }
114
115    /// The chevron between crumbs.
116    fn breadcrumb_separator(&self) -> Svg {
117        let theme = self.theme();
118        crate::icons::icon(crate::icons::ALT_ARROW_RIGHT)
119            .size(px(12.0))
120            .text_color(theme.text_faint)
121    }
122
123    /// The centered "nothing here yet" panel: icon, headline, one line of hint.
124    fn empty_state(
125        &self,
126        icon_path: &'static str,
127        title: impl Into<SharedString>,
128        hint: impl Into<SharedString>,
129    ) -> Div {
130        let theme = self.theme();
131        div()
132            .w_full()
133            .flex()
134            .flex_col()
135            .items_center()
136            .justify_center()
137            .gap(px(6.0))
138            .py(px(40.0))
139            .child(
140                crate::icons::icon(icon_path)
141                    .size(px(24.0))
142                    .text_color(theme.text_faint),
143            )
144            .child(
145                div()
146                    .text_style(TextStyle::Headline)
147                    .text_color(theme.text)
148                    .child(title.into()),
149            )
150            .child(
151                div()
152                    .text_style(TextStyle::Callout)
153                    .text_color(theme.text_muted)
154                    .child(hint.into()),
155            )
156    }
157}
158
159impl Content for Theme {}