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