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}
33
34impl Default for GridTheme {
35    fn default() -> Self {
36        Self {
37            bg: hsla(0.0, 0.0, 1.0, 1.0),
38            header_bg: hsla(0.0, 0.0, 0.93, 1.0),
39            filter_bg: hsla(0.0, 0.0, 0.96, 1.0),
40            filter_active_bg: hsla(0.58, 0.30, 0.85, 1.0),
41            row_header_bg: hsla(0.0, 0.0, 0.90, 1.0),
42            selection_bg: hsla(0.58, 0.50, 0.80, 0.50),
43            alt_row_bg: hsla(0.0, 0.0, 0.95, 1.0),
44            grid_line: hsla(0.0, 0.0, 0.85, 1.0),
45            header_fg: hsla(0.0, 0.0, 0.15, 1.0),
46            text_fg: hsla(0.0, 0.0, 0.1, 1.0),
47            negative_fg: hsla(0.0, 0.75, 0.45, 1.0),
48            sort_indicator: hsla(0.58, 0.50, 0.40, 1.0),
49            filter_cursor: hsla(0.0, 0.0, 0.1, 1.0),
50            menu_bg: hsla(0.0, 0.0, 1.0, 1.0),
51            menu_hover_bg: hsla(0.58, 0.45, 0.85, 1.0),
52            menu_fg: hsla(0.0, 0.0, 0.1, 1.0),
53            muted_text: hsla(0.0, 0.0, 0.5, 1.0),
54        }
55    }
56}
57
58fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
59    Hsla { h, s, l, a }
60}
61
62impl GridTheme {
63    /// The light palette. Identical to [`GridTheme::default`]; provided as a
64    /// named constructor so callers can be explicit about intent.
65    #[must_use]
66    pub fn light() -> Self {
67        Self::default()
68    }
69
70    /// A dark palette tuned to pair with the light one: light text on dark
71    /// surfaces, matching accent hue (0.58) for selection/sort/menu-hover.
72    #[must_use]
73    pub fn dark() -> Self {
74        Self {
75            bg: hsla(0.0, 0.0, 0.12, 1.0),
76            header_bg: hsla(0.0, 0.0, 0.18, 1.0),
77            filter_bg: hsla(0.0, 0.0, 0.15, 1.0),
78            filter_active_bg: hsla(0.58, 0.40, 0.30, 1.0),
79            row_header_bg: hsla(0.0, 0.0, 0.16, 1.0),
80            selection_bg: hsla(0.58, 0.50, 0.45, 0.50),
81            alt_row_bg: hsla(0.0, 0.0, 0.15, 1.0),
82            grid_line: hsla(0.0, 0.0, 0.28, 1.0),
83            header_fg: hsla(0.0, 0.0, 0.80, 1.0),
84            text_fg: hsla(0.0, 0.0, 0.90, 1.0),
85            negative_fg: hsla(0.0, 0.70, 0.62, 1.0),
86            sort_indicator: hsla(0.58, 0.60, 0.68, 1.0),
87            filter_cursor: hsla(0.0, 0.0, 0.90, 1.0),
88            menu_bg: hsla(0.0, 0.0, 0.16, 1.0),
89            menu_hover_bg: hsla(0.58, 0.45, 0.38, 1.0),
90            menu_fg: hsla(0.0, 0.0, 0.90, 1.0),
91            muted_text: hsla(0.0, 0.0, 0.55, 1.0),
92        }
93    }
94
95    /// Pick the palette that matches the OS window appearance. `Dark` and
96    /// `VibrantDark` resolve to [`GridTheme::dark`]; everything else to
97    /// [`GridTheme::light`].
98    #[must_use]
99    pub fn for_appearance(appearance: WindowAppearance) -> Self {
100        match appearance {
101            WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
102            WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    /// The context menu must be paintable from the theme (not a hardcoded
112    /// color), and its hover fill must be visually distinct from the menu
113    /// background so a mouse-over state is actually perceivable. The label
114    /// color must also contrast with the background. This guards the
115    /// dark/light theming + hover-state regression.
116    #[test]
117    fn default_theme_exposes_distinct_menu_colors() {
118        let t = GridTheme::default();
119        // Menu surface must be opaque so it fully covers content beneath it.
120        assert_eq!(t.menu_bg.a, 1.0, "menu background must be opaque");
121        // Hover fill must differ from the surface, else hover is invisible.
122        assert_ne!(
123            t.menu_hover_bg, t.menu_bg,
124            "menu hover fill must differ from the menu background"
125        );
126        // Label color must differ from the surface for legible text.
127        assert_ne!(
128            t.menu_fg, t.menu_bg,
129            "menu label color must contrast with the menu background"
130        );
131    }
132
133    /// `light()` must equal the default palette, and `dark()` must be a
134    /// genuinely different, legible palette (dark surface, light text). This
135    /// guards the OS light/dark following.
136    #[test]
137    fn light_matches_default_and_dark_differs() {
138        assert_eq!(
139            GridTheme::light().bg,
140            GridTheme::default().bg,
141            "light() must alias the default palette"
142        );
143        let dark = GridTheme::dark();
144        assert_ne!(dark.bg, GridTheme::light().bg, "dark bg must differ");
145        // Dark surface should be darker than its text (light-on-dark).
146        assert!(
147            dark.bg.l < dark.text_fg.l,
148            "dark theme must be light text on a dark surface"
149        );
150        assert_eq!(dark.menu_bg.a, 1.0, "dark menu background must be opaque");
151        assert_ne!(
152            dark.menu_hover_bg, dark.menu_bg,
153            "dark menu hover fill must differ from the menu background"
154        );
155    }
156
157    /// `for_appearance` must map the two dark variants to the dark palette and
158    /// the two light variants to the light palette.
159    #[test]
160    fn for_appearance_maps_dark_and_light_variants() {
161        assert_eq!(
162            GridTheme::for_appearance(WindowAppearance::Dark).bg,
163            GridTheme::dark().bg
164        );
165        assert_eq!(
166            GridTheme::for_appearance(WindowAppearance::VibrantDark).bg,
167            GridTheme::dark().bg
168        );
169        assert_eq!(
170            GridTheme::for_appearance(WindowAppearance::Light).bg,
171            GridTheme::light().bg
172        );
173        assert_eq!(
174            GridTheme::for_appearance(WindowAppearance::VibrantLight).bg,
175            GridTheme::light().bg
176        );
177    }
178}