1use std::cell::RefCell;
18use std::sync::atomic::{AtomicU64, Ordering};
19
20use crate::color::Color;
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
28pub enum ThemePreference {
29 #[default]
30 Dark,
31 Light,
32 System,
34}
35
36impl ThemePreference {
37 pub fn key(self) -> &'static str {
38 match self {
39 ThemePreference::Dark => "dark",
40 ThemePreference::Light => "light",
41 ThemePreference::System => "system",
42 }
43 }
44
45 pub fn from_key(key: &str) -> Option<Self> {
46 match key {
47 "dark" => Some(ThemePreference::Dark),
48 "light" => Some(ThemePreference::Light),
49 "system" => Some(ThemePreference::System),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
57pub enum AccentColor {
58 #[default]
59 Blue,
60 Purple,
61 Pink,
62 Red,
63 Orange,
64 Yellow,
65 Green,
66 Teal,
67}
68
69impl AccentColor {
70 pub const ALL: [AccentColor; 8] = [
71 AccentColor::Blue,
72 AccentColor::Purple,
73 AccentColor::Pink,
74 AccentColor::Red,
75 AccentColor::Orange,
76 AccentColor::Yellow,
77 AccentColor::Green,
78 AccentColor::Teal,
79 ];
80
81 pub fn color(self) -> Color {
82 match self {
83 AccentColor::Blue => Color::rgb(0.22, 0.45, 0.88),
84 AccentColor::Purple => Color::rgb(0.48, 0.36, 0.86),
85 AccentColor::Pink => Color::rgb(0.78, 0.28, 0.58),
86 AccentColor::Red => Color::rgb(0.82, 0.24, 0.24),
87 AccentColor::Orange => Color::rgb(0.90, 0.46, 0.18),
88 AccentColor::Yellow => Color::rgb(0.82, 0.62, 0.16),
89 AccentColor::Green => Color::rgb(0.20, 0.62, 0.34),
90 AccentColor::Teal => Color::rgb(0.14, 0.62, 0.66),
91 }
92 }
93
94 pub fn label(self) -> &'static str {
95 match self {
96 AccentColor::Blue => "Blue",
97 AccentColor::Purple => "Purple",
98 AccentColor::Pink => "Pink",
99 AccentColor::Red => "Red",
100 AccentColor::Orange => "Orange",
101 AccentColor::Yellow => "Yellow",
102 AccentColor::Green => "Green",
103 AccentColor::Teal => "Teal",
104 }
105 }
106
107 pub fn key(self) -> &'static str {
108 match self {
109 AccentColor::Blue => "blue",
110 AccentColor::Purple => "purple",
111 AccentColor::Pink => "pink",
112 AccentColor::Red => "red",
113 AccentColor::Orange => "orange",
114 AccentColor::Yellow => "yellow",
115 AccentColor::Green => "green",
116 AccentColor::Teal => "teal",
117 }
118 }
119
120 pub fn from_key(key: &str) -> Option<Self> {
121 match key {
122 "blue" => Some(AccentColor::Blue),
123 "purple" => Some(AccentColor::Purple),
124 "pink" => Some(AccentColor::Pink),
125 "red" => Some(AccentColor::Red),
126 "orange" => Some(AccentColor::Orange),
127 "yellow" => Some(AccentColor::Yellow),
128 "green" => Some(AccentColor::Green),
129 "teal" => Some(AccentColor::Teal),
130 _ => None,
131 }
132 }
133}
134
135#[derive(Clone, Debug)]
147pub struct Visuals {
148 pub bg_color: Color,
151 pub panel_fill: Color,
153 pub top_bar_bg: Color,
155
156 pub window_fill: Color,
159 pub window_title_fill: Color,
161 pub window_title_fill_drag: Color,
163 pub window_shadow: Color,
165 pub window_stroke: Color,
167 pub window_title_text: Color,
169 pub window_close_bg: Color,
171 pub window_close_bg_hovered: Color,
173 pub window_close_fg: Color,
175 pub window_resize_hover: Color,
177 pub window_resize_active: Color,
179
180 pub text_color: Color,
183 pub text_dim: Color,
185 pub text_link: Color,
187 pub text_link_hovered: Color,
189
190 pub accent: Color,
193 pub accent_hovered: Color,
195 pub accent_pressed: Color,
197 pub accent_focus: Color,
199
200 pub widget_bg: Color,
203 pub widget_bg_hovered: Color,
205 pub widget_stroke: Color,
207 pub widget_stroke_active: Color,
209
210 pub track_bg: Color,
212
213 pub scroll_track: Color,
215 pub scroll_thumb: Color,
216 pub scroll_thumb_hovered: Color,
217 pub scroll_thumb_dragging: Color,
218
219 pub separator: Color,
221
222 pub selection_bg: Color,
225 pub selection_bg_unfocused: Color,
228}
229
230impl Visuals {
231 fn accent_hovered(accent: Color) -> Color {
232 if accent == AccentColor::Blue.color() {
233 return Color::rgb(0.30, 0.52, 0.92);
234 }
235 mix_color(accent, Color::white(), 0.18)
236 }
237
238 fn accent_pressed(accent: Color) -> Color {
239 if accent == AccentColor::Blue.color() {
240 return Color::rgb(0.16, 0.36, 0.72);
241 }
242 mix_color(accent, Color::black(), 0.18)
243 }
244
245 pub fn with_accent(mut self, accent: Color) -> Self {
247 let hovered = Self::accent_hovered(accent);
248 let pressed = Self::accent_pressed(accent);
249 let dark =
250 0.299 * self.bg_color.r + 0.587 * self.bg_color.g + 0.114 * self.bg_color.b < 0.5;
251 self.accent = accent;
252 self.accent_hovered = hovered;
253 self.accent_pressed = pressed;
254 self.accent_focus = accent.with_alpha(0.45);
255 self.text_link = if dark { hovered } else { pressed };
256 self.text_link_hovered = if dark {
257 mix_color(hovered, Color::white(), 0.12)
258 } else {
259 accent
260 };
261 self.widget_stroke_active = pressed;
262 self.selection_bg = accent.with_alpha(0.45);
263 self
264 }
265
266 pub fn with_accent_color(self, accent: AccentColor) -> Self {
268 self.with_accent(accent.color())
269 }
270
271 pub fn dark() -> Self {
273 let accent = Color::rgb(0.22, 0.45, 0.88);
274 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
275 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
276 Self {
277 bg_color: Color::rgb(0.10, 0.10, 0.12),
279 panel_fill: Color::rgb(0.13, 0.13, 0.15),
280 top_bar_bg: Color::rgb(0.15, 0.15, 0.17),
281 window_fill: Color::rgb(0.15, 0.15, 0.18),
283 window_title_fill: Color::rgb(0.20, 0.20, 0.24),
284 window_title_fill_drag: Color::rgb(0.16, 0.16, 0.20),
285 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.35),
286 window_stroke: Color::rgba(1.0, 1.0, 1.0, 0.08),
287 window_title_text: Color::rgba(1.0, 1.0, 1.0, 0.90),
288 window_close_bg: Color::rgba(1.0, 1.0, 1.0, 0.12),
289 window_close_bg_hovered: Color::rgba(1.0, 1.0, 1.0, 0.25),
290 window_close_fg: Color::rgba(1.0, 1.0, 1.0, 0.80),
291 window_resize_hover: Color::rgba(1.0, 1.0, 1.0, 0.40),
292 window_resize_active: Color::rgba(1.0, 1.0, 1.0, 0.80),
293 text_color: Color::rgb(0.90, 0.90, 0.92),
295 text_dim: Color::rgba(0.90, 0.90, 0.92, 0.50),
296 text_link: Color::rgb(0.45, 0.65, 1.00),
297 text_link_hovered: Color::rgb(0.35, 0.55, 0.90),
298 accent,
300 accent_hovered,
301 accent_pressed,
302 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
303 widget_bg: Color::rgb(0.22, 0.22, 0.26),
305 widget_bg_hovered: Color::rgb(0.28, 0.28, 0.33),
306 widget_stroke: Color::rgba(0.60, 0.60, 0.65, 0.60),
307 widget_stroke_active: accent_pressed,
308 track_bg: Color::rgb(0.25, 0.25, 0.28),
310 scroll_track: Color::rgba(1.0, 1.0, 1.0, 0.04),
312 scroll_thumb: Color::rgba(1.0, 1.0, 1.0, 0.18),
313 scroll_thumb_hovered: Color::rgba(1.0, 1.0, 1.0, 0.32),
314 scroll_thumb_dragging: Color::rgba(1.0, 1.0, 1.0, 0.45),
315 separator: Color::rgba(1.0, 1.0, 1.0, 0.10),
317 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
319 selection_bg_unfocused: Color::rgba(0.60, 0.60, 0.65, 0.35),
320 }
321 }
322
323 pub fn light() -> Self {
325 let accent = Color::rgb(0.22, 0.45, 0.88);
326 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
327 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
328 Self {
329 bg_color: Color::rgb(0.90, 0.90, 0.92),
331 panel_fill: Color::rgb(0.92, 0.92, 0.95),
332 top_bar_bg: Color::rgb(0.88, 0.88, 0.91),
333 window_fill: Color::rgb(0.97, 0.97, 0.98),
335 window_title_fill: Color::rgb(0.87, 0.87, 0.91),
336 window_title_fill_drag: Color::rgb(0.80, 0.80, 0.85),
337 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.18),
338 window_stroke: Color::rgba(0.0, 0.0, 0.0, 0.15),
339 window_title_text: Color::rgba(0.05, 0.05, 0.10, 0.90),
340 window_close_bg: Color::rgba(0.0, 0.0, 0.0, 0.08),
341 window_close_bg_hovered: Color::rgba(0.0, 0.0, 0.0, 0.18),
342 window_close_fg: Color::rgba(0.0, 0.0, 0.0, 0.65),
343 window_resize_hover: Color::rgba(0.0, 0.0, 0.0, 0.30),
344 window_resize_active: Color::rgba(0.0, 0.0, 0.0, 0.65),
345 text_color: Color::rgb(0.08, 0.08, 0.10),
347 text_dim: Color::rgba(0.08, 0.08, 0.10, 0.50),
348 text_link: Color::rgb(0.15, 0.35, 0.75),
349 text_link_hovered: Color::rgb(0.10, 0.28, 0.62),
350 accent,
352 accent_hovered,
353 accent_pressed,
354 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
355 widget_bg: Color::rgb(1.00, 1.00, 1.00),
357 widget_bg_hovered: Color::rgb(0.92, 0.93, 0.95),
358 widget_stroke: Color::rgb(0.75, 0.76, 0.78),
359 widget_stroke_active: accent_pressed,
360 track_bg: Color::rgb(0.85, 0.86, 0.88),
362 scroll_track: Color::rgba(0.0, 0.0, 0.0, 0.04),
364 scroll_thumb: Color::rgba(0.0, 0.0, 0.0, 0.18),
365 scroll_thumb_hovered: Color::rgba(0.0, 0.0, 0.0, 0.32),
366 scroll_thumb_dragging: Color::rgba(0.0, 0.0, 0.0, 0.45),
367 separator: Color::rgba(0.0, 0.0, 0.0, 0.12),
369 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
371 selection_bg_unfocused: Color::rgba(0.45, 0.45, 0.50, 0.35),
372 }
373 }
374
375 pub fn for_preference(pref: ThemePreference) -> Self {
377 match pref {
378 ThemePreference::Light => Self::light(),
379 _ => Self::dark(),
380 }
381 }
382}
383
384fn mix_color(a: Color, b: Color, t: f32) -> Color {
385 let u = t.clamp(0.0, 1.0);
386 Color::rgba(
387 a.r + (b.r - a.r) * u,
388 a.g + (b.g - a.g) * u,
389 a.b + (b.b - a.b) * u,
390 a.a + (b.a - a.a) * u,
391 )
392}
393
394thread_local! {
399 static VISUALS: RefCell<Visuals> = RefCell::new(Visuals::dark());
400}
401
402static VISUALS_EPOCH: AtomicU64 = AtomicU64::new(1);
410
411pub fn current_visuals_epoch() -> u64 {
414 VISUALS_EPOCH.load(Ordering::Relaxed)
415}
416
417pub fn set_visuals(v: Visuals) {
422 VISUALS.with(|cell| *cell.borrow_mut() = v);
423 VISUALS_EPOCH.fetch_add(1, Ordering::Relaxed);
424}
425
426pub fn current_visuals() -> Visuals {
431 VISUALS.with(|cell| cell.borrow().clone())
432}