Skip to main content

sqlly_datatable/grid/
theme.rs

1//! `GridTheme` — typed color set used by the widget. Default is monochrome on
2//! white; downstream code that wants a dark mode or accent palette can
3//! construct a custom theme and pass it on the [`crate::grid::GridState`].
4
5use gpui::{Hsla, WindowAppearance};
6
7#[derive(Clone, Debug)]
8pub struct GridTheme {
9    pub bg: Hsla,
10    pub header_bg: Hsla,
11    pub filter_bg: Hsla,
12    pub filter_active_bg: Hsla,
13    pub row_header_bg: Hsla,
14    pub selection_bg: Hsla,
15    pub alt_row_bg: Hsla,
16    pub grid_line: Hsla,
17    pub header_fg: Hsla,
18    pub text_fg: Hsla,
19    pub negative_fg: Hsla,
20    pub sort_indicator: Hsla,
21    pub filter_cursor: Hsla,
22    /// Background fill of the right-click context menu / filter popup surface.
23    pub menu_bg: Hsla,
24    /// Fill drawn behind the menu item currently under the pointer (hover).
25    pub menu_hover_bg: Hsla,
26    /// Foreground color for menu item labels.
27    pub menu_fg: Hsla,
28    /// Muted text color for labels, placeholders, and secondary text inside
29    /// the filter panel and context menu. Chosen for legibility against
30    /// `menu_bg` / `bg` in both light and dark palettes.
31    pub muted_text: Hsla,
32    /// Foreground for the null-value placeholder (see
33    /// [`crate::config::NullFormat`]).
34    pub null_fg: Hsla,
35    /// Distinctive background painted behind null-value cells when the
36    /// column's [`crate::config::NullFormat::background`] is enabled.
37    pub null_bg: Hsla,
38}
39
40impl Default for GridTheme {
41    fn default() -> Self {
42        Self {
43            bg: hsla(0.0, 0.0, 1.0, 1.0),
44            header_bg: hsla(0.0, 0.0, 0.93, 1.0),
45            filter_bg: hsla(0.0, 0.0, 0.96, 1.0),
46            filter_active_bg: hsla(0.58, 0.30, 0.85, 1.0),
47            row_header_bg: hsla(0.0, 0.0, 0.90, 1.0),
48            selection_bg: hsla(0.58, 0.50, 0.80, 0.50),
49            alt_row_bg: hsla(0.0, 0.0, 0.95, 1.0),
50            grid_line: hsla(0.0, 0.0, 0.85, 1.0),
51            header_fg: hsla(0.0, 0.0, 0.15, 1.0),
52            text_fg: hsla(0.0, 0.0, 0.1, 1.0),
53            negative_fg: hsla(0.0, 0.75, 0.45, 1.0),
54            sort_indicator: hsla(0.58, 0.50, 0.40, 1.0),
55            filter_cursor: hsla(0.0, 0.0, 0.1, 1.0),
56            menu_bg: hsla(0.0, 0.0, 1.0, 1.0),
57            menu_hover_bg: hsla(0.58, 0.45, 0.85, 1.0),
58            menu_fg: hsla(0.0, 0.0, 0.1, 1.0),
59            muted_text: hsla(0.0, 0.0, 0.5, 1.0),
60            null_fg: hsla(0.0, 0.0, 0.45, 1.0),
61            null_bg: hsla(0.13, 0.55, 0.90, 1.0),
62        }
63    }
64}
65
66fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
67    Hsla { h, s, l, a }
68}
69
70impl GridTheme {
71    /// The light palette. Identical to [`GridTheme::default`]; provided as a
72    /// named constructor so callers can be explicit about intent.
73    #[must_use]
74    pub fn light() -> Self {
75        Self::default()
76    }
77
78    /// A dark palette tuned to pair with the light one: light text on dark
79    /// surfaces, matching accent hue (0.58) for selection/sort/menu-hover.
80    #[must_use]
81    pub fn dark() -> Self {
82        Self {
83            bg: hsla(0.0, 0.0, 0.12, 1.0),
84            header_bg: hsla(0.0, 0.0, 0.18, 1.0),
85            filter_bg: hsla(0.0, 0.0, 0.15, 1.0),
86            filter_active_bg: hsla(0.58, 0.40, 0.30, 1.0),
87            row_header_bg: hsla(0.0, 0.0, 0.16, 1.0),
88            selection_bg: hsla(0.58, 0.50, 0.45, 0.50),
89            alt_row_bg: hsla(0.0, 0.0, 0.15, 1.0),
90            grid_line: hsla(0.0, 0.0, 0.28, 1.0),
91            header_fg: hsla(0.0, 0.0, 0.80, 1.0),
92            text_fg: hsla(0.0, 0.0, 0.90, 1.0),
93            negative_fg: hsla(0.0, 0.70, 0.62, 1.0),
94            sort_indicator: hsla(0.58, 0.60, 0.68, 1.0),
95            filter_cursor: hsla(0.0, 0.0, 0.90, 1.0),
96            menu_bg: hsla(0.0, 0.0, 0.16, 1.0),
97            menu_hover_bg: hsla(0.58, 0.45, 0.38, 1.0),
98            menu_fg: hsla(0.0, 0.0, 0.90, 1.0),
99            muted_text: hsla(0.0, 0.0, 0.55, 1.0),
100            null_fg: hsla(0.0, 0.0, 0.60, 1.0),
101            null_bg: hsla(0.13, 0.35, 0.22, 1.0),
102        }
103    }
104
105    /// Pick the palette that matches the OS window appearance. `Dark` and
106    /// `VibrantDark` resolve to [`GridTheme::dark`]; everything else to
107    /// [`GridTheme::light`].
108    #[must_use]
109    pub fn for_appearance(appearance: WindowAppearance) -> Self {
110        match appearance {
111            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
112            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    /// The context menu must be paintable from the theme (not a hardcoded
122    /// color), and its hover fill must be visually distinct from the menu
123    /// background so a mouse-over state is actually perceivable. The label
124    /// color must also contrast with the background. This guards the
125    /// dark/light theming + hover-state regression.
126    #[test]
127    fn default_theme_exposes_distinct_menu_colors() {
128        let t = GridTheme::default();
129        // Menu surface must be opaque so it fully covers content beneath it.
130        assert_eq!(t.menu_bg.a, 1.0, "menu background must be opaque");
131        // Hover fill must differ from the surface, else hover is invisible.
132        assert_ne!(
133            t.menu_hover_bg, t.menu_bg,
134            "menu hover fill must differ from the menu background"
135        );
136        // Label color must differ from the surface for legible text.
137        assert_ne!(
138            t.menu_fg, t.menu_bg,
139            "menu label color must contrast with the menu background"
140        );
141    }
142
143    /// `light()` must equal the default palette, and `dark()` must be a
144    /// genuinely different, legible palette (dark surface, light text). This
145    /// guards the OS light/dark following.
146    #[test]
147    fn light_matches_default_and_dark_differs() {
148        assert_eq!(
149            GridTheme::light().bg,
150            GridTheme::default().bg,
151            "light() must alias the default palette"
152        );
153        let dark = GridTheme::dark();
154        assert_ne!(dark.bg, GridTheme::light().bg, "dark bg must differ");
155        // Dark surface should be darker than its text (light-on-dark).
156        assert!(
157            dark.bg.l < dark.text_fg.l,
158            "dark theme must be light text on a dark surface"
159        );
160        assert_eq!(dark.menu_bg.a, 1.0, "dark menu background must be opaque");
161        assert_ne!(
162            dark.menu_hover_bg, dark.menu_bg,
163            "dark menu hover fill must differ from the menu background"
164        );
165    }
166
167    /// `for_appearance` must map the two dark variants to the dark palette and
168    /// the two light variants to the light palette.
169    #[test]
170    fn for_appearance_maps_dark_and_light_variants() {
171        assert_eq!(
172            GridTheme::for_appearance(WindowAppearance::Dark).bg,
173            GridTheme::dark().bg
174        );
175        assert_eq!(
176            GridTheme::for_appearance(WindowAppearance::VibrantDark).bg,
177            GridTheme::dark().bg
178        );
179        assert_eq!(
180            GridTheme::for_appearance(WindowAppearance::Light).bg,
181            GridTheme::light().bg
182        );
183        assert_eq!(
184            GridTheme::for_appearance(WindowAppearance::VibrantLight).bg,
185            GridTheme::light().bg
186        );
187    }
188}