Skip to main content

theme/
lib.rs

1//! The app theme — two concrete appearances, one token set.
2//!
3//! Colors are precomputed from an oklch-derived neutral scale (perceptually even
4//! lightness steps; the same scale the reference Tailwind theme used) into gpui
5//! [`Hsla`](gpui::Hsla).
6//! **Numbers drive layout, colors are paint**: layout constants live in
7//! `theme/layout.rs` as plain numbers and never depend on which color is painted.
8//!
9//! # Light is designed, not inverted
10//!
11//! Mirroring lightness produces the classic "washed-out inverted" look, for three
12//! reasons this module handles explicitly:
13//!
14//! 1. **Surface order flips meaning.** In dark, the main content panel is the
15//!    *darkest* plane and raised surfaces get *lighter*. In light, the content
16//!    panel is *white* and the shell/sidebar goes *grey* — chrome recedes by
17//!    getting darker, not lighter. Popovers stay white and earn separation from a
18//!    border and shadow rather than from lightness.
19//! 2. **Elevation reverses.** On dark, a faint *white* wash means "raised". Its
20//!    literal translation — a faint *black* wash on white — means "recessed", so
21//!    the composer read as a dent instead of a plate. Light lifts with white plus
22//!    a border and shadow ([`Theme::input_bg`], the elevation ladder). Fill
23//!    *alphas* carry over unchanged ([`INK_FILL_SCALE`]); only hairlines scale, so
24//!    a 1px edge survives a bright surround ([`INK_HAIRLINE_SCALE`]).
25//! 3. **Accents must move down the scale.** The dark palette's 400-level accents
26//!    (red/amber, and whatever hue an app sets [`Theme::accent`] to) are chosen
27//!    for contrast against near-black; on white they fall to 2–4:1 and fail WCAG
28//!    AA. Light mode uses the 600-level siblings at the same hue, which restores
29//!    the *contrast ratio* the dark token had.
30//!
31//! Text tones are chosen so each light token lands within ~0.5 of its dark
32//! counterpart's contrast ratio against its own background — the pairing is
33//! verified in `tests/theme.rs`, not eyeballed.
34//!
35//! Installed as a gpui [`Global`](gpui::Global) at boot; read with [`Theme::of`].
36
37mod app;
38pub use app::AppExt;
39
40pub mod appearance;
41
42mod brand;
43mod color;
44mod paint;
45mod platform;
46mod selection;
47mod theme;
48
49pub use brand::{BASE_COLORS, Brand, Ink, TextInk, Tint, Vibrancy};
50
51pub use platform::{LENSED, behind_window_blending};
52
53pub use selection::Selection;
54
55pub use color::{
56    LEGIBLE, contrast_ratio, flatten, grey, hsl_to_rgb, ink_over, kept, legible, legible_on,
57    lightness, mix, mix_oklab, neutral, oklch, oklch_to_srgb, over, relative_luminance, rgb_to_hsl,
58    stack, tint,
59};
60pub use paint::{
61    INK_FILL_SCALE, INK_HAIRLINE_SCALE, SCRIM_ALPHA_DARK, band, card_selected_bg,
62    card_selected_shadows, content_shadows, current_appearance, frame_shadows, glass_selected_bg,
63    glass_selected_shadows, hairline, ink, lock_appearance, scrim, set_current_appearance,
64    surface_shadows, theme_generation, user_bubble_bg, wash,
65};
66pub use theme::{
67    ControlSize, Glass, HighlightKind, Material, MaterialSpec, Metrics, Seed, Sizing, SurfaceSpec,
68    SurfaceStyle, SyntaxPalette, TextStyle, Theme, ThemeFamily, Typeset, Variant, base_text_size,
69};
70
71/// The carrier seam for catalog traits: any type holding a [`Theme`] exposes
72/// it through this one method, and every component group
73/// (`ui::widgets::Scaffolding`, …) extends it, so group methods read
74/// the environment through `self.theme()`.
75pub trait ThemeExt {
76    fn theme(&self) -> &Theme;
77}
78
79impl ThemeExt for Theme {
80    fn theme(&self) -> &Theme {
81        self
82    }
83}
84
85/// Which appearance the app is painting.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
87pub enum Appearance {
88    #[default]
89    Dark,
90    Light,
91}
92
93impl Appearance {
94    pub fn is_dark(self) -> bool {
95        matches!(self, Self::Dark)
96    }
97
98    pub fn is_light(self) -> bool {
99        matches!(self, Self::Light)
100    }
101
102    /// Map a gpui window appearance onto ours (both vibrant variants are just
103    /// the blurred flavour of the same tone).
104    pub fn from_window(appearance: gpui::WindowAppearance) -> Self {
105        use gpui::WindowAppearance::*;
106        match appearance {
107            Light | VibrantLight => Self::Light,
108            Dark | VibrantDark => Self::Dark,
109        }
110    }
111}