Skip to main content

gpui_kit/foundation/
styled_ext.rs

1use gpui::{Div, FontWeight, InteractiveElement, ParentElement, SharedString, Styled, div, px};
2use gpui_kit_theme::{Elevation, Radius, Space, Surface, TextTone, Theme, TypeScale};
3
4/// Creates text with an explicit complete type step and primary text tone.
5///
6/// This is the only entry point for putting a string directly into the GPUI
7/// element tree. A component cannot rely on a host ancestor's font or colour:
8/// the same component must keep its metrics when embedded by itself. Callers
9/// that need a secondary colour apply [`StyledExt::text_tone`] to the returned
10/// element; callers that need edge alignment apply
11/// [`DirectionalExt::text_start`](super::DirectionalExt::text_start) or
12/// [`DirectionalExt::text_end`](super::DirectionalExt::text_end). The helper
13/// deliberately chooses no physical left/right alignment, so it does not turn
14/// a right-to-left run back into a left-to-right one.
15pub fn text(theme: &Theme, scale: TypeScale, content: impl Into<SharedString>) -> Div {
16    div()
17        .type_scale(theme, scale)
18        .text_tone(theme, TextTone::Primary)
19        .child(content.into())
20}
21
22/// Token-addressed styling helpers.
23///
24/// These exist so component code names a semantic role instead of repeating a
25/// literal, which keeps `crates/gpui-kit-tokens/tokens/*.json` the single authority for values that
26/// occur more than once.
27pub trait StyledExt: Styled + Sized {
28    fn surface(self, theme: &Theme, surface: Surface) -> Self {
29        self.bg(theme.surface(surface))
30    }
31
32    fn text_tone(self, theme: &Theme, tone: TextTone) -> Self {
33        self.text_color(theme.text_color(tone))
34    }
35
36    /// Applies size, line height and weight from one typographic step.
37    fn type_scale(self, theme: &Theme, scale: TypeScale) -> Self {
38        let style = theme.type_style(scale);
39        self.text_size(px(style.size))
40            .line_height(px(style.line_height))
41            .font_weight(FontWeight(style.weight))
42    }
43
44    fn radius(self, theme: &Theme, radius: Radius) -> Self {
45        self.rounded(px(theme.radius(radius)))
46    }
47
48    fn gap_token(self, theme: &Theme, space: Space) -> Self {
49        self.gap(px(theme.space(space)))
50    }
51
52    fn p_token(self, theme: &Theme, space: Space) -> Self {
53        self.p(px(theme.space(space)))
54    }
55
56    fn px_token(self, theme: &Theme, space: Space) -> Self {
57        self.px(px(theme.space(space)))
58    }
59
60    fn py_token(self, theme: &Theme, space: Space) -> Self {
61        self.py(px(theme.space(space)))
62    }
63
64    fn mt_token(self, theme: &Theme, space: Space) -> Self {
65        self.mt(px(theme.space(space)))
66    }
67
68    /// Applies the shadow for an elevation step. Flat applies nothing.
69    fn elevation(self, theme: &Theme, level: Elevation) -> Self {
70        let shadow = theme.shadow(level);
71        if shadow.is_empty() {
72            self
73        } else {
74            self.shadow(shadow.to_vec())
75        }
76    }
77
78    fn hairline(self, theme: &Theme) -> Self {
79        self.border(px(theme.borders.hairline))
80            .border_color(theme.colors.hairline)
81    }
82
83    fn hairline_strong(self, theme: &Theme) -> Self {
84        self.border(px(theme.borders.hairline))
85            .border_color(theme.colors.hairline_strong)
86    }
87
88    /// A surface that is a distinct thing from the one behind it.
89    ///
90    /// The colour step and the shadow do the separating, which is why a card,
91    /// a popover and a dialog carry no line around them: a line drawn around
92    /// something already legible is decoration, and this library reserves
93    /// lines for what they alone can say — focus, invalidity, a drop target.
94    fn frame(self, theme: &Theme, surface: Surface, level: Elevation) -> Self {
95        self.surface(theme, surface).elevation(theme, level)
96    }
97
98    /// The recess an editable value sits in.
99    ///
100    /// A field is a well rather than an outlined box, so the resting state of
101    /// every editable control in the library is a colour and nothing else.
102    /// The border it still carries is transparent and exists only to hold the
103    /// space that an invalid state will colour, so becoming invalid never
104    /// reflows the row the field is in.
105    fn well(self, theme: &Theme) -> Self {
106        self.surface(theme, Surface::Sunken)
107            .border(px(theme.borders.hairline))
108            .border_color(gpui::transparent_black())
109    }
110
111    /// The colour a surface in a named state bleeds into the pixels around it.
112    fn glow(self, theme: &Theme, color: gpui::Hsla) -> Self {
113        self.shadow(theme.glow(color))
114    }
115
116    /// A horizontal flex row, the layout most component frames start from.
117    fn row(self) -> Self {
118        self.flex().flex_row().items_center()
119    }
120
121    fn column(self) -> Self {
122        self.flex().flex_col()
123    }
124}
125
126impl<T: Styled + Sized> StyledExt for T {}
127
128/// The one focus treatment in the library.
129///
130/// Every keyboard-reachable element wears the same ring from the same tokens,
131/// so "the keyboard is here" looks identical whether it is on a button, a
132/// table header, or a tree row. It is a shadow rather than a border so turning
133/// focus on never reflows what is around it.
134pub trait FocusRing: InteractiveElement + Sized {
135    fn focus_ring(self, theme: &Theme) -> Self {
136        self.focus(|style| {
137            style
138                .border_color(theme.colors.focus)
139                .shadow(theme.focus_ring())
140        })
141    }
142}
143
144impl<T: InteractiveElement + Sized> FocusRing for T {}