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    /// A name in a colour — a label on an entry, an option of a
89    /// multi-select. `tint` is the hue it is told apart by, painted as a wash
90    /// under the theme's text; [`crate::multi_select::tint`] derives one from
91    /// the name. A long name ends in `…`; the caller caps the width.
92    fn chip(&self, label: impl Into<SharedString>, tint: gpui::Hsla) -> Div {
93        let theme = self.theme();
94        div()
95            .flex_none()
96            .flex()
97            .flex_row()
98            .items_center()
99            .gap(px(2.0))
100            .px(px(6.0))
101            .rounded(px(Theme::control_radius()))
102            .bg(tint.opacity(0.22))
103            .text_style(TextStyle::Callout)
104            .text_color(theme.text)
105            .child(div().min_w_0().truncate().child(label.into()))
106    }
107
108    /// Breadcrumb trail. Items are added by the caller with
109    /// [`Self::breadcrumb_item`], separated by [`Self::breadcrumb_separator`].
110    fn breadcrumb(&self) -> Div {
111        div()
112            .self_start()
113            .flex()
114            .flex_row()
115            .items_center()
116            .gap(px(4.0))
117            .min_w_0()
118    }
119
120    /// One crumb. The last one is `current` and stops looking clickable.
121    fn breadcrumb_item(&self, label: impl Into<SharedString>, current: bool) -> Div {
122        let theme = self.theme();
123        let mut crumb = div()
124            .min_w_0()
125            .truncate()
126            .text_style(TextStyle::Callout)
127            .child(label.into());
128        crumb = if current {
129            crumb.text_color(theme.text)
130        } else {
131            crumb.text_color(theme.text_muted).cursor_pointer()
132        };
133        crumb
134    }
135
136    /// The chevron between crumbs.
137    fn breadcrumb_separator(&self) -> Svg {
138        let theme = self.theme();
139        crate::icons::icon(crate::icons::glyph::ChevronRight)
140            .size(px(12.0))
141            .text_color(theme.text_faint)
142    }
143
144    /// The centered "nothing here yet" panel: icon, headline, one line of hint.
145    fn empty_state(
146        &self,
147        icon: impl Into<Icon>,
148        title: impl Into<SharedString>,
149        hint: impl Into<SharedString>,
150    ) -> Div {
151        let theme = self.theme();
152        div()
153            .w_full()
154            .flex()
155            .flex_col()
156            .items_center()
157            .justify_center()
158            .gap(px(6.0))
159            .py(px(40.0))
160            .child(
161                crate::icons::icon(icon)
162                    .size(px(24.0))
163                    .text_color(theme.text_faint),
164            )
165            .child(
166                div()
167                    .text_style(TextStyle::Headline)
168                    .text_color(theme.text)
169                    .child(title.into()),
170            )
171            .child(
172                div()
173                    .text_style(TextStyle::Callout)
174                    .text_color(theme.text_muted)
175                    .child(hint.into()),
176            )
177    }
178}
179
180impl Content for Theme {}