Skip to main content

guise/
style.rs

1//! Shared visual variants. Mantine resolves a `(color, variant)` pair into a
2//! background / foreground / border triple that every interactive component
3//! (Button, Badge, ActionIcon, ...) draws from. This is that resolver.
4
5use gpui::{AlignContent, AlignItems, Div, Hsla, Styled};
6
7use crate::theme::{ColorName, Size, Theme};
8
9/// Apply an element transform — notably a [`style!`](crate::style) block — to
10/// any styled element: `div().apply(style! { … })`.
11pub trait StyleExt: Sized {
12    /// Run `f` on `self` and return the result. `style!` produces exactly the
13    /// `FnOnce(Self) -> Self` this expects.
14    fn apply(self, f: impl FnOnce(Self) -> Self) -> Self {
15        f(self)
16    }
17}
18
19impl<T: Styled> StyleExt for T {}
20
21/// Flex helpers that reach into gpui's [`StyleRefinement`] for things the
22/// stable `Styled` builders can't express: an arbitrary grow/shrink factor
23/// (the built-in `flex_grow`/`flex_shrink` only ever set `1.0`), cross-axis
24/// `stretch`, and `space-evenly` main-axis distribution.
25pub(crate) trait FlexExt: Sized {
26    /// Set an arbitrary `flex-grow` factor.
27    fn grow(self, factor: f32) -> Self;
28    /// Set an arbitrary `flex-shrink` factor.
29    fn shrink(self, factor: f32) -> Self;
30    /// `align-items: stretch`.
31    fn items_stretch(self) -> Self;
32    /// `justify-content: space-evenly`.
33    fn justify_evenly(self) -> Self;
34}
35
36impl FlexExt for Div {
37    fn grow(mut self, factor: f32) -> Self {
38        self.style().flex_grow = Some(factor);
39        self
40    }
41
42    fn shrink(mut self, factor: f32) -> Self {
43        self.style().flex_shrink = Some(factor);
44        self
45    }
46
47    fn items_stretch(mut self) -> Self {
48        self.style().align_items = Some(AlignItems::Stretch);
49        self
50    }
51
52    fn justify_evenly(mut self) -> Self {
53        self.style().justify_content = Some(AlignContent::SpaceEvenly);
54        self
55    }
56}
57
58/// Shared square dimension (px) for icon-style controls (ActionIcon,
59/// CloseButton) across the size scale.
60pub(crate) fn icon_size(size: Size) -> f32 {
61    match size {
62        Size::Xs => 18.0,
63        Size::Sm => 22.0,
64        Size::Md => 28.0,
65        Size::Lg => 34.0,
66        Size::Xl => 44.0,
67    }
68}
69
70/// How a colored component is filled. Matches Mantine's `variant` prop.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum Variant {
73    /// Solid fill in the color (the default for Button).
74    #[default]
75    Filled,
76    /// Tinted background, colored text.
77    Light,
78    /// Transparent background, colored border + text.
79    Outline,
80    /// Transparent until hovered, colored text.
81    Subtle,
82    /// Neutral surface with a border (the gray button).
83    Default,
84    /// No background or border, colored text only.
85    Transparent,
86    /// White background, colored text.
87    White,
88}
89
90/// Resolved colors for one `(color, variant)` pairing.
91#[derive(Debug, Clone, Copy)]
92pub struct Surface {
93    pub bg: Hsla,
94    pub bg_hover: Hsla,
95    pub fg: Hsla,
96    pub border: Option<Hsla>,
97}
98
99/// A color a component can be tinted with: either a palette family (which the
100/// variant resolver expands into shades) or a single explicit color — e.g. from
101/// the [`color!`](crate::color) macro / [`css`](crate::theme::css).
102///
103/// `ColorName` and `Hsla` both `Into<ColorValue>`, so `.color(ColorName::Blue)`
104/// and `.color(color!(rgba(34, 139, 230, 0.5)))` both work.
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub enum ColorValue {
107    /// A named palette family with its full shade ramp.
108    Named(ColorName),
109    /// One explicit color; variant shades are derived from it.
110    Custom(Hsla),
111}
112
113impl Default for ColorValue {
114    fn default() -> Self {
115        ColorValue::Named(ColorName::Blue)
116    }
117}
118
119impl From<ColorName> for ColorValue {
120    fn from(name: ColorName) -> Self {
121        ColorValue::Named(name)
122    }
123}
124
125impl From<Hsla> for ColorValue {
126    fn from(color: Hsla) -> Self {
127        ColorValue::Custom(color)
128    }
129}
130
131impl ColorValue {
132    /// The single accent color (for components that don't go through variants).
133    pub fn accent(self, theme: &Theme) -> Hsla {
134        match self {
135            ColorValue::Named(name) => theme.color(name, theme.primary_shade()).hsla(),
136            ColorValue::Custom(c) => c,
137        }
138    }
139
140    /// The soft (lightly tinted) background used by selected/checked states.
141    pub fn soft(self, theme: &Theme) -> Hsla {
142        let dark = theme.scheme.is_dark();
143        match self {
144            ColorValue::Named(name) if dark => theme.color(name, 5).alpha(0.20),
145            ColorValue::Named(name) => theme.color(name, 0).hsla(),
146            ColorValue::Custom(c) => with_alpha(c, if dark { 0.22 } else { 0.12 }),
147        }
148    }
149}
150
151fn shift_l(c: Hsla, delta: f32) -> Hsla {
152    Hsla {
153        l: (c.l + delta).clamp(0.0, 1.0),
154        ..c
155    }
156}
157
158fn with_alpha(c: Hsla, a: f32) -> Hsla {
159    Hsla { a, ..c }
160}
161
162/// A readable foreground (near-black or near-white) over `c`.
163fn readable_on(c: Hsla) -> Hsla {
164    let v = if c.l > 0.6 { 0.0 } else { 1.0 };
165    Hsla {
166        h: 0.0,
167        s: 0.0,
168        l: v,
169        a: 1.0,
170    }
171}
172
173/// Resolve a color + variant against the theme into drawable colors. Accepts a
174/// palette [`ColorName`] or an explicit [`ColorValue`] (e.g. a `color!(..)`).
175pub fn surface(theme: &Theme, color: impl Into<ColorValue>, variant: Variant) -> Surface {
176    match color.into() {
177        ColorValue::Named(name) => surface_named(theme, name, variant),
178        ColorValue::Custom(c) => surface_custom(theme, c, variant),
179    }
180}
181
182/// Variant resolution for a single explicit color (no palette shades to draw
183/// on, so hover/tints are derived algorithmically).
184fn surface_custom(theme: &Theme, c: Hsla, variant: Variant) -> Surface {
185    let dark = theme.scheme.is_dark();
186    let transparent = gpui::transparent_black();
187    let hover_fill = if dark {
188        shift_l(c, 0.06)
189    } else {
190        shift_l(c, -0.06)
191    };
192    match variant {
193        Variant::Filled => Surface {
194            bg: c,
195            bg_hover: hover_fill,
196            fg: readable_on(c),
197            border: None,
198        },
199        Variant::Light => Surface {
200            bg: with_alpha(c, if dark { 0.20 } else { 0.12 }),
201            bg_hover: with_alpha(c, if dark { 0.28 } else { 0.20 }),
202            fg: c,
203            border: None,
204        },
205        Variant::Outline => Surface {
206            bg: transparent,
207            bg_hover: with_alpha(c, 0.08),
208            fg: c,
209            border: Some(c),
210        },
211        Variant::Subtle => Surface {
212            bg: transparent,
213            bg_hover: with_alpha(c, if dark { 0.15 } else { 0.08 }),
214            fg: c,
215            border: None,
216        },
217        Variant::Default => Surface {
218            bg: theme.surface().hsla(),
219            bg_hover: theme.surface_hover().hsla(),
220            fg: theme.text().hsla(),
221            border: Some(theme.border().hsla()),
222        },
223        Variant::Transparent => Surface {
224            bg: transparent,
225            bg_hover: transparent,
226            fg: c,
227            border: None,
228        },
229        Variant::White => Surface {
230            bg: theme.white.hsla(),
231            bg_hover: theme.color(ColorName::Gray, 0).hsla(),
232            fg: c,
233            border: None,
234        },
235    }
236}
237
238/// Variant resolution for a named palette color (the original Mantine mapping).
239fn surface_named(theme: &Theme, name: ColorName, variant: Variant) -> Surface {
240    let dark = theme.scheme.is_dark();
241    let transparent = gpui::transparent_black();
242    let shade = theme.primary_shade();
243    let filled = theme.color(name, shade);
244    let filled_hover = theme.color(name, (shade + 1).min(9));
245    // The accent shade used for text/border in non-filled variants.
246    let accent = theme.color(name, if dark { 4 } else { 6 });
247
248    match variant {
249        Variant::Filled => Surface {
250            bg: filled.hsla(),
251            bg_hover: filled_hover.hsla(),
252            fg: filled.contrasting().hsla(),
253            border: None,
254        },
255        Variant::Light => {
256            let (bg, bg_hover, fg) = if dark {
257                (
258                    theme.color(name, 5).alpha(0.20),
259                    theme.color(name, 5).alpha(0.30),
260                    theme.color(name, 2).hsla(),
261                )
262            } else {
263                (
264                    theme.color(name, 0).hsla(),
265                    theme.color(name, 1).hsla(),
266                    theme.color(name, 8).hsla(),
267                )
268            };
269            Surface {
270                bg,
271                bg_hover,
272                fg,
273                border: None,
274            }
275        }
276        Variant::Outline => Surface {
277            bg: transparent,
278            bg_hover: accent.alpha(0.08),
279            fg: accent.hsla(),
280            border: Some(accent.hsla()),
281        },
282        Variant::Subtle => Surface {
283            bg: transparent,
284            bg_hover: accent.alpha(if dark { 0.15 } else { 0.08 }),
285            fg: accent.hsla(),
286            border: None,
287        },
288        Variant::Default => Surface {
289            bg: theme.surface().hsla(),
290            bg_hover: theme.surface_hover().hsla(),
291            fg: theme.text().hsla(),
292            border: Some(theme.border().hsla()),
293        },
294        Variant::Transparent => Surface {
295            bg: transparent,
296            bg_hover: transparent,
297            fg: accent.hsla(),
298            border: None,
299        },
300        Variant::White => Surface {
301            bg: theme.white.hsla(),
302            bg_hover: theme.color(ColorName::Gray, 0).hsla(),
303            fg: theme.color(name, shade).hsla(),
304            border: None,
305        },
306    }
307}