1use eframe::egui::{self, Color32, CornerRadius, Theme, Visuals};
5use serde::{Deserialize, Serialize};
6use std::sync::OnceLock;
7
8fn reg_dword(root: winreg::HKEY, path: &str, name: &str) -> Option<u32> {
9 winreg::RegKey::predef(root).open_subkey(path).ok()?.get_value::<u32, _>(name).ok()
10}
11
12pub fn system_accent() -> Color32 {
14 static ACCENT: OnceLock<Color32> = OnceLock::new();
15 *ACCENT.get_or_init(|| {
16 reg_dword(winreg::enums::HKEY_CURRENT_USER, r"Software\Microsoft\Windows\DWM", "AccentColor")
17 .map(|v| Color32::from_rgb((v & 0xff) as u8, ((v >> 8) & 0xff) as u8, ((v >> 16) & 0xff) as u8))
18 .unwrap_or(Color32::from_rgb(0, 120, 212))
19 })
20}
21
22#[derive(Clone, Copy, Debug)]
24pub struct Palette {
25 pub accent: Color32,
26 pub bg: Color32,
27 pub panel: Color32,
28 pub header: Color32,
29 pub border: Color32,
30 pub text: Color32,
31 pub text_dim: Color32,
32 pub clip_video: Color32,
33 pub clip_audio: Color32,
34 pub clip_image: Color32,
35 pub clip_text: Color32,
36 pub clip_sequence: Color32,
37 pub clip_shape: Color32,
38 pub clip_adjust: Color32,
39 pub waveform: Color32,
40 pub playhead: Color32,
41 pub in_out: Color32,
42 pub keyframe: Color32,
43 pub selection: Color32,
44}
45
46impl Palette {
47 pub fn new(dark: bool, accent: Color32) -> Self {
48 let g = |v: u8| Color32::from_gray(v);
49 if dark {
50 Self {
51 accent,
52 bg: g(28),
53 panel: g(38),
54 header: g(46),
55 border: g(62),
56 text: g(225),
57 text_dim: g(150),
58 clip_video: Color32::from_rgb(62, 98, 142),
59 clip_audio: Color32::from_rgb(56, 120, 84),
60 clip_image: Color32::from_rgb(110, 84, 140),
61 clip_text: Color32::from_rgb(150, 112, 58),
62 clip_sequence: Color32::from_rgb(60, 130, 140),
63 clip_shape: Color32::from_rgb(120, 100, 60),
64 clip_adjust: Color32::from_rgb(96, 96, 110),
65 waveform: Color32::from_rgb(150, 215, 170),
66 playhead: Color32::from_rgb(235, 70, 70),
67 in_out: accent,
68 keyframe: Color32::from_rgb(255, 200, 60),
69 selection: accent,
70 }
71 } else {
72 Self {
73 accent,
74 bg: g(250),
75 panel: g(240),
76 header: g(228),
77 border: g(200),
78 text: g(20),
79 text_dim: g(100),
80 clip_video: Color32::from_rgb(150, 180, 220),
81 clip_audio: Color32::from_rgb(150, 205, 170),
82 clip_image: Color32::from_rgb(190, 170, 215),
83 clip_text: Color32::from_rgb(225, 195, 140),
84 clip_sequence: Color32::from_rgb(150, 205, 215),
85 clip_shape: Color32::from_rgb(225, 210, 160),
86 clip_adjust: Color32::from_rgb(190, 190, 205),
87 waveform: Color32::from_rgb(30, 110, 60),
88 playhead: Color32::from_rgb(220, 40, 40),
89 in_out: accent,
90 keyframe: Color32::from_rgb(200, 140, 0),
91 selection: accent,
92 }
93 }
94 }
95 pub fn clip_color(&self, kind: crate::model::ClipKind) -> Color32 {
96 use crate::model::ClipKind::*;
97 match kind {
98 Video => self.clip_video,
99 Audio => self.clip_audio,
100 Image => self.clip_image,
101 Text => self.clip_text,
102 Sequence => self.clip_sequence,
103 Shape => self.clip_shape,
104 Adjustment => self.clip_adjust,
105 }
106 }
107}
108
109#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
114#[serde(default)]
115pub struct PaletteOverride {
116 pub mode: String,
117 pub background: Option<[u8; 3]>,
118 pub panel: Option<[u8; 3]>,
119 pub header: Option<[u8; 3]>,
120 pub border: Option<[u8; 3]>,
121 pub text: Option<[u8; 3]>,
122 pub text_dim: Option<[u8; 3]>,
123 pub accent: Option<[u8; 3]>,
124 pub selection: Option<[u8; 3]>,
125 pub keyframe: Option<[u8; 3]>,
126 pub waveform: Option<[u8; 3]>,
127}
128
129pub fn palette_with(ctx: &egui::Context, ov: &PaletteOverride) -> Palette {
132 let dark = match ov.mode.as_str() {
133 "light" => false,
134 "dark" => true,
135 "custom" => ctx.style().visuals.dark_mode,
136 _ => return palette(ctx),
137 };
138 let rgb = |c: [u8; 3]| Color32::from_rgb(c[0], c[1], c[2]);
139 let mut p = Palette::new(dark, ov.accent.map(rgb).unwrap_or_else(system_accent));
140 if let Some(c) = ov.background {
141 p.bg = rgb(c);
142 }
143 if let Some(c) = ov.panel {
144 p.panel = rgb(c);
145 }
146 if let Some(c) = ov.header {
147 p.header = rgb(c);
148 }
149 if let Some(c) = ov.border {
150 p.border = rgb(c);
151 }
152 if let Some(c) = ov.text {
153 p.text = rgb(c);
154 }
155 if let Some(c) = ov.text_dim {
156 p.text_dim = rgb(c);
157 }
158 if let Some(c) = ov.selection {
159 p.selection = rgb(c);
160 }
161 if let Some(c) = ov.keyframe {
162 p.keyframe = rgb(c);
163 }
164 if let Some(c) = ov.waveform {
165 p.waveform = rgb(c);
166 }
167 p
168}
169
170fn visuals(dark: bool, accent: Color32) -> Visuals {
171 let mut v = if dark { Visuals::dark() } else { Visuals::light() };
172 let r = CornerRadius::same(2);
173 v.window_corner_radius = r;
174 v.menu_corner_radius = r;
175 for w in [
176 &mut v.widgets.noninteractive,
177 &mut v.widgets.inactive,
178 &mut v.widgets.hovered,
179 &mut v.widgets.active,
180 &mut v.widgets.open,
181 ] {
182 w.corner_radius = r;
183 }
184 v.selection.bg_fill = if dark { accent.linear_multiply(0.6) } else { accent.linear_multiply(0.35) };
185 v.selection.stroke.color = accent;
186 v.hyperlink_color = accent;
187 v.widgets.active.bg_fill = accent;
188 v.widgets.active.weak_bg_fill = accent;
189 v.widgets.hovered.bg_stroke.color = accent;
190 v.slider_trailing_fill = true;
191 v
192}
193
194fn fonts() -> egui::FontDefinitions {
196 let mut f = egui::FontDefinitions::default();
197 let dir =
198 std::path::PathBuf::from(std::env::var_os("WINDIR").unwrap_or_else(|| r"C:\Windows".into())).join("Fonts");
199 if let Ok(bytes) = std::fs::read(dir.join("segoeui.ttf")) {
200 f.font_data.insert("Segoe UI".into(), std::sync::Arc::new(egui::FontData::from_owned(bytes)));
201 f.families.entry(egui::FontFamily::Proportional).or_default().insert(0, "Segoe UI".into());
202 }
203 f
204}
205
206pub fn apply(ctx: &egui::Context, pref: &str) {
208 ctx.set_fonts(fonts());
211 ctx.options_mut(|o| o.zoom_with_keyboard = false);
212 let accent = system_accent();
213 ctx.set_visuals_of(Theme::Dark, visuals(true, accent));
214 ctx.set_visuals_of(Theme::Light, visuals(false, accent));
215 let theme = match pref {
216 "dark" => egui::ThemePreference::Dark,
217 "light" => egui::ThemePreference::Light,
218 _ => egui::ThemePreference::System,
219 };
220 ctx.set_theme(theme);
221 ctx.style_mut(|s| {
222 s.spacing.item_spacing = egui::vec2(6.0, 4.0);
223 s.spacing.button_padding = egui::vec2(6.0, 2.0);
224 });
225}
226
227pub fn palette(ctx: &egui::Context) -> Palette {
229 Palette::new(ctx.style().visuals.dark_mode, system_accent())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn override_round_trips_and_falls_through() {
238 let ctx = egui::Context::default();
239 let derived = palette_with(&ctx, &PaletteOverride::default());
241 let sys = palette(&ctx);
242 assert_eq!(derived.bg, sys.bg);
243 assert_eq!(derived.accent, sys.accent);
244
245 let mut ov = PaletteOverride { mode: "custom".into(), ..Default::default() };
247 ov.background = Some([10, 20, 30]);
248 ov.accent = Some([200, 30, 40]);
249 let p = palette_with(&ctx, &ov);
250 assert_eq!(p.bg, Color32::from_rgb(10, 20, 30));
251 assert_eq!(p.accent, Color32::from_rgb(200, 30, 40));
252 assert_eq!(p.panel, Palette::new(ctx.style().visuals.dark_mode, Color32::from_rgb(200, 30, 40)).panel);
253
254 let json = serde_json::to_string(&ov).unwrap();
256 let back: PaletteOverride = serde_json::from_str(&json).unwrap();
257 assert_eq!(back, ov);
258 }
259}