1use std::cell::RefCell;
18use std::sync::atomic::{AtomicU64, Ordering};
19
20use crate::color::Color;
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
28#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
29pub enum ThemePreference {
30 #[default]
31 Dark,
32 Light,
33 System,
35}
36
37impl ThemePreference {
38 pub fn key(self) -> &'static str {
39 match self {
40 ThemePreference::Dark => "dark",
41 ThemePreference::Light => "light",
42 ThemePreference::System => "system",
43 }
44 }
45
46 pub fn from_key(key: &str) -> Option<Self> {
47 match key {
48 "dark" => Some(ThemePreference::Dark),
49 "light" => Some(ThemePreference::Light),
50 "system" => Some(ThemePreference::System),
51 _ => None,
52 }
53 }
54}
55
56#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
58#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
59pub enum AccentColor {
60 #[default]
61 Blue,
62 Purple,
63 Pink,
64 Red,
65 Orange,
66 Yellow,
67 Green,
68 Teal,
69}
70
71impl AccentColor {
72 pub const ALL: [AccentColor; 8] = [
73 AccentColor::Blue,
74 AccentColor::Purple,
75 AccentColor::Pink,
76 AccentColor::Red,
77 AccentColor::Orange,
78 AccentColor::Yellow,
79 AccentColor::Green,
80 AccentColor::Teal,
81 ];
82
83 pub fn color(self) -> Color {
84 match self {
85 AccentColor::Blue => Color::rgb(0.22, 0.45, 0.88),
86 AccentColor::Purple => Color::rgb(0.48, 0.36, 0.86),
87 AccentColor::Pink => Color::rgb(0.78, 0.28, 0.58),
88 AccentColor::Red => Color::rgb(0.82, 0.24, 0.24),
89 AccentColor::Orange => Color::rgb(0.90, 0.46, 0.18),
90 AccentColor::Yellow => Color::rgb(0.82, 0.62, 0.16),
91 AccentColor::Green => Color::rgb(0.20, 0.62, 0.34),
92 AccentColor::Teal => Color::rgb(0.14, 0.62, 0.66),
93 }
94 }
95
96 pub fn label(self) -> &'static str {
97 match self {
98 AccentColor::Blue => "Blue",
99 AccentColor::Purple => "Purple",
100 AccentColor::Pink => "Pink",
101 AccentColor::Red => "Red",
102 AccentColor::Orange => "Orange",
103 AccentColor::Yellow => "Yellow",
104 AccentColor::Green => "Green",
105 AccentColor::Teal => "Teal",
106 }
107 }
108
109 pub fn key(self) -> &'static str {
110 match self {
111 AccentColor::Blue => "blue",
112 AccentColor::Purple => "purple",
113 AccentColor::Pink => "pink",
114 AccentColor::Red => "red",
115 AccentColor::Orange => "orange",
116 AccentColor::Yellow => "yellow",
117 AccentColor::Green => "green",
118 AccentColor::Teal => "teal",
119 }
120 }
121
122 pub fn from_key(key: &str) -> Option<Self> {
123 match key {
124 "blue" => Some(AccentColor::Blue),
125 "purple" => Some(AccentColor::Purple),
126 "pink" => Some(AccentColor::Pink),
127 "red" => Some(AccentColor::Red),
128 "orange" => Some(AccentColor::Orange),
129 "yellow" => Some(AccentColor::Yellow),
130 "green" => Some(AccentColor::Green),
131 "teal" => Some(AccentColor::Teal),
132 _ => None,
133 }
134 }
135}
136
137#[derive(Clone, Debug)]
149#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
150pub struct Visuals {
151 pub bg_color: Color,
154 pub panel_fill: Color,
156 pub top_bar_bg: Color,
158
159 pub window_fill: Color,
162 pub window_title_fill: Color,
164 pub window_title_fill_drag: Color,
166 pub window_shadow: Color,
168 pub window_stroke: Color,
170 pub window_title_text: Color,
172 pub window_close_bg: Color,
174 pub window_close_bg_hovered: Color,
176 pub window_close_fg: Color,
178 pub window_resize_hover: Color,
180 pub window_resize_active: Color,
182
183 pub text_color: Color,
186 pub text_dim: Color,
188 pub text_link: Color,
190 pub text_link_hovered: Color,
192
193 pub accent: Color,
196 pub accent_hovered: Color,
198 pub accent_pressed: Color,
200 pub accent_focus: Color,
202
203 pub widget_bg: Color,
206 pub widget_bg_hovered: Color,
208 pub widget_stroke: Color,
210 pub widget_stroke_active: Color,
212
213 pub track_bg: Color,
215
216 pub scroll_track: Color,
218 pub scroll_thumb: Color,
219 pub scroll_thumb_hovered: Color,
220 pub scroll_thumb_dragging: Color,
221
222 pub separator: Color,
224
225 pub selection_bg: Color,
228 pub selection_bg_unfocused: Color,
231}
232
233impl Visuals {
234 fn accent_hovered(accent: Color) -> Color {
235 if accent == AccentColor::Blue.color() {
236 return Color::rgb(0.30, 0.52, 0.92);
237 }
238 mix_color(accent, Color::white(), 0.18)
239 }
240
241 fn accent_pressed(accent: Color) -> Color {
242 if accent == AccentColor::Blue.color() {
243 return Color::rgb(0.16, 0.36, 0.72);
244 }
245 mix_color(accent, Color::black(), 0.18)
246 }
247
248 pub fn with_accent(mut self, accent: Color) -> Self {
250 let hovered = Self::accent_hovered(accent);
251 let pressed = Self::accent_pressed(accent);
252 let dark =
253 0.299 * self.bg_color.r + 0.587 * self.bg_color.g + 0.114 * self.bg_color.b < 0.5;
254 self.accent = accent;
255 self.accent_hovered = hovered;
256 self.accent_pressed = pressed;
257 self.accent_focus = accent.with_alpha(0.45);
258 self.text_link = if dark { hovered } else { pressed };
259 self.text_link_hovered = if dark {
260 mix_color(hovered, Color::white(), 0.12)
261 } else {
262 accent
263 };
264 self.widget_stroke_active = pressed;
265 self.selection_bg = accent.with_alpha(0.45);
266 self
267 }
268
269 pub fn with_accent_color(self, accent: AccentColor) -> Self {
271 self.with_accent(accent.color())
272 }
273
274 pub fn dark() -> Self {
276 let accent = Color::rgb(0.22, 0.45, 0.88);
277 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
278 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
279 Self {
280 bg_color: Color::rgb(0.10, 0.10, 0.12),
282 panel_fill: Color::rgb(0.13, 0.13, 0.15),
283 top_bar_bg: Color::rgb(0.15, 0.15, 0.17),
284 window_fill: Color::rgb(0.15, 0.15, 0.18),
286 window_title_fill: Color::rgb(0.20, 0.20, 0.24),
287 window_title_fill_drag: Color::rgb(0.16, 0.16, 0.20),
288 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.35),
289 window_stroke: Color::rgba(1.0, 1.0, 1.0, 0.08),
290 window_title_text: Color::rgba(1.0, 1.0, 1.0, 0.90),
291 window_close_bg: Color::rgba(1.0, 1.0, 1.0, 0.12),
292 window_close_bg_hovered: Color::rgba(1.0, 1.0, 1.0, 0.25),
293 window_close_fg: Color::rgba(1.0, 1.0, 1.0, 0.80),
294 window_resize_hover: Color::rgba(1.0, 1.0, 1.0, 0.40),
295 window_resize_active: Color::rgba(1.0, 1.0, 1.0, 0.80),
296 text_color: Color::rgb(0.90, 0.90, 0.92),
298 text_dim: Color::rgba(0.90, 0.90, 0.92, 0.50),
299 text_link: Color::rgb(0.45, 0.65, 1.00),
300 text_link_hovered: Color::rgb(0.35, 0.55, 0.90),
301 accent,
303 accent_hovered,
304 accent_pressed,
305 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
306 widget_bg: Color::rgb(0.22, 0.22, 0.26),
308 widget_bg_hovered: Color::rgb(0.28, 0.28, 0.33),
309 widget_stroke: Color::rgba(0.60, 0.60, 0.65, 0.60),
310 widget_stroke_active: accent_pressed,
311 track_bg: Color::rgb(0.25, 0.25, 0.28),
313 scroll_track: Color::rgba(1.0, 1.0, 1.0, 0.04),
315 scroll_thumb: Color::rgba(1.0, 1.0, 1.0, 0.18),
316 scroll_thumb_hovered: Color::rgba(1.0, 1.0, 1.0, 0.32),
317 scroll_thumb_dragging: Color::rgba(1.0, 1.0, 1.0, 0.45),
318 separator: Color::rgba(1.0, 1.0, 1.0, 0.10),
320 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
322 selection_bg_unfocused: Color::rgba(0.60, 0.60, 0.65, 0.35),
323 }
324 }
325
326 pub fn light() -> Self {
328 let accent = Color::rgb(0.22, 0.45, 0.88);
329 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
330 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
331 Self {
332 bg_color: Color::rgb(0.90, 0.90, 0.92),
334 panel_fill: Color::rgb(0.92, 0.92, 0.95),
335 top_bar_bg: Color::rgb(0.88, 0.88, 0.91),
336 window_fill: Color::rgb(0.97, 0.97, 0.98),
338 window_title_fill: Color::rgb(0.87, 0.87, 0.91),
339 window_title_fill_drag: Color::rgb(0.80, 0.80, 0.85),
340 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.18),
341 window_stroke: Color::rgba(0.0, 0.0, 0.0, 0.15),
342 window_title_text: Color::rgba(0.05, 0.05, 0.10, 0.90),
343 window_close_bg: Color::rgba(0.0, 0.0, 0.0, 0.08),
344 window_close_bg_hovered: Color::rgba(0.0, 0.0, 0.0, 0.18),
345 window_close_fg: Color::rgba(0.0, 0.0, 0.0, 0.65),
346 window_resize_hover: Color::rgba(0.0, 0.0, 0.0, 0.30),
347 window_resize_active: Color::rgba(0.0, 0.0, 0.0, 0.65),
348 text_color: Color::rgb(0.08, 0.08, 0.10),
350 text_dim: Color::rgba(0.08, 0.08, 0.10, 0.50),
351 text_link: Color::rgb(0.15, 0.35, 0.75),
352 text_link_hovered: Color::rgb(0.10, 0.28, 0.62),
353 accent,
355 accent_hovered,
356 accent_pressed,
357 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
358 widget_bg: Color::rgb(1.00, 1.00, 1.00),
360 widget_bg_hovered: Color::rgb(0.92, 0.93, 0.95),
361 widget_stroke: Color::rgb(0.75, 0.76, 0.78),
362 widget_stroke_active: accent_pressed,
363 track_bg: Color::rgb(0.85, 0.86, 0.88),
365 scroll_track: Color::rgba(0.0, 0.0, 0.0, 0.04),
367 scroll_thumb: Color::rgba(0.0, 0.0, 0.0, 0.18),
368 scroll_thumb_hovered: Color::rgba(0.0, 0.0, 0.0, 0.32),
369 scroll_thumb_dragging: Color::rgba(0.0, 0.0, 0.0, 0.45),
370 separator: Color::rgba(0.0, 0.0, 0.0, 0.12),
372 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
374 selection_bg_unfocused: Color::rgba(0.45, 0.45, 0.50, 0.35),
375 }
376 }
377
378 pub fn for_preference(pref: ThemePreference) -> Self {
380 match pref {
381 ThemePreference::Light => Self::light(),
382 _ => Self::dark(),
383 }
384 }
385}
386
387fn mix_color(a: Color, b: Color, t: f32) -> Color {
388 let u = t.clamp(0.0, 1.0);
389 Color::rgba(
390 a.r + (b.r - a.r) * u,
391 a.g + (b.g - a.g) * u,
392 a.b + (b.b - a.b) * u,
393 a.a + (b.a - a.a) * u,
394 )
395}
396
397thread_local! {
402 static VISUALS: RefCell<Visuals> = RefCell::new(Visuals::dark());
403}
404
405static VISUALS_EPOCH: AtomicU64 = AtomicU64::new(1);
413
414pub fn current_visuals_epoch() -> u64 {
417 VISUALS_EPOCH.load(Ordering::Relaxed)
418}
419
420pub fn set_visuals(v: Visuals) {
425 VISUALS.with(|cell| *cell.borrow_mut() = v);
426 VISUALS_EPOCH.fetch_add(1, Ordering::Relaxed);
427}
428
429pub fn current_visuals() -> Visuals {
434 VISUALS.with(|cell| cell.borrow().clone())
435}