1use alloc::boxed::Box;
46use alloc::string::String;
47use alloc::vec::Vec;
48
49use super::consts::*;
50use super::{FontSel, Style, Ui};
51
52#[path = "theme_app.rs"]
53mod app;
54pub use app::{token_index, Radius, TextStyle, Theme, ThemeError, THEME_CONTRAST, THEME_DARK, THEME_LIGHT, TOKEN_NAMES, TS_NAMES};
55pub(crate) use app::parse_color;
56
57pub(crate) struct Look {
59 pub(crate) theme: Option<Box<Theme>>,
61 pub(crate) rad_mode: u8,
63 pub(crate) rad: i32,
64 pub(crate) spacing: i32,
66 pub(crate) ctrl_h: i32,
68 pub(crate) text: bool,
70 pub(crate) fonts: [u32; 3],
71 files: Vec<(String, u32)>,
73 styles: Vec<&'static dyn Style>,
75 pub(crate) serial: u32,
77}
78
79impl Look {
80 pub(crate) fn new(style: &'static dyn Style) -> Look {
81 Look {
82 theme: None,
83 rad_mode: 0,
84 rad: 0,
85 spacing: 100,
86 ctrl_h: 0,
87 text: false,
88 fonts: [FONT_REG, FONT_SEMI, FONT_BOLD],
89 files: Vec::new(),
90 styles: alloc::vec![style],
91 serial: 0,
92 }
93 }
94}
95
96pub(crate) const THEME_N: usize = 64;
98
99pub fn color_mix(a: u32, b: u32, t: i32) -> u32 {
102 let mut r = 0u32;
103 for sh in (0..32).step_by(8) {
104 let x = ((a >> sh) & 255) as i32;
105 let y = ((b >> sh) & 255) as i32;
106 let v = x + (((y - x).wrapping_mul(t)) >> 8);
107 r |= (v as u32) << sh;
108 }
109 r
110}
111
112static SRGB_LIN: [u16; 256] = [
114 0, 20, 40, 60, 80, 99, 119, 139, 159, 179, 199, 219, 241, 264, 288, 313,
115 340, 367, 396, 427, 458, 491, 526, 562, 599, 637, 677, 718, 761, 805, 851, 898,
116 947, 997, 1048, 1101, 1156, 1212, 1270, 1330, 1391, 1453, 1517, 1583, 1651, 1720, 1790, 1863,
117 1937, 2013, 2090, 2170, 2250, 2333, 2418, 2504, 2592, 2681, 2773, 2866, 2961, 3058, 3157, 3258,
118 3360, 3464, 3570, 3678, 3788, 3900, 4014, 4129, 4247, 4366, 4488, 4611, 4736, 4864, 4993, 5124,
119 5257, 5392, 5530, 5669, 5810, 5953, 6099, 6246, 6395, 6547, 6700, 6856, 7014, 7174, 7335, 7500,
120 7666, 7834, 8004, 8177, 8352, 8528, 8708, 8889, 9072, 9258, 9445, 9635, 9828, 10022, 10219, 10417,
121 10619, 10822, 11028, 11235, 11446, 11658, 11873, 12090, 12309, 12530, 12754, 12980, 13209, 13440, 13673, 13909,
122 14146, 14387, 14629, 14874, 15122, 15371, 15623, 15878, 16135, 16394, 16656, 16920, 17187, 17456, 17727, 18001,
123 18277, 18556, 18837, 19121, 19407, 19696, 19987, 20281, 20577, 20876, 21177, 21481, 21787, 22096, 22407, 22721,
124 23038, 23357, 23678, 24002, 24329, 24658, 24990, 25325, 25662, 26001, 26344, 26688, 27036, 27386, 27739, 28094,
125 28452, 28813, 29176, 29542, 29911, 30282, 30656, 31033, 31412, 31794, 32179, 32567, 32957, 33350, 33745, 34143,
126 34544, 34948, 35355, 35764, 36176, 36591, 37008, 37429, 37852, 38278, 38706, 39138, 39572, 40009, 40449, 40891,
127 41337, 41785, 42236, 42690, 43147, 43606, 44069, 44534, 45002, 45473, 45947, 46423, 46903, 47385, 47871, 48359,
128 48850, 49344, 49841, 50341, 50844, 51349, 51858, 52369, 52884, 53401, 53921, 54445, 54971, 55500, 56032, 56567,
129 57105, 57646, 58190, 58737, 59287, 59840, 60396, 60955, 61517, 62082, 62650, 63221, 63795, 64372, 64952, 65535,
130];
131
132pub fn luminance(rgb: u32) -> f32 {
134 let ch = |sh: u32| SRGB_LIN[((rgb >> sh) & 255) as usize] as f32 / 65535.0;
135 0.2126 * ch(16) + 0.7152 * ch(8) + 0.0722 * ch(0)
136}
137
138pub fn contrast_ratio(a: u32, b: u32) -> f32 {
141 let (la, lb) = (luminance(a), luminance(b));
142 let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
143 (hi + 0.05) / (lo + 0.05)
144}
145
146fn luma(rgb: u32) -> u32 {
148 ((rgb >> 16 & 255) * 299 + (rgb >> 8 & 255) * 587 + (rgb & 255) * 114) / 1000
149}
150
151fn look_tokens(t: &mut [u32; T_COUNT], th: &Theme, mode: usize) {
153 for (v, c) in t.iter_mut().zip(th.colors[mode].iter()) {
154 if let Some(c) = c {
155 *v = *c;
156 }
157 }
158}
159
160pub fn contrast_tokens(c: &plat::HcColors) -> [u32; T_COUNT] {
163 let o = |rgb: u32| rgb | 0xFF00_0000;
164 let (bg, text, hl, hlt, btn, btnt, dis, link) =
165 (o(c.bg), o(c.text), o(c.highlight), o(c.highlight_text), o(c.button), o(c.button_text), o(c.disabled), o(c.link));
166 let hover = color_mix(btn, hl, 64);
167 let pressed = color_mix(btn, hl, 112);
168 let mut t = [0u32; T_COUNT];
169 let mut set = |tok: u32, v: u32| t[(tok / 4) as usize] = v;
170 for tok in [T_BG, T_BG_ALT, T_LAYER, T_CARD, T_FLYOUT, T_INPUT_ACTIVE, T_CRIT_BG, T_OK_BG, T_WARN_BG, T_INFO_BG, T_ALT_FILL] {
171 set(tok, bg);
172 }
173 for tok in [T_LAYER_STROKE, T_CARD_STROKE, T_CTRL_STROKE, T_CTRL_STROKE2, T_STRONG_STROKE, T_FLYOUT_STROKE, T_DIVIDER, T_FOCUS_OUT] {
174 set(tok, text);
175 }
176 for tok in [T_TEXT, T_TEXT2, T_TEXT3, T_CRIT, T_OK, T_WARN, T_INFO, T_SCROLL, T_TRACK, T_KNOB, T_STRONG_FILL] {
177 set(tok, text);
178 }
179 set(T_CTRL, btn);
180 set(T_CTRL_HOVER, hover);
181 set(T_CTRL_PRESSED, pressed);
182 set(T_CTRL_DISABLED, btn);
183 set(T_SUBTLE, btn & 0x00FF_FFFF); set(T_SUBTLE_HOVER, hover);
185 set(T_SUBTLE_PRESSED, pressed);
186 set(T_SUBTLE_DISABLED, btn & 0x00FF_FFFF);
187 set(T_TEXT_DISABLED, dis);
188 set(T_ACCENT, hl);
189 set(T_ACCENT_HOVER, hl);
190 set(T_ACCENT_PRESSED, hl);
191 set(T_ACCENT_DISABLED, dis);
192 set(T_ON_ACCENT, hlt);
193 set(T_ON_ACCENT_DIS, bg);
194 set(T_ACCENT_TEXT, link);
195 set(T_FOCUS_IN, bg);
196 set(T_SMOKE, 0xA000_0000);
197 set(T_CLOSE_HOVER, hl);
198 set(T_SHADOW, 0);
199 if btnt != text {
200 set(T_KNOB, btnt);
201 }
202 t
203}
204
205impl Ui {
206 pub fn theme_set(&mut self, mode: u32) {
208 let mut m = mode;
209 self.theme_sys = m == 2;
210 if m == 2 {
211 let t = plat::theme();
212 m = if t < 0 { 1 } else { t as u32 };
213 }
214 let before = self.theme_dark;
215 self.theme_base_dark = m & 1 != 0;
216 self.theme_build();
217 if before != self.theme_dark { self.theme_invalidate(); }
218 }
219
220 pub fn theme_dark(&self) -> bool {
222 self.theme_dark
223 }
224
225 pub fn accent_set(&mut self, rgb: u32) {
227 self.accent = rgb & 0xFF_FFFF;
228 self.accent_sys = false;
229 self.theme_build();
230 }
231
232 pub fn high_contrast(&self) -> bool {
234 self.high_contrast
235 }
236
237 pub fn set_contrast(&mut self, on: Option<bool>) {
239 self.contrast_mode = match on {
240 None => 0,
241 Some(true) => 1,
242 Some(false) => 2,
243 };
244 self.high_contrast = match on {
245 Some(v) => v,
246 None => self.high_contrast,
247 };
248 self.theme_build();
249 }
250
251 pub fn set_reduced_motion(&mut self, on: Option<bool>) {
254 let before = self.anim_off;
255 match on {
256 Some(v) => {
257 self.anim_forced = true;
258 self.anim_off = v;
259 }
260 None => {
261 self.anim_forced = false;
262 self.anim_off = self.anim_sys;
263 }
264 }
265 if before != self.anim_off {
266 self.repass = true;
267 }
268 }
269
270 pub fn text_scale(&self) -> u32 {
272 self.text_scale
273 }
274
275 pub(crate) fn layout_scale(&self) -> u32 {
278 self.text_scale | self.look.serial << 16
279 }
280
281 pub fn font_prefs(&self) -> (Option<bool>, Option<u8>, Option<u8>) {
286 self.font_prefs
287 }
288
289 pub fn set_text_scale(&mut self, percent: u32) {
291 self.text_scale_forced = true;
292 self.text_scale_to(percent);
293 }
294
295 fn text_scale_to(&mut self, percent: u32) {
296 let p = percent.clamp(50, 300);
297 if p != self.text_scale {
298 self.text_scale = p;
299 self.theme_invalidate();
300 self.repass = true;
301 }
302 }
303
304 pub(crate) fn sys_apply(&mut self, s: plat::SysSettings) {
306 if self.theme_sys {
307 if let Some(d) = s.dark {
308 self.theme_base_dark = d;
309 }
310 }
311 if self.accent_sys {
312 self.accent = s.accent.unwrap_or(0) & 0xFF_FFFF;
313 }
314 self.high_contrast = match self.contrast_mode {
315 1 => true,
316 2 => false,
317 _ => s.high_contrast,
318 };
319 self.hc_sys = s.contrast_colors;
320 self.anim_sys = s.reduce_motion;
321 if !self.anim_forced {
322 self.anim_off = s.reduce_motion;
323 }
324 if !self.text_scale_forced {
325 self.text_scale_to(s.text_scale);
326 }
327 self.dbl_ms = s.double_click_ms;
328 self.blink_ms = s.caret_blink_ms;
329 let aa = s.font_aa != Some(false);
333 let order = if aa { s.font_rgba.unwrap_or(0) } else { 0 }; if self.text.antialias() != aa || self.text.subpixel() != order {
335 self.text.set_antialias(aa);
336 self.text.set_subpixel(order);
337 self.theme_invalidate(); }
339 self.font_prefs = (s.font_aa, s.font_hinting, s.font_rgba);
340 self.tb_sys = s.titlebar; self.theme_build();
342 }
343
344 pub(crate) fn theme_build(&mut self) {
347 let before = self.theme_dark;
348 let t = if self.high_contrast {
349 let dark = self.theme_base_dark;
351 let c = self.hc_sys.unwrap_or(if dark { plat::HcColors::BLACK } else { plat::HcColors::WHITE });
352 self.theme_dark = luma(c.bg) < 128;
353 let mut t = contrast_tokens(&c);
354 if let Some(th) = &self.look.theme {
355 if let Some(a) = th.accent[THEME_CONTRAST] {
356 for tok in [T_ACCENT, T_ACCENT_HOVER, T_ACCENT_PRESSED] {
357 t[(tok / 4) as usize] = a | 0xFF00_0000;
358 }
359 }
360 look_tokens(&mut t, th, THEME_CONTRAST);
361 }
362 t
363 } else {
364 self.theme_dark = self.theme_base_dark;
365 let st = self.style;
366 let mut t = st.tokens(self.theme_dark);
367 let mode = if self.theme_dark { THEME_DARK } else { THEME_LIGHT };
368 let acc = self.look.theme.as_ref().and_then(|th| th.accent[mode]).unwrap_or(self.accent);
370 st.accent(&mut t, self.theme_dark, acc); if let Some(th) = &self.look.theme {
372 look_tokens(&mut t, th, mode);
373 }
374 t
375 };
376 let changed = self.theme[..T_COUNT] != t || before != self.theme_dark;
377 self.theme[..T_COUNT].copy_from_slice(&t);
378 if changed { self.theme_invalidate(); }
379 }
380
381 fn theme_invalidate(&mut self) {
384 self.dirty_force_full();
385 for window in &mut self.m.win.recs {
386 if let Some(parts) = &mut window.save { parts.dirty.full = true; }
387 }
388 self.need = true;
389 self.need_render = true;
390 }
391
392 pub fn color(&self, tok: u32) -> u32 {
394 self.theme[((tok & 252) / 4) as usize]
395 }
396
397 pub fn ts_font(&mut self, ts: u32) {
399 let ts = if (ts & 7) < TS_COUNT { ts & 7 } else { TS_BODY } as usize;
400 let (fx, font) = if self.look.text {
401 self.look_ts(ts)
402 } else {
403 (self.style.ts_size_fx()[ts] as u32, self.style.ts_font()[ts] as u32)
404 };
405 let size = fx.wrapping_mul(self.wp.rt.dpi as u32) / 96;
406 let size = if self.text_scale == 100 { size } else { size * self.text_scale / 100 };
407 self.text.set_font(font, size as i32);
408 }
409
410 pub fn ts_line(&self, ts: u32) -> i32 {
412 let ts = if (ts & 7) < TS_COUNT { ts & 7 } else { TS_BODY } as usize;
413 let dip = if self.look.text { self.look_line(ts) } else { self.style.ts_line()[ts] as i32 };
414 self.dp(if self.text_scale == 100 { dip } else { (dip * self.text_scale as i32 + 50) / 100 })
415 }
416
417 fn look_ts(&self, ts: usize) -> (u32, u32) {
420 let st = self.style;
421 let t = self.look.theme.as_ref().map_or(TextStyle::default(), |th| th.text[ts]);
422 let fx = t.size_fx.map_or(st.ts_size_fx()[ts] as u32, u32::from);
423 let face = match t.weight {
424 Some(w) if w >= 650 => FONT_BOLD,
425 Some(w) if w >= 500 => FONT_SEMI,
426 Some(_) => FONT_REG,
427 None => st.ts_font()[ts] as u32,
428 };
429 (fx, if face < 3 { self.look.fonts[face as usize] } else { face })
430 }
431
432 fn look_line(&self, ts: usize) -> i32 {
435 let st = self.style;
436 let base = st.ts_line()[ts] as i32;
437 let t = self.look.theme.as_ref().map_or(TextStyle::default(), |th| th.text[ts]);
438 match (t.line, t.size_fx) {
439 (Some(l), _) => l as i32,
440 (None, Some(s)) => {
441 let ss = st.ts_size_fx()[ts] as i32;
442 if ss > 0 { (base * s as i32 + ss / 2) / ss } else { base }
443 }
444 _ => base,
445 }
446 }
447
448 pub fn set_theme(&mut self, th: &Theme) -> Result<(), ThemeError> {
454 let fonts = self.theme_fonts(th)?;
455 let l = &mut self.look;
456 (l.rad_mode, l.rad) = match th.radius {
457 None => (0, 0),
458 Some(Radius::Dip(d)) => (1, d.min(64) as i32),
459 Some(Radius::Percent(p)) => (2, p.min(400) as i32),
460 };
461 l.spacing = (th.spacing as i32).clamp(25, 400);
462 l.ctrl_h = if th.control_height == 0 { 0 } else { (th.control_height as i32).clamp(12, 96) };
463 l.text = th.has_text();
464 l.fonts = fonts;
465 l.theme = Some(Box::new(th.clone()));
466 self.look_changed();
467 Ok(())
468 }
469
470 pub fn clear_theme(&mut self) {
472 if self.look.theme.is_none() {
473 return;
474 }
475 let l = &mut self.look;
476 l.theme = None;
477 (l.rad_mode, l.rad, l.spacing, l.ctrl_h, l.text) = (0, 0, 100, 0, false);
478 l.fonts = [FONT_REG, FONT_SEMI, FONT_BOLD];
479 self.look_changed();
480 }
481
482 pub fn app_theme(&self) -> Option<&Theme> {
484 self.look.theme.as_deref()
485 }
486
487 pub fn load_theme(&mut self, path: &str) -> Result<(), ThemeError> {
490 let th = Theme::load(path)?;
491 self.set_theme(&th)
492 }
493
494 fn theme_fonts(&mut self, th: &Theme) -> Result<[u32; 3], ThemeError> {
496 if th.family.is_empty() {
497 return Ok([FONT_REG, FONT_SEMI, FONT_BOLD]);
498 }
499 let ax = &th.family_axes;
500 let reg = self.theme_font(&th.family, 400, ax)?;
501 let face = |ui: &mut Ui, own: &str, other: &str, weight: u32| -> Result<u32, ThemeError> {
502 if !own.is_empty() {
503 ui.theme_font(own, weight, ax)
504 } else if !other.is_empty() {
505 ui.theme_font(other, weight, ax)
506 } else if app::is_file(&th.family) && !ui.text.var_axes(reg).iter().any(|a| &a.tag == b"wght") {
507 let id = ui.text.style(reg, text::FS_SYN_BOLD);
509 if id < 0 { Err(ThemeError::general(app::E_FONT, &th.family)) } else { Ok(id as u32) }
510 } else {
511 ui.theme_font(&th.family, weight, ax)
512 }
513 };
514 let semi = face(self, &th.family_semibold, &th.family_bold, 600)?;
515 let bold = face(self, &th.family_bold, &th.family_semibold, 700)?;
516 Ok([reg, semi, bold])
517 }
518
519 fn theme_font(&mut self, spec: &str, weight: u32, axes: &[([u8; 4], f32)]) -> Result<u32, ThemeError> {
522 let err = || ThemeError::general(app::E_FONT, spec);
523 if !app::is_file(spec) {
524 let sel = FontSel { family: String::from(spec), weight, italic: false, size: 11.0, axes: axes.to_vec() };
525 return self.font_load(&sel).ok_or_else(err);
526 }
527 let id = match self.look.files.iter().find(|f| f.0 == spec) {
528 Some(&(_, id)) => id,
529 None => {
530 let data = plat::read_file(spec).ok_or_else(err)?;
531 let id = self.text.load_mem(&data, 0);
532 if id < 0 {
533 return Err(err());
534 }
535 self.look.files.push((String::from(spec), id as u32));
536 id as u32
537 }
538 };
539 if self.text.var_axes(id).is_empty() {
540 return Ok(id);
541 }
542 let mut vals = axes.to_vec();
543 if !vals.iter().any(|a| &a.0 == b"wght") {
544 vals.push((*b"wght", weight as f32));
545 }
546 let v = self.text.vary(id, &vals);
547 if v < 0 { Err(err()) } else { Ok(v as u32) }
548 }
549
550 fn look_changed(&mut self) {
552 self.look.serial = self.look.serial.wrapping_add(1);
553 self.theme_build();
554 self.theme_invalidate();
555 self.repass = true;
556 }
557
558 pub(crate) fn look_radius(&self, r: i32, w: i32, h: i32) -> i32 {
561 let half = w.min(h) >> 1;
562 if r <= 0 || r >= half {
563 return r;
564 }
565 let n = if self.look.rad_mode == 1 { self.dp(self.look.rad) } else { (r * self.look.rad + 50) / 100 };
566 n.clamp(0, half)
567 }
568
569 pub(crate) fn dp_space(&self, dip: i32) -> i32 {
571 let s = self.look.spacing;
572 self.dp(if s == 100 { dip } else { (dip * s + 50) / 100 })
573 }
574
575 pub(crate) fn ctrl_size(&self, w: i32, h: i32, flags: u32, square: bool) -> (i32, i32) {
578 if self.look.ctrl_h == 0 || flags & (ST_LARGE | ST_COMPACT) != 0 {
579 return (w, h);
580 }
581 let nh = self.dp_text(self.look.ctrl_h);
582 (if square && w == h { nh } else { w }, nh)
583 }
584
585 pub fn styles_add(&mut self, styles: &[&'static dyn Style]) {
591 for &s in styles {
592 if !self.look.styles.iter().any(|x| x.name() == s.name()) {
593 self.look.styles.push(s);
594 }
595 }
596 }
597
598 pub fn style_names(&self) -> Vec<&'static str> {
600 self.look.styles.iter().map(|s| s.name()).collect()
601 }
602
603 pub fn style_name(&self) -> &'static str {
605 self.style.name()
606 }
607
608 pub fn set_style(&mut self, name: &str) -> bool {
611 let Some(&st) = self.look.styles.iter().find(|s| s.name() == name) else { return false };
612 if self.style.name() == name {
613 return true;
614 }
615 self.style = st;
616 self.text.set_embedded(st.fonts(), st.font_names());
617 for (font, raster) in st.font_raster().iter().enumerate() {
618 self.text.set_font_raster(font as u32, *raster);
619 }
620 self.look_changed();
621 true
622 }
623
624 pub fn dp_text(&self, dip: i32) -> i32 {
627 self.dp(if self.text_scale <= 100 { dip } else { (dip * self.text_scale as i32 + 50) / 100 })
628 }
629
630 pub fn ts_base(&mut self, ts: u32) -> i32 {
632 self.ts_font(ts);
633 let line = self.ts_line(ts);
634 let asc = self.text.ascent();
635 let desc = self.text.descent();
636 ((line - (asc + desc)) >> 1) + asc
637 }
638}