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. Initials only:
45    /// there is no image variant.
46    fn avatar(&self, initials: impl Into<SharedString>) -> Div {
47        let theme = self.theme();
48        div()
49            .flex_none()
50            .size(px(28.0))
51            .rounded_full()
52            .bg(theme.ink(0.12))
53            .flex()
54            .items_center()
55            .justify_center()
56            .text_style(TextStyle::Subheadline)
57            .font_weight(gpui::FontWeight::MEDIUM)
58            .text_color(theme.text_muted)
59            .child(initials.into())
60    }
61
62    /// A removable chip — a token in a filter bar or a recipient field. The
63    /// caller adds the click handler for the ✕.
64    fn tag(&self, label: impl Into<SharedString>) -> Div {
65        let theme = self.theme();
66        div()
67            .self_start()
68            .flex()
69            .flex_row()
70            .items_center()
71            .gap(px(5.0))
72            .pl(px(8.0))
73            .pr(px(5.0))
74            .py(px(3.0))
75            .rounded(px(Theme::control_radius()))
76            .bg(theme.ink(0.07))
77            .border_1()
78            .border_color(theme.border)
79            .text_style(TextStyle::Callout)
80            .text_color(theme.text)
81            .child(label.into())
82            .child(
83                crate::icons::icon(crate::icons::glyph::X)
84                    .size(px(10.0))
85                    .text_color(theme.text_faint),
86            )
87    }
88
89    /// A name in a colour — a label on an entry, an option of a
90    /// multi-select. `tint` is the hue it is told apart by, painted as a wash
91    /// under the theme's text; [`crate::multi_select::tint`] derives one from
92    /// the name. A long name ends in `…`; the caller caps the width.
93    fn chip(&self, label: impl Into<SharedString>, tint: gpui::Hsla) -> Div {
94        let theme = self.theme();
95        div()
96            .flex_none()
97            .flex()
98            .flex_row()
99            .items_center()
100            .gap(px(2.0))
101            .px(px(6.0))
102            .rounded(px(Theme::control_radius()))
103            .bg(tint.opacity(0.22))
104            .text_style(TextStyle::Callout)
105            .text_color(theme.text)
106            .child(div().min_w_0().truncate().child(label.into()))
107    }
108
109    /// Breadcrumb trail. Items are added by the caller with
110    /// [`Self::breadcrumb_item`], separated by [`Self::breadcrumb_separator`].
111    fn breadcrumb(&self) -> Div {
112        div()
113            .self_start()
114            .flex()
115            .flex_row()
116            .items_center()
117            .gap(px(4.0))
118            .min_w_0()
119    }
120
121    /// One crumb. The last one is `current` and stops looking clickable.
122    fn breadcrumb_item(&self, label: impl Into<SharedString>, current: bool) -> Div {
123        let theme = self.theme();
124        let mut crumb = div()
125            .min_w_0()
126            .truncate()
127            .text_style(TextStyle::Callout)
128            .child(label.into());
129        crumb = if current {
130            crumb.text_color(theme.text)
131        } else {
132            crumb.text_color(theme.text_muted).cursor_pointer()
133        };
134        crumb
135    }
136
137    /// The chevron between crumbs.
138    fn breadcrumb_separator(&self) -> Svg {
139        let theme = self.theme();
140        crate::icons::icon(crate::icons::glyph::ChevronRight)
141            .size(px(12.0))
142            .text_color(theme.text_faint)
143    }
144
145    /// The centered "nothing here yet" panel: icon, headline, one line of hint.
146    fn empty_state(
147        &self,
148        icon: impl Into<Icon>,
149        title: impl Into<SharedString>,
150        hint: impl Into<SharedString>,
151    ) -> Div {
152        let theme = self.theme();
153        div()
154            .w_full()
155            .flex()
156            .flex_col()
157            .items_center()
158            .justify_center()
159            .gap(px(6.0))
160            .py(px(40.0))
161            .child(
162                crate::icons::icon(icon)
163                    .size(px(24.0))
164                    .text_color(theme.text_faint),
165            )
166            .child(
167                div()
168                    .text_style(TextStyle::Headline)
169                    .text_color(theme.text)
170                    .child(title.into()),
171            )
172            .child(
173                div()
174                    .text_style(TextStyle::Callout)
175                    .text_color(theme.text_muted)
176                    .child(hint.into()),
177            )
178    }
179}
180
181impl Content for Theme {}