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 is_dark(&self) -> bool {
253 0.299 * self.bg_color.r + 0.587 * self.bg_color.g + 0.114 * self.bg_color.b < 0.5
254 }
255
256 pub fn with_accent(mut self, accent: Color) -> Self {
258 let hovered = Self::accent_hovered(accent);
259 let pressed = Self::accent_pressed(accent);
260 let dark = self.is_dark();
261 self.accent = accent;
262 self.accent_hovered = hovered;
263 self.accent_pressed = pressed;
264 self.accent_focus = accent.with_alpha(0.45);
265 self.text_link = if dark { hovered } else { pressed };
266 self.text_link_hovered = if dark {
267 mix_color(hovered, Color::white(), 0.12)
268 } else {
269 accent
270 };
271 self.widget_stroke_active = pressed;
272 self.selection_bg = accent.with_alpha(0.45);
273 self
274 }
275
276 pub fn with_accent_color(self, accent: AccentColor) -> Self {
278 self.with_accent(accent.color())
279 }
280
281 pub fn dark() -> Self {
283 let accent = Color::rgb(0.22, 0.45, 0.88);
284 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
285 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
286 Self {
287 bg_color: Color::rgb(0.10, 0.10, 0.12),
289 panel_fill: Color::rgb(0.13, 0.13, 0.15),
290 top_bar_bg: Color::rgb(0.15, 0.15, 0.17),
291 window_fill: Color::rgb(0.15, 0.15, 0.18),
293 window_title_fill: Color::rgb(0.20, 0.20, 0.24),
294 window_title_fill_drag: Color::rgb(0.16, 0.16, 0.20),
295 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.35),
296 window_stroke: Color::rgba(1.0, 1.0, 1.0, 0.08),
297 window_title_text: Color::rgba(1.0, 1.0, 1.0, 0.90),
298 window_close_bg: Color::rgba(1.0, 1.0, 1.0, 0.12),
299 window_close_bg_hovered: Color::rgba(1.0, 1.0, 1.0, 0.25),
300 window_close_fg: Color::rgba(1.0, 1.0, 1.0, 0.80),
301 window_resize_hover: Color::rgba(1.0, 1.0, 1.0, 0.40),
302 window_resize_active: Color::rgba(1.0, 1.0, 1.0, 0.80),
303 text_color: Color::rgb(0.90, 0.90, 0.92),
305 text_dim: Color::rgba(0.90, 0.90, 0.92, 0.50),
306 text_link: Color::rgb(0.45, 0.65, 1.00),
307 text_link_hovered: Color::rgb(0.35, 0.55, 0.90),
308 accent,
310 accent_hovered,
311 accent_pressed,
312 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
313 widget_bg: Color::rgb(0.22, 0.22, 0.26),
315 widget_bg_hovered: Color::rgb(0.28, 0.28, 0.33),
316 widget_stroke: Color::rgba(0.60, 0.60, 0.65, 0.60),
317 widget_stroke_active: accent_pressed,
318 track_bg: Color::rgb(0.25, 0.25, 0.28),
320 scroll_track: Color::rgba(1.0, 1.0, 1.0, 0.04),
322 scroll_thumb: Color::rgba(1.0, 1.0, 1.0, 0.18),
323 scroll_thumb_hovered: Color::rgba(1.0, 1.0, 1.0, 0.32),
324 scroll_thumb_dragging: Color::rgba(1.0, 1.0, 1.0, 0.45),
325 separator: Color::rgba(1.0, 1.0, 1.0, 0.10),
327 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
329 selection_bg_unfocused: Color::rgba(0.60, 0.60, 0.65, 0.35),
330 }
331 }
332
333 pub fn light() -> Self {
335 let accent = Color::rgb(0.22, 0.45, 0.88);
336 let accent_hovered = Color::rgb(0.30, 0.52, 0.92);
337 let accent_pressed = Color::rgb(0.16, 0.36, 0.72);
338 Self {
339 bg_color: Color::rgb(0.90, 0.90, 0.92),
341 panel_fill: Color::rgb(0.92, 0.92, 0.95),
342 top_bar_bg: Color::rgb(0.88, 0.88, 0.91),
343 window_fill: Color::rgb(0.97, 0.97, 0.98),
345 window_title_fill: Color::rgb(0.87, 0.87, 0.91),
346 window_title_fill_drag: Color::rgb(0.80, 0.80, 0.85),
347 window_shadow: Color::rgba(0.0, 0.0, 0.0, 0.18),
348 window_stroke: Color::rgba(0.0, 0.0, 0.0, 0.15),
349 window_title_text: Color::rgba(0.05, 0.05, 0.10, 0.90),
350 window_close_bg: Color::rgba(0.0, 0.0, 0.0, 0.08),
351 window_close_bg_hovered: Color::rgba(0.0, 0.0, 0.0, 0.18),
352 window_close_fg: Color::rgba(0.0, 0.0, 0.0, 0.65),
353 window_resize_hover: Color::rgba(0.0, 0.0, 0.0, 0.30),
354 window_resize_active: Color::rgba(0.0, 0.0, 0.0, 0.65),
355 text_color: Color::rgb(0.08, 0.08, 0.10),
357 text_dim: Color::rgba(0.08, 0.08, 0.10, 0.50),
358 text_link: Color::rgb(0.15, 0.35, 0.75),
359 text_link_hovered: Color::rgb(0.10, 0.28, 0.62),
360 accent,
362 accent_hovered,
363 accent_pressed,
364 accent_focus: Color::rgba(0.22, 0.45, 0.88, 0.45),
365 widget_bg: Color::rgb(1.00, 1.00, 1.00),
367 widget_bg_hovered: Color::rgb(0.92, 0.93, 0.95),
368 widget_stroke: Color::rgb(0.75, 0.76, 0.78),
369 widget_stroke_active: accent_pressed,
370 track_bg: Color::rgb(0.85, 0.86, 0.88),
372 scroll_track: Color::rgba(0.0, 0.0, 0.0, 0.04),
374 scroll_thumb: Color::rgba(0.0, 0.0, 0.0, 0.18),
375 scroll_thumb_hovered: Color::rgba(0.0, 0.0, 0.0, 0.32),
376 scroll_thumb_dragging: Color::rgba(0.0, 0.0, 0.0, 0.45),
377 separator: Color::rgba(0.0, 0.0, 0.0, 0.12),
379 selection_bg: Color::rgba(0.22, 0.45, 0.88, 0.45),
381 selection_bg_unfocused: Color::rgba(0.45, 0.45, 0.50, 0.35),
382 }
383 }
384
385 pub fn for_preference(pref: ThemePreference) -> Self {
387 match pref {
388 ThemePreference::Light => Self::light(),
389 _ => Self::dark(),
390 }
391 }
392}
393
394fn mix_color(a: Color, b: Color, t: f32) -> Color {
395 let u = t.clamp(0.0, 1.0);
396 Color::rgba(
397 a.r + (b.r - a.r) * u,
398 a.g + (b.g - a.g) * u,
399 a.b + (b.b - a.b) * u,
400 a.a + (b.a - a.a) * u,
401 )
402}
403
404thread_local! {
409 static VISUALS: RefCell<Visuals> = RefCell::new(Visuals::dark());
410}
411
412static VISUALS_EPOCH: AtomicU64 = AtomicU64::new(1);
420
421pub fn current_visuals_epoch() -> u64 {
424 VISUALS_EPOCH.load(Ordering::Relaxed)
425}
426
427pub fn set_visuals(v: Visuals) {
432 VISUALS.with(|cell| *cell.borrow_mut() = v);
433 VISUALS_EPOCH.fetch_add(1, Ordering::Relaxed);
434}
435
436pub fn current_visuals() -> Visuals {
441 VISUALS.with(|cell| cell.borrow().clone())
442}