1use egui::Color32;
21use serde::Deserialize;
22
23#[derive(Clone)]
26pub struct Theme {
27 pub backdrop: Color32,
29 pub bg: Color32,
31 pub card: Color32,
33 pub tile: Color32,
35 pub tile_hover: Color32,
37 pub tile_selected: Color32,
39 pub thumb: Color32,
41 pub text: Color32,
43 pub text_dim: Color32,
45 pub accent: Color32,
47 pub screen_accent: Color32,
49 pub window_accent: Color32,
51
52 pub font: Option<String>,
54 pub font_path: Option<String>,
56 pub cjk_font: Option<String>,
58 pub font_size: Option<f32>,
60}
61
62impl Default for Theme {
63 fn default() -> Self {
64 let c = |r, g, b| Color32::from_rgb(r, g, b);
65 Self {
66 backdrop: Color32::from_rgba_unmultiplied(0, 0, 0, 140),
67 bg: c(0x1e, 0x21, 0x27),
68 card: c(0x21, 0x25, 0x2d),
69 tile: c(0x18, 0x1b, 0x22),
70 tile_hover: c(0x26, 0x2b, 0x33),
71 tile_selected: c(0x3b, 0x42, 0x52),
72 thumb: c(0x12, 0x14, 0x1a),
73 text: c(0xd8, 0xde, 0xe9),
74 text_dim: c(0x7a, 0x82, 0x90),
75 accent: c(0x88, 0xc0, 0xd0),
76 screen_accent: c(0x81, 0xa1, 0xc1), window_accent: c(0xb4, 0x8e, 0xad), font: None,
79 font_path: None,
80 cjk_font: None,
81 font_size: None,
82 }
83 }
84}
85
86#[derive(Deserialize, Default)]
87#[serde(rename_all = "kebab-case", default)]
88struct Raw {
89 backdrop: Option<String>,
90 bg: Option<String>,
91 card: Option<String>,
92 tile: Option<String>,
93 tile_hover: Option<String>,
94 tile_selected: Option<String>,
95 thumb: Option<String>,
96 text: Option<String>,
97 text_dim: Option<String>,
98 accent: Option<String>,
99 screen_accent: Option<String>,
100 window_accent: Option<String>,
101 font: Option<String>,
102 font_path: Option<String>,
103 cjk_font: Option<String>,
104 font_size: Option<f32>,
105}
106
107impl Theme {
108 pub fn load() -> Self {
110 let mut t = Theme::default();
111 let Some(raw) = config_path()
112 .and_then(|p| std::fs::read_to_string(p).ok())
113 .and_then(|s| toml::from_str::<Raw>(&s).ok())
114 else {
115 return t;
116 };
117 let set = |dst: &mut Color32, src: &Option<String>| {
118 if let Some(c) = src.as_deref().and_then(parse_hex) {
119 *dst = c;
120 }
121 };
122 set(&mut t.backdrop, &raw.backdrop);
123 set(&mut t.bg, &raw.bg);
124 set(&mut t.card, &raw.card);
125 set(&mut t.tile, &raw.tile);
126 set(&mut t.tile_hover, &raw.tile_hover);
127 set(&mut t.tile_selected, &raw.tile_selected);
128 set(&mut t.thumb, &raw.thumb);
129 set(&mut t.text, &raw.text);
130 set(&mut t.text_dim, &raw.text_dim);
131 set(&mut t.accent, &raw.accent);
132 set(&mut t.screen_accent, &raw.screen_accent);
133 set(&mut t.window_accent, &raw.window_accent);
134 t.font = raw.font;
135 t.font_path = raw.font_path;
136 t.cjk_font = raw.cjk_font;
137 t.font_size = raw.font_size;
138 t
139 }
140
141 pub fn apply(&self, ctx: &egui::Context) {
143 let mut v = egui::Visuals::dark();
144 v.panel_fill = self.bg;
145 v.window_fill = self.card;
146 v.extreme_bg_color = self.thumb;
147 v.override_text_color = Some(self.text);
148 v.selection.bg_fill = self.accent.gamma_multiply(0.4);
149 v.selection.stroke = egui::Stroke::new(1.0, self.accent);
150 v.hyperlink_color = self.accent;
151 v.widgets.hovered.bg_fill = self.tile_hover;
152 v.widgets.active.bg_fill = self.tile_selected;
153 ctx.set_visuals(v);
154
155 self.install_fonts(ctx);
156 if let Some(sz) = self.font_size {
157 ctx.global_style_mut(|s| {
158 use egui::{FontFamily, FontId, TextStyle};
159 let prop = FontFamily::Proportional;
160 s.text_styles
161 .insert(TextStyle::Body, FontId::new(sz, prop.clone()));
162 s.text_styles
163 .insert(TextStyle::Button, FontId::new(sz, prop.clone()));
164 s.text_styles
165 .insert(TextStyle::Small, FontId::new(sz * 0.85, prop.clone()));
166 s.text_styles
167 .insert(TextStyle::Heading, FontId::new(sz * 1.4, prop));
168 s.text_styles
169 .insert(TextStyle::Monospace, FontId::new(sz, FontFamily::Monospace));
170 });
171 }
172 }
173
174 fn install_fonts(&self, ctx: &egui::Context) {
178 let mut fonts = egui::FontDefinitions::default();
179 let mut db = fontdb::Database::new();
180 db.load_system_fonts();
181
182 let primary = self
184 .font_path
185 .as_deref()
186 .and_then(read_font_file)
187 .or_else(|| self.font.as_deref().and_then(|f| load_family(&db, f)));
188 if let Some(data) = primary {
189 fonts.font_data.insert("ui".into(), data.into());
190 for fam in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
191 fonts
192 .families
193 .entry(fam)
194 .or_default()
195 .insert(0, "ui".into());
196 }
197 }
198
199 let cjk = self
201 .cjk_font
202 .as_deref()
203 .and_then(|f| load_family(&db, f))
204 .or_else(|| CJK_FAMILIES.iter().find_map(|f| load_family(&db, f)));
205 if let Some(data) = cjk {
206 fonts.font_data.insert("cjk".into(), data.into());
207 for fam in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
208 fonts.families.entry(fam).or_default().push("cjk".into());
209 }
210 }
211
212 ctx.set_fonts(fonts);
213 }
214}
215
216const CJK_FAMILIES: &[&str] = &[
218 "Noto Sans CJK JP",
219 "Noto Sans CJK SC",
220 "Noto Sans CJK KR",
221 "Source Han Sans",
222 "Sarasa Gothic",
223 "WenQuanYi Zen Hei",
224];
225
226fn read_font_file(path: &str) -> Option<egui::FontData> {
227 let bytes = std::fs::read(path).ok()?;
228 Some(egui::FontData::from_owned(bytes))
229}
230
231fn load_family(db: &fontdb::Database, family: &str) -> Option<egui::FontData> {
233 let query = fontdb::Query {
234 families: &[fontdb::Family::Name(family)],
235 ..Default::default()
236 };
237 let id = db.query(&query)?;
238 db.with_face_data(id, |bytes, index| {
239 let mut data = egui::FontData::from_owned(bytes.to_vec());
240 data.index = index;
241 data
242 })
243}
244
245fn config_path() -> Option<std::path::PathBuf> {
246 let base = std::env::var_os("XDG_CONFIG_HOME")
247 .map(std::path::PathBuf::from)
248 .or_else(|| {
249 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
250 })?;
251 Some(base.join("wlr-chooser").join("theme.toml"))
252}
253
254fn parse_hex(s: &str) -> Option<Color32> {
256 let h = s.trim().strip_prefix('#')?;
257 let n = |i: usize| u8::from_str_radix(&h[i..i + 2], 16).ok();
258 match h.len() {
259 6 => Some(Color32::from_rgb(n(0)?, n(2)?, n(4)?)),
260 8 => Some(Color32::from_rgba_unmultiplied(n(0)?, n(2)?, n(4)?, n(6)?)),
261 3 => {
262 let d = |i: usize| {
263 let v = u8::from_str_radix(&h[i..i + 1], 16).ok()?;
264 Some(v * 17)
265 };
266 Some(Color32::from_rgb(d(0)?, d(1)?, d(2)?))
267 }
268 _ => None,
269 }
270}