Skip to main content

guise/
style.rs

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