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