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