Skip to main content

ui/
theme.rs

1// theme -- colour tokens, accent colour and type ramp of the linked style  [src/ui/ui_theme.s]
2//
3//   theme_set(mode)            0 light, 1 dark, 2 follow the system (plat::theme)
4//   theme_dark() -> bool       the dark theme is active
5//   accent_set(rgb)            0xRRGGBB accent, 0 = the style's default (an app's accent
6//                              stops the system's)
7//   high_contrast() / set_contrast(Some(on) | None)   the high-contrast theme: every token
8//                              from the scheme's colours (Windows: the system's; else white on
9//                              black or black on white), no translucent fills; None follows
10//                              the system; --contrast high|normal
11//   set_reduced_motion(Some(on) | None)   reduced motion (anim_off) forced on or off, or as
12//                              the system says (None); --anim off|on
13//   text_scale() / set_text_scale(percent)   text sizes and line heights (not the controls)
14//   sys_apply(settings)        (runtime) plat::sys_settings: theme, accent, contrast, reduced
15//                              motion (anim_off), text scale, double-click and caret times,
16//                              font smoothing and its subpixel order; what the app or the
17//                              command line fixed stays
18//   color(tok) -> argb         0xAARRGGBB of a T_* token (byte offset)
19//   ts_font(ts)                select font and pixel size of a TS_* text style
20//   ts_line(ts) -> px          line height of a text style
21//   ts_base(ts) -> px          baseline offset from the line top
22//   dp_text(dip) -> px         dp(dip) grown with a text scale above 100 % (pills around text)
23//   color_mix(a, b, t)         per-channel a + (b - a) * t / 256 (t 0..256)
24//   luminance(rgb) / contrast_ratio(a, b)   WCAG 2 relative luminance (0..1) of 0xRRGGBB and
25//                              the contrast of two colours (1..21; text wants 4.5, graphics 3)
26// App themes (theme_app.rs: Theme, its file format and errors) on top of the style:
27//   set_theme(&theme) -> Result   from the next frame on; a font that cannot be loaded is an
28//                              error and the previous theme stays; clear_theme() the style's
29//                              own look again; app_theme() -> Option<&Theme>;
30//                              load_theme(path) = Theme::load + set_theme
31//   The theme layer: theme_build (colours: the theme's tokens over the style's, its accent
32//   through the style's ramp), cmd_rrect / cmd_shadow (look_radius: corners that are not pills),
33//   box_begin / gridbox_begin (dp_space: padding and gaps), the size calls of buttons, text
34//   boxes, combo boxes, drop-down and split buttons (ctrl_size), ts_font / ts_line (sizes,
35//   line heights, weights, the family's faces).  Without a theme every value is the style's.
36// The run-time style switch (styles linked side by side, build_rs.py: every style_NAME the
37// app's sources name):
38//   styles_add(&[&style_kde::STYLE, ..])   style_names() -> names   style_name() -> current
39//   set_style(name) -> bool    the style's fonts replace ids 0..3 (Text::set_embedded), the
40//                              tokens are built again, everything is redrawn and laid out
41// look.serial counts the changes of theme and style: caches of laid-out text compare it.
42//
43// The values come from the style (Style::tokens, accent, ts_size / ts_line / ts_font).
44
45use 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
57/// the app's theme, resolved for the drawing calls, and the styles set_style can switch to
58pub(crate) struct Look {
59    /// the active theme (None: the style's look)
60    pub(crate) theme: Option<Box<Theme>>,
61    /// corners: 0 the style's, 1 rad DIP, 2 rad percent of the style's
62    pub(crate) rad_mode: u8,
63    pub(crate) rad: i32,
64    /// box padding and gaps in percent
65    pub(crate) spacing: i32,
66    /// standard control height in DIP (0: the style's)
67    pub(crate) ctrl_h: i32,
68    /// the theme changes text styles; the font ids it uses for FONT_REG / _SEMI / _BOLD
69    pub(crate) text: bool,
70    pub(crate) fonts: [u32; 3],
71    /// font files themes loaded (path, id): a theme applied again reuses them
72    files: Vec<(String, u32)>,
73    /// the styles set_style can switch to (the app's own first)
74    styles: Vec<&'static dyn Style>,
75    /// changes of theme and style so far
76    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
96/// g_theme: the T_COUNT tokens padded to a power of two (256 bytes)
97pub(crate) const THEME_N: usize = 64;
98
99/// color_mix(a, b, t): per-channel a + (b - a) * t / 256 (t 0..256), every channel
100/// including alpha
101pub 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
112// sRGB byte -> linear light * 65535 (IEC 61966-2-1)
113static 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
132/// the WCAG 2 relative luminance of 0xRRGGBB (alpha ignored): 0 black .. 1 white
133pub 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
138/// the WCAG 2 contrast ratio of two colours (alpha ignored): 1 (equal) .. 21 (black on
139/// white); text needs 4.5 (large text 3), graphical objects and controls 3
140pub 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
146// the brightness of 0xRRGGBB (0..255)
147fn luma(rgb: u32) -> u32 {
148    ((rgb >> 16 & 255) * 299 + (rgb >> 8 & 255) * 587 + (rgb & 255) * 114) / 1000
149}
150
151// the colours a theme sets for a mode over the tokens
152fn 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
160/// the tokens of a high-contrast scheme: opaque fills from its colours, borders and strokes
161/// in the text colour, accents in the highlight colour, links in the link colour
162pub 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); // transparent at rest
184    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    /// ui_theme_set(mode): 0 light, 1 dark, 2 the system setting (unknown: dark)
207    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    /// ui_theme_dark: the dark theme is active
221    pub fn theme_dark(&self) -> bool {
222        self.theme_dark
223    }
224
225    /// ui_accent_set(0xRRGGBB), 0 = the style's default; the system's accent no longer applies
226    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    /// the high-contrast theme is active
233    pub fn high_contrast(&self) -> bool {
234        self.high_contrast
235    }
236
237    /// high contrast on (Some(true)), off (Some(false)) or as the system says (None)
238    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    /// reduced motion on (Some(true): no tweens, no fades), off (Some(false)) or as the
252    /// system says (None); like --anim, a forced value outlasts the system's changes
253    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    /// the text scale in percent (100 = the style's sizes)
271    pub fn text_scale(&self) -> u32 {
272        self.text_scale
273    }
274
275    /// (caches of laid-out text) the text scale and the changes of theme and style in one
276    /// key: the text scale (50..300) in the low bits, look.serial above
277    pub(crate) fn layout_scale(&self) -> u32 {
278        self.text_scale | self.look.serial << 16
279    }
280
281    /// the system's font rendering: (smoothing, plat::FONT_HINT_*, plat::FONT_RGBA_*), None
282    /// where the system says nothing; smoothing off (ui.text.antialias()) and the subpixel
283    /// order (ui.text.subpixel(): subpixel smoothing on the window's framebuffer) are applied,
284    /// hinting is not
285    pub fn font_prefs(&self) -> (Option<bool>, Option<u8>, Option<u8>) {
286        self.font_prefs
287    }
288
289    /// scale text sizes and line heights (50..300 %); the system's scale no longer applies
290    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    /// (runtime) the system's settings, except what the app or the command line fixed
305    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        // font smoothing off: black-and-white glyphs; a subpixel order: subpixel smoothing on
330        // opaque targets (text/lcd.rs; headless only through jfgui_FONT_RGBA, so shots stay
331        // the same everywhere); hinting is kept for the app (font_prefs)
332        let aa = s.font_aa != Some(false);
333        let order = if aa { s.font_rgba.unwrap_or(0) } else { 0 }; // FONT_RGBA_* = text::SUBPIXEL_*
334        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(); // every pixel of text changes: redraw everything
338        }
339        self.font_prefs = (s.font_aa, s.font_hinting, s.font_rgba);
340        self.tb_sys = s.titlebar; // the library's title bar (chrome.rs)
341        self.theme_build();
342    }
343
344    /// ui_theme_build: copy the table for the current mode and fill in the accent tokens (high
345    /// contrast: the scheme's tokens)
346    pub(crate) fn theme_build(&mut self) {
347        let before = self.theme_dark;
348        let t = if self.high_contrast {
349            // the system's scheme, else white on black (dark) or black on white (light)
350            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            // the theme's accent wins over the app's and the system's
369            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); // the style derives the accent tokens
371            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    // agent_core: style icon colours may change without changing Cmd::Icon's
382    // foreground. Invalidate the live window and every saved window together.
383    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    /// ui_color(tok): 0xAARRGGBB of a token (byte offset; any value reads inside the table)
393    pub fn color(&self, tok: u32) -> u32 {
394        self.theme[((tok & 252) / 4) as usize]
395    }
396
397    /// ui_ts_font(ts): select the font and pixel size (size * dpi / 96, 24.8) of a style
398    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    /// ui_ts_line(ts): line height of a text style in px
411    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    // (theme) size (24.8 DIP) and font id of a text style: the theme's size and weight, the
418    // family's face of that weight
419    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    // (theme) the line height of a text style in DIP: the theme's, else the style's (grown
433    // with a size of the theme)
434    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    // ------------------------------------------------------------------ app themes
449
450    /// the app's theme over the style (theme_app.rs) from the next frame on: colours of light,
451    /// dark and high contrast, the accent, corners, spacing, control heights, text styles and
452    /// a font family.  A font that cannot be loaded is an error; the previous theme stays.
453    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    /// no app theme: the style's own look again
471    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    /// the active app theme (None: the style's look)
483    pub fn app_theme(&self) -> Option<&Theme> {
484        self.look.theme.as_deref()
485    }
486
487    /// read a theme file (app://, config://, a path) and make it the app's theme; on an error
488    /// the previous theme stays
489    pub fn load_theme(&mut self, path: &str) -> Result<(), ThemeError> {
490        let th = Theme::load(path)?;
491        self.set_theme(&th)
492    }
493
494    // the font ids of a theme's family for FONT_REG / _SEMI / _BOLD (none: the style's)
495    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                // a file without its bold faces (and no weight axis): a synthetic bold
508                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    // one face: a font file (VFS path) or an installed family (ui.font_load); a variable one
520    // at the weight and the theme's axis values
521    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    // the theme or the style changed: tokens again, everything laid out and drawn again
551    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    /// (theme) a corner radius in px of a rounded rectangle w x h: corners that are not pills
559    /// take the theme's radius (square ones stay square)
560    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    /// (theme) DIP of box padding and gaps -> px, scaled by the theme's spacing
570    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    /// (theme) the size of a standard control with the theme's control height (flags ST_*;
576    /// compact and large controls keep theirs); square: an icon-only control stays square
577    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    // ------------------------------------------------------------------ the style at run time
586
587    /// the styles set_style can switch to: the app names the style crates it links besides its
588    /// own (build_rs.py links every style_NAME its sources name):
589    /// `ui.styles_add(&[&style_classic::STYLE, &style_kde::STYLE])`
590    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    /// the names of the styles set_style knows (the app's own first, then as added)
599    pub fn style_names(&self) -> Vec<&'static str> {
600        self.look.styles.iter().map(|s| s.name()).collect()
601    }
602
603    /// the name of the style in use (Style::name)
604    pub fn style_name(&self) -> &'static str {
605        self.style.name()
606    }
607
608    /// switch to another linked style (styles_add) from the next frame on: its tokens, fonts,
609    /// metrics and painters; the app theme stays on top -> false: no such style
610    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    /// dp_text(dip): DIP -> px, grown with a text scale above 100 % (the height of parts
625    /// around one line of text, such as badge pills)
626    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    /// ui_ts_base(ts): baseline offset from the top of the line box (px; selects the font)
631    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}