Skip to main content

kitchen_ui/theme/
mod.rs

1//! Design tokens shared by the neuro-kitchen apps, and their application to gpui-kit's theme, so
2//! every gpui-kit component (buttons, menus, inputs, tooltips) and the apps' own layout use one
3//! set of values.
4//!
5//! - Spacing on the 8 px grid with a 4 px half-step: [`XS`] 4, [`SM`] 8, [`MD`] 12, [`LG`] 16,
6//!   [`XL`] 24, [`XXL`] 32 (designparser `8pt-grid`, `4pt-system`).
7//! - Type: base [`FONT_SIZE`] 15 px, so gpui's `text_xs` ≈ 11 (captions), `text_sm` ≈ 13 (body),
8//!   `text_base` 15 (titles), `text_lg` ≈ 17 (headings); weights 400 and 600 only. The sizes are
9//!   relative to the base, so a larger base in Settings scales every one of them.
10//! - Radii by tier: [`RADIUS_SM`] 4 (chips, swatches), [`RADIUS`] 6 (controls), [`RADIUS_LG`] 8
11//!   (sections, panes, dialogs) (designparser `corner-radius-tone`: neutral, one per tier).
12//! - Panes are rounded cards [`PANE_GAP`] apart on the chrome, with [`PANE_PADDING`] inside
13//!   (padding ≥ 1.5 × gap: designparser `padding-gap-ratio`).
14//! - Neutrals are shared plain greys ([`color`]); the accent is the app's ([`Palette`]) unless
15//!   the user picked another colour ([`Look::accent`], any [`Rgb`]: made to pass contrast).
16//!
17//! The look (dark or light, palette, accent, text size) is process-wide: set it with [`set_look`], then
18//! [`apply`] hands it to gpui-kit and redraws every window.
19
20pub mod color;
21pub mod palette;
22
23use std::sync::RwLock;
24
25use gpui_kit::component::{Theme, ThemeMode};
26use gpui_kit::{App, Pixels, Window, px, rgba};
27
28pub use color::{Colors, colors, contrast, rgb8, rgb_hsla};
29pub use palette::{Accent, Palette, Rgb};
30
31/// Spacing: a hairline step, icon gaps (px).
32pub const XS: f32 = 4.;
33/// Spacing: the base unit; gaps between controls and rows (px).
34pub const SM: f32 = 8.;
35/// Spacing: padding inside sections and boxes (px).
36pub const MD: f32 = 12.;
37/// Spacing: padding of panels (px).
38pub const LG: f32 = 16.;
39/// Spacing: between groups (px).
40pub const XL: f32 = 24.;
41/// Spacing: around page-level blocks (px).
42pub const XXL: f32 = 32.;
43
44/// Base text size (px) by default.
45pub const FONT_SIZE: f32 = 15.;
46/// Smallest and largest base text size Settings may choose (px).
47pub const FONT_RANGE: (f32, f32) = (12., 18.);
48
49/// Corner radius of small things: chips, swatches, scrub labels (px).
50pub const RADIUS_SM: f32 = 4.;
51/// Corner radius of controls: buttons, inputs, menus (px).
52pub const RADIUS: f32 = 6.;
53/// Corner radius of large things: boxed sections, panes, dialogs (px).
54pub const RADIUS_LG: f32 = 8.;
55
56/// Gap between panes on the chrome (px).
57pub const PANE_GAP: f32 = 4.;
58/// Padding inside a pane (px).
59pub const PANE_PADDING: f32 = 8.;
60
61/// Width of a row's label ([`crate::widgets::row`]) (px).
62pub const LABEL_W: f32 = 112.;
63/// Narrowest a row's control gets before it drops under its label (px).
64pub const CONTROL_MIN_W: f32 = 140.;
65/// Smallest pointer target (px): WCAG 2.2 AA (designparser `touch-target`).
66pub const TARGET_MIN: f32 = 24.;
67
68/// What the user chose: dark or light, an accent of their own or the app's, the base text size.
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct Look {
71    /// The dark theme (`false`: light).
72    pub dark: bool,
73    /// The app's accent colours (its default).
74    pub palette: Palette,
75    /// The accent the user picked instead of the app's, if any (any colour: its light and dark
76    /// accents are made with [`Palette::from_rgb`]). Store it in the app's preferences.
77    pub accent: Option<Rgb>,
78    /// Base text size (px), within [`FONT_RANGE`].
79    pub font_size: f32,
80}
81
82impl Look {
83    /// Dark, `palette`, the default text size.
84    pub const fn new(palette: Palette) -> Self {
85        Look { dark: true, palette, accent: None, font_size: FONT_SIZE }
86    }
87
88    /// The palette in use: the user's accent ([`Palette::custom`]) or the app's.
89    pub fn active_palette(&self) -> Palette {
90        match self.accent {
91            Some(rgb) => Palette::custom(rgb),
92            None => self.palette,
93        }
94    }
95
96    /// The same look, dark or light.
97    pub const fn with_dark(self, dark: bool) -> Self {
98        Look { dark, ..self }
99    }
100
101    /// The same look with another app palette.
102    pub const fn with_palette(self, palette: Palette) -> Self {
103        Look { palette, ..self }
104    }
105
106    /// The same look with the user's accent (`None`: the app's palette).
107    pub const fn with_accent(self, accent: Option<Rgb>) -> Self {
108        Look { accent, ..self }
109    }
110
111    /// The same look with another base text size (kept within [`FONT_RANGE`]).
112    pub fn with_font_size(self, font_size: f32) -> Self {
113        Look { font_size: font_size.round().clamp(FONT_RANGE.0, FONT_RANGE.1), ..self }
114    }
115}
116
117impl Default for Look {
118    fn default() -> Self {
119        Look::new(Palette::LIME)
120    }
121}
122
123static LOOK: RwLock<Look> = RwLock::new(Look::new(Palette::LIME));
124
125/// Sets the look every colour reads. [`apply`] then hands it to gpui-kit.
126pub fn set_look(look: Look) {
127    let look = look.with_font_size(look.font_size);
128    *LOOK.write().unwrap_or_else(|e| e.into_inner()) = look;
129}
130
131/// The current look.
132pub fn look() -> Look {
133    *LOOK.read().unwrap_or_else(|e| e.into_inner())
134}
135
136/// Whether the current look is dark.
137pub fn is_dark() -> bool {
138    look().dark
139}
140
141/// Whether the system shows `window` dark (for a theme that follows the system).
142pub fn system_dark(window: &Window) -> bool {
143    use gpui_kit::WindowAppearance as W;
144    matches!(window.appearance(), W::Dark | W::VibrantDark)
145}
146
147/// Sets the look and applies it ([`set_look`], then [`apply`]).
148pub fn use_look(look: Look, cx: &mut App) {
149    set_look(look);
150    apply(cx);
151}
152
153/// Applies the current look ([`set_look`]) and the tokens to gpui-kit's theme, and redraws every
154/// window.
155pub fn apply(cx: &mut App) {
156    let l = look();
157    Theme::change(if l.dark { ThemeMode::Dark } else { ThemeMode::Light }, None, cx);
158    let c = Colors::resolve(&l);
159    let (on_danger, on_success, on_warning) = (palette::best_text_on(c.danger), palette::best_text_on(c.success), palette::best_text_on(c.warning));
160    Theme::update(cx, |t| {
161        t.font_size = px(l.font_size);
162        t.radius = px(RADIUS);
163        t.radius_lg = px(RADIUS_LG);
164        t.shadow = true;
165        let k = &mut t.colors;
166        k.background = c.panel;
167        k.foreground = c.text;
168        k.muted = c.raised;
169        k.muted_foreground = c.text_muted;
170        k.border = c.border;
171        k.input = c.hover;
172        k.ring = c.accent_mark;
173        k.caret = c.text;
174        k.selection = c.accent.opacity(0.45);
175        k.popover = c.raised;
176        k.popover_foreground = c.text;
177        k.sidebar = c.chrome;
178        k.sidebar_foreground = c.text;
179        k.sidebar_border = c.border;
180        k.sidebar_accent = c.control;
181        k.sidebar_accent_foreground = c.text;
182        k.sidebar_primary = c.accent;
183        k.sidebar_primary_foreground = c.on_accent;
184        k.primary = c.accent;
185        k.primary_hover = c.accent_hover;
186        k.primary_active = c.accent;
187        k.primary_foreground = c.on_accent;
188        k.secondary = c.control;
189        k.secondary_hover = c.hover;
190        k.secondary_active = c.hover;
191        k.secondary_foreground = c.text;
192        k.accent = c.control;
193        k.accent_foreground = c.text;
194        k.title_bar = c.chrome;
195        k.title_bar_border = c.border;
196        k.tab_bar = c.chrome;
197        k.tab = c.chrome;
198        k.tab_active = c.panel;
199        k.tab_foreground = c.text_muted;
200        k.tab_active_foreground = c.text;
201        k.list = c.panel;
202        k.list_even = c.panel;
203        k.list_hover = c.control;
204        k.list_active = c.accent.opacity(0.25);
205        k.list_active_border = c.accent_mark;
206        k.list_head = c.raised;
207        k.table = c.panel;
208        k.table_even = c.raised;
209        k.table_head = c.raised;
210        k.table_head_foreground = c.text_muted;
211        k.table_active = c.accent.opacity(0.25);
212        k.table_active_border = c.accent_mark;
213        k.danger = c.danger;
214        k.danger_foreground = on_danger;
215        k.success = c.success;
216        k.success_foreground = on_success;
217        k.warning = c.warning;
218        k.warning_foreground = on_warning;
219        k.info = c.accent_mark;
220        k.link = c.accent_mark;
221        k.scrollbar = rgba(0x00000000).into();
222        k.scrollbar_thumb = if l.dark { rgba(0xffffff33).into() } else { rgba(0x00000033).into() };
223        k.scrollbar_thumb_hover = if l.dark { rgba(0xffffff55).into() } else { rgba(0x00000055).into() };
224        k.window_border = c.border;
225        k.overlay = rgba(0x00000080).into();
226        k.drag_border = c.accent_mark;
227        k.drop_target = c.accent.opacity(0.2);
228        k.chart_grid = c.border;
229    });
230}
231
232/// `px` of a token.
233pub fn sp(v: f32) -> Pixels {
234    px(v)
235}