Skip to main content

gpui_base/
styled.rs

1use gpui::{
2    App, BoxShadow, Corners, DefiniteLength, Div, Edges, FocusHandle, Hsla, Pixels,
3    Refineable as _, Role, StyleRefinement, Styled, Window, div, hsla, point,
4};
5
6#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7pub enum RoleOverride {
8    #[default]
9    Implicit,
10    Presentational,
11    Role(Role),
12}
13
14impl RoleOverride {
15    pub fn resolve(self, default: impl FnOnce() -> Role) -> Option<Role> {
16        match self {
17            Self::Implicit => Some(default()),
18            Self::Presentational => None,
19            Self::Role(role) => Some(role),
20        }
21    }
22}
23impl From<Role> for RoleOverride {
24    fn from(role: Role) -> Self {
25        Self::Role(role)
26    }
27}
28impl From<Option<Role>> for RoleOverride {
29    fn from(role: Option<Role>) -> Self {
30        role.map_or(Self::Presentational, Self::Role)
31    }
32}
33
34/// A row that centers its children on the cross axis.
35///
36/// See [`StyledExt::h_flex`] for the cross-axis rule, which is not symmetric
37/// with [`v_flex`].
38pub fn h_flex() -> Div {
39    div().h_flex()
40}
41
42/// A column whose children stretch across the cross axis.
43///
44/// See [`StyledExt::v_flex`] for the cross-axis rule, which is not symmetric
45/// with [`h_flex`].
46pub fn v_flex() -> Div {
47    div().v_flex()
48}
49
50pub fn box_shadow(
51    x: impl Into<Pixels>,
52    y: impl Into<Pixels>,
53    blur: impl Into<Pixels>,
54    spread: impl Into<Pixels>,
55    color: Hsla,
56) -> BoxShadow {
57    BoxShadow {
58        offset: point(x.into(), y.into()),
59        blur_radius: blur.into(),
60        spread_radius: spread.into(),
61        inset: false,
62        color,
63    }
64}
65
66macro_rules! font_weight {
67    ($method:ident, $weight:ident) => {
68        fn $method(self) -> Self {
69            self.font_weight(gpui::FontWeight::$weight)
70        }
71    };
72}
73
74#[cfg_attr(
75    any(feature = "inspector", debug_assertions),
76    gpui_macros::derive_inspector_reflection
77)]
78pub trait StyledExt: Styled + Sized {
79    fn refine_style(mut self, style: &StyleRefinement) -> Self {
80        self.style().refine(style);
81        self
82    }
83
84    /// Lays children out in a row, centered on the cross axis.
85    ///
86    /// The centering is the desktop default for a row of controls — an icon
87    /// beside its label lines up without either side asking for it — but it is
88    /// **not** the mirror image of [`Self::v_flex`], which leaves the cross axis
89    /// stretching. A column placed in a row therefore does not take the row's
90    /// height: it takes its content's height and is centered inside the row.
91    /// When its content is taller than the row, it overflows equally above and
92    /// below, so the column's header is pushed off the top edge and clipped.
93    ///
94    /// Give a full-height column `h_full()` (or the row `items_start()` /
95    /// `items_stretch()`) whenever the child owns a header, a footer, or a
96    /// scroll region that has to resolve against the row's height.
97    ///
98    /// ```
99    /// use gpui_base::StyledExt as _;
100    /// use gpui::{ParentElement as _, Styled as _, div};
101    ///
102    /// // A sidebar beside a detail pane, both spanning the full height.
103    /// div().h_flex().size_full().child(div().w_64().h_full());
104    /// ```
105    fn h_flex(self) -> Self {
106        self.flex().flex_row().items_center()
107    }
108
109    /// Lays children out in a column, stretching them across the cross axis.
110    ///
111    /// Unlike [`Self::h_flex`] this installs no cross-axis alignment, so a child
112    /// without a width fills the column. See `h_flex` for the asymmetry.
113    fn v_flex(self) -> Self {
114        self.flex().flex_col()
115    }
116
117    fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
118    where
119        L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
120    {
121        let paddings = paddings.into();
122        self.pt(paddings.top.into())
123            .pb(paddings.bottom.into())
124            .pl(paddings.left.into())
125            .pr(paddings.right.into())
126    }
127
128    fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
129    where
130        L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
131    {
132        let margins = margins.into();
133        self.mt(margins.top.into())
134            .mb(margins.bottom.into())
135            .ml(margins.left.into())
136            .mr(margins.right.into())
137    }
138
139    fn debug_red(self) -> Self {
140        if cfg!(debug_assertions) {
141            self.border_1().border_color(hsl(0., 72.2, 50.6))
142        } else {
143            self
144        }
145    }
146
147    fn debug_blue(self) -> Self {
148        if cfg!(debug_assertions) {
149            self.border_1().border_color(hsl(217.2, 91.2, 59.8))
150        } else {
151            self
152        }
153    }
154
155    fn debug_yellow(self) -> Self {
156        if cfg!(debug_assertions) {
157            self.border_1().border_color(hsl(47.9, 95.8, 53.1))
158        } else {
159            self
160        }
161    }
162
163    fn debug_green(self) -> Self {
164        if cfg!(debug_assertions) {
165            self.border_1().border_color(hsl(142.1, 70.6, 45.3))
166        } else {
167            self
168        }
169    }
170
171    fn debug_pink(self) -> Self {
172        if cfg!(debug_assertions) {
173            self.border_1().border_color(hsl(330.4, 81.2, 60.4))
174        } else {
175            self
176        }
177    }
178
179    fn debug_focused(self, focus_handle: &FocusHandle, window: &Window, cx: &App) -> Self {
180        if cfg!(debug_assertions) && focus_handle.contains_focused(window, cx) {
181            self.debug_blue()
182        } else {
183            self
184        }
185    }
186
187    font_weight!(font_thin, THIN);
188    font_weight!(font_extralight, EXTRA_LIGHT);
189    font_weight!(font_light, LIGHT);
190    font_weight!(font_normal, NORMAL);
191    font_weight!(font_medium, MEDIUM);
192    font_weight!(font_semibold, SEMIBOLD);
193    font_weight!(font_bold, BOLD);
194    font_weight!(font_extrabold, EXTRA_BOLD);
195    font_weight!(font_black, BLACK);
196
197    fn corner_radii(self, radius: Corners<Pixels>) -> Self {
198        self.rounded_tl(radius.top_left)
199            .rounded_tr(radius.top_right)
200            .rounded_bl(radius.bottom_left)
201            .rounded_br(radius.bottom_right)
202    }
203}
204
205impl<E: Styled> StyledExt for E {}
206
207#[cfg(any(feature = "inspector", debug_assertions))]
208pub fn styled_ext_reflection_methods<T: Styled + 'static>()
209-> Vec<gpui::inspector_reflection::FunctionReflection<T>> {
210    styled_ext_reflection::methods::<T>()
211}
212
213fn hsl(hue: f32, saturation: f32, lightness: f32) -> Hsla {
214    hsla(hue / 360., saturation / 100., lightness / 100., 1.)
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use gpui::{
221        Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, TestAppContext,
222        px,
223    };
224
225    fn column(selector: &'static str, height: f32) -> Div {
226        div()
227            .w(px(20.))
228            .h(px(height))
229            .flex_shrink_0()
230            .debug_selector(move || selector.to_string())
231    }
232
233    struct CrossAxisTest;
234
235    impl Render for CrossAxisTest {
236        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
237            div()
238                .w(px(200.))
239                .h(px(100.))
240                .child(
241                    h_flex()
242                        .size_full()
243                        .child(column("row-child", 40.))
244                        .child(column("row-overflowing-child", 140.))
245                        .child(
246                            div()
247                                .w(px(20.))
248                                .debug_selector(|| "row-stretch".to_string()),
249                        ),
250                )
251                .child(
252                    v_flex().size_full().child(
253                        div()
254                            .h(px(20.))
255                            .debug_selector(|| "col-stretch".to_string()),
256                    ),
257                )
258        }
259    }
260
261    /// `h_flex` centers on the cross axis while `v_flex` leaves the default
262    /// stretch. The asymmetry is deliberate but easy to trip over, so lock it.
263    #[gpui::test]
264    fn h_flex_centers_and_v_flex_stretches_on_the_cross_axis(cx: &mut TestAppContext) {
265        let (_, cx) = cx.add_window_view(|_, _| CrossAxisTest);
266        cx.run_until_parked();
267        cx.update(|window, cx| {
268            _ = window.draw(cx);
269        });
270
271        // A shorter child of a 100px row sits at (100 - 40) / 2, not at the top,
272        // and a height-less child keeps its content height instead of filling.
273        let child = cx.debug_bounds("row-child").unwrap();
274        assert_eq!(child.top(), px(30.));
275        assert_eq!(cx.debug_bounds("row-stretch").unwrap().size.height, px(0.));
276
277        // And a child taller than the row is centered too, so its top — a
278        // column's header, in a real layout — is pushed off the top edge.
279        let overflowing = cx.debug_bounds("row-overflowing-child").unwrap();
280        assert_eq!(overflowing.top(), px(-20.));
281
282        // A width-less child of a column does fill the cross axis.
283        assert_eq!(cx.debug_bounds("col-stretch").unwrap().size.width, px(200.));
284    }
285}