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 {}