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