Skip to main content

plat/
settings.rs

1// settings -- the user's system settings the UI follows
2//
3//   sys_settings() -> SysSettings       the system's (Windows: system calls and the registry;
4//                                       Linux: the settings portal, at most 300 ms; elsewhere
5//                                       the defaults), then the jfgui_* overrides
6//   sys_settings_env() -> SysSettings   the defaults and the overrides only (headless runs)
7//
8// Overrides (tests, shots, users): jfgui_THEME=dark|light, jfgui_ACCENT=RRGGBB,
9// jfgui_CONTRAST=high|normal, jfgui_REDUCE_MOTION=1|0, jfgui_TEXT_SCALE=PERCENT,
10// jfgui_DOUBLE_CLICK_MS=N, jfgui_CARET_BLINK_MS=N (0: a steady caret),
11// jfgui_FONT_AA=1|0, jfgui_FONT_HINTING=none|slight|medium|full,
12// jfgui_FONT_RGBA=none|rgb|bgr|vrgb|vbgr.
13//
14// Font rendering (font_aa, font_hinting, font_rgba; None: the system says nothing): Windows
15// SPI_GETFONTSMOOTHING, SPI_GETFONTSMOOTHINGTYPE (ClearType: subpixels) and
16// SPI_GETFONTSMOOTHINGORIENTATION; Linux the portal's org.gnome.desktop.interface
17// font-antialiasing / font-hinting / font-rgba-order, else what fontconfig's conf.d turns on
18// (10-hinting-*.conf, 10-sub-pixel-*.conf, 10-no-antialias.conf, 10-yes-antialias.conf).
19//
20// The title bar the library draws (titlebar: the buttons and what clicks on the bar do): Linux
21// GNOME's org.gnome.desktop.wm.preferences button-layout, action-double-click-titlebar,
22// action-middle-click-titlebar, action-right-click-titlebar (the portal), else GTK's
23// gtk-decoration-layout / gtk-titlebar-*-click (settings.ini); Windows has none (its own title
24// bar and the system's snap / caption behaviour).  Overrides: jfgui_BUTTON_LAYOUT=LAYOUT (GNOME's
25// syntax, e.g. "close,minimize,maximize:"), jfgui_TITLEBAR_DOUBLE_CLICK / _MIDDLE_CLICK /
26// _RIGHT_CLICK=toggle-maximize|minimize|lower|menu|none.
27
28/// the colours of a high-contrast scheme (0xRRGGBB)
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct HcColors {
31    pub bg: u32,
32    pub text: u32,
33    /// selection, accent fills
34    pub highlight: u32,
35    pub highlight_text: u32,
36    pub button: u32,
37    pub button_text: u32,
38    pub disabled: u32,
39    pub link: u32,
40}
41
42impl HcColors {
43    /// white on black (Windows "Night sky" / "HC Black")
44    pub const BLACK: HcColors = HcColors {
45        bg: 0x000000,
46        text: 0xFFFFFF,
47        highlight: 0x1AEBFF,
48        highlight_text: 0x000000,
49        button: 0x000000,
50        button_text: 0xFFFFFF,
51        disabled: 0x3FF23F,
52        link: 0xFFFF00,
53    };
54    /// black on white (Windows "Desert" / "HC White")
55    pub const WHITE: HcColors = HcColors {
56        bg: 0xFFFFFF,
57        text: 0x000000,
58        highlight: 0x37006E,
59        highlight_text: 0xFFFFFF,
60        button: 0xFFFFFF,
61        button_text: 0x000000,
62        disabled: 0x600000,
63        link: 0x00009F,
64    };
65}
66
67/// SysSettings::font_hinting
68pub const FONT_HINT_NONE: u8 = 0;
69pub const FONT_HINT_SLIGHT: u8 = 1;
70pub const FONT_HINT_MEDIUM: u8 = 2;
71pub const FONT_HINT_FULL: u8 = 3;
72/// SysSettings::font_rgba: the order of a pixel's subpixels (none: greyscale smoothing)
73pub const FONT_RGBA_NONE: u8 = 0;
74pub const FONT_RGBA_RGB: u8 = 1;
75pub const FONT_RGBA_BGR: u8 = 2;
76pub const FONT_RGBA_VRGB: u8 = 3;
77pub const FONT_RGBA_VBGR: u8 = 4;
78
79/// a FONT_HINT_* name ("none", "slight", "medium", "full"; also "hintnone" ... of fontconfig)
80pub fn font_hint_parse(s: &str) -> Option<u8> {
81    match s.trim().trim_start_matches("hint") {
82        "none" => Some(FONT_HINT_NONE),
83        "slight" => Some(FONT_HINT_SLIGHT),
84        "medium" => Some(FONT_HINT_MEDIUM),
85        "full" => Some(FONT_HINT_FULL),
86        _ => None,
87    }
88}
89
90/// a FONT_RGBA_* name ("none", "rgb", "bgr", "vrgb", "vbgr"; "grayscale" is none)
91pub fn font_rgba_parse(s: &str) -> Option<u8> {
92    match s.trim() {
93        "none" | "grayscale" | "greyscale" => Some(FONT_RGBA_NONE),
94        "rgb" => Some(FONT_RGBA_RGB),
95        "bgr" => Some(FONT_RGBA_BGR),
96        "vrgb" => Some(FONT_RGBA_VRGB),
97        "vbgr" => Some(FONT_RGBA_VBGR),
98        _ => None,
99    }
100}
101
102/// Titlebar::buttons: the caption buttons of a title bar the library draws
103pub const TB_BTN_CLOSE: u8 = 1;
104pub const TB_BTN_MIN: u8 = 2;
105pub const TB_BTN_MAX: u8 = 3;
106/// Titlebar::double / middle / right: what a click on the empty part of the bar does
107pub const TB_ACT_NONE: u8 = 0;
108/// maximize, or restore a maximized window
109pub const TB_ACT_MAXIMIZE: u8 = 1;
110pub const TB_ACT_MINIMIZE: u8 = 2;
111/// below the other windows (X11; Wayland has no request for it: nothing)
112pub const TB_ACT_LOWER: u8 = 3;
113/// the window manager's window menu (move, resize, always on top, ...)
114pub const TB_ACT_MENU: u8 = 4;
115
116/// the buttons and click actions of a title bar the library draws (SysSettings::titlebar)
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub struct Titlebar {
119    /// the buttons at the left end and at the right end, each side left to right (TB_BTN_*,
120    /// 0 ends a side; each button at most once); None: the style's places
121    pub buttons: Option<([u8; 3], [u8; 3])>,
122    /// TB_ACT_* of a double click, a middle click and a right click
123    pub double: u8,
124    pub middle: u8,
125    pub right: u8,
126}
127
128impl Default for Titlebar {
129    /// the style's buttons, double click maximizes, right click the window menu
130    fn default() -> Self {
131        Titlebar { buttons: None, double: TB_ACT_MAXIMIZE, middle: TB_ACT_NONE, right: TB_ACT_MENU }
132    }
133}
134
135/// a button layout in GNOME's button-layout syntax (GTK's gtk-decoration-layout): the left
136/// side, ':', the right side, names separated by ','.  close, minimize and maximize are kept,
137/// other names (appmenu, icon, menu, spacer) and repeats skipped; without ':' everything is
138/// the left side (as GTK reads it)
139pub fn titlebar_layout_parse(s: &str) -> ([u8; 3], [u8; 3]) {
140    let (l, r) = s.split_once(':').unwrap_or((s, ""));
141    let mut seen = 0u8;
142    let mut side = |part: &str| {
143        let mut out = [0u8; 3];
144        let mut n = 0;
145        for name in part.split(',') {
146            let b = match name.trim() {
147                "close" => TB_BTN_CLOSE,
148                "minimize" => TB_BTN_MIN,
149                "maximize" => TB_BTN_MAX,
150                _ => continue,
151            };
152            if seen & (1 << b) == 0 && n < 3 {
153                seen |= 1 << b;
154                out[n] = b;
155                n += 1;
156            }
157        }
158        out
159    };
160    let left = side(l);
161    (left, side(r))
162}
163
164/// a title bar action as GNOME names it (GDesktopTitlebarAction): toggle-maximize (also
165/// -horizontally / -vertically, which xdg-shell can not do: GTK treats them alike), minimize,
166/// lower, menu, none; toggle-shade is none (no compositor shades client-side windows)
167pub fn titlebar_action_parse(s: &str) -> Option<u8> {
168    match s.trim() {
169        "toggle-maximize" | "toggle-maximize-horizontally" | "toggle-maximize-vertically" => Some(TB_ACT_MAXIMIZE),
170        "minimize" => Some(TB_ACT_MINIMIZE),
171        "lower" => Some(TB_ACT_LOWER),
172        "menu" => Some(TB_ACT_MENU),
173        "none" | "toggle-shade" => Some(TB_ACT_NONE),
174        _ => None,
175    }
176}
177
178/// what the system says (see the module comment)
179#[derive(Clone, Copy, Debug, PartialEq)]
180pub struct SysSettings {
181    /// None: no preference
182    pub dark: Option<bool>,
183    /// 0xRRGGBB
184    pub accent: Option<u32>,
185    pub high_contrast: bool,
186    /// Windows: the colours of the active high-contrast scheme
187    pub contrast_colors: Option<HcColors>,
188    pub reduce_motion: bool,
189    /// percent, 100 = normal
190    pub text_scale: u32,
191    pub double_click_ms: u32,
192    /// one phase of the caret (on or off); 0 = steady
193    pub caret_blink_ms: u32,
194    /// font smoothing: Some(false) = off (sharp black-and-white glyphs)
195    pub font_aa: Option<bool>,
196    /// FONT_HINT_*
197    pub font_hinting: Option<u8>,
198    /// FONT_RGBA_* (subpixel smoothing and its order)
199    pub font_rgba: Option<u8>,
200    /// the title bar the library draws (Linux client-side decorations, AF_CUSTOM_TITLEBAR)
201    pub titlebar: Titlebar,
202}
203
204impl Default for SysSettings {
205    fn default() -> Self {
206        SysSettings {
207            dark: None,
208            accent: None,
209            high_contrast: false,
210            contrast_colors: None,
211            reduce_motion: false,
212            text_scale: 100,
213            double_click_ms: 500,
214            caret_blink_ms: 530,
215            font_aa: None,
216            font_hinting: None,
217            font_rgba: None,
218            titlebar: Titlebar::default(),
219        }
220    }
221}
222
223/// the system's settings and the jfgui_* overrides
224pub fn sys_settings() -> SysSettings {
225    let mut s = SysSettings::default();
226    #[cfg(any(windows, target_os = "linux"))]
227    crate::sys_settings_os(&mut s);
228    // the browser's media queries / Android's configuration, as the host sent them (web_system.rs)
229    #[cfg(any(target_arch = "wasm32", target_os = "android"))]
230    crate::web::sys_settings_host(&mut s);
231    env_overrides(&mut s);
232    s
233}
234
235/// the defaults and the jfgui_* overrides only (headless runs stay the same everywhere)
236pub fn sys_settings_env() -> SysSettings {
237    let mut s = SysSettings::default();
238    env_overrides(&mut s);
239    s
240}
241
242fn env_overrides(s: &mut SysSettings) {
243    let num = |n: &str| crate::env(n).and_then(|v| v.trim().parse::<u32>().ok());
244    match crate::env("jfgui_THEME").as_deref() {
245        Some("dark") => s.dark = Some(true),
246        Some("light") => s.dark = Some(false),
247        _ => {}
248    }
249    if let Some(v) = crate::env("jfgui_ACCENT") {
250        if let Ok(c) = u32::from_str_radix(v.trim().trim_start_matches('#'), 16) {
251            s.accent = Some(c & 0xFF_FFFF);
252        }
253    }
254    match crate::env("jfgui_CONTRAST").as_deref() {
255        Some("high") | Some("1") => s.high_contrast = true,
256        Some("normal") | Some("0") => {
257            s.high_contrast = false;
258            s.contrast_colors = None;
259        }
260        _ => {}
261    }
262    if let Some(v) = num("jfgui_REDUCE_MOTION") {
263        s.reduce_motion = v != 0;
264    }
265    if let Some(v) = num("jfgui_TEXT_SCALE") {
266        s.text_scale = v.clamp(50, 300);
267    }
268    if let Some(v) = num("jfgui_DOUBLE_CLICK_MS") {
269        s.double_click_ms = v.clamp(100, 5000);
270    }
271    if let Some(v) = num("jfgui_CARET_BLINK_MS") {
272        s.caret_blink_ms = v.min(5000);
273    }
274    if let Some(v) = num("jfgui_FONT_AA") {
275        s.font_aa = Some(v != 0);
276    }
277    if let Some(v) = crate::env("jfgui_FONT_HINTING").as_deref().and_then(font_hint_parse) {
278        s.font_hinting = Some(v);
279    }
280    if let Some(v) = crate::env("jfgui_FONT_RGBA").as_deref().and_then(font_rgba_parse) {
281        s.font_rgba = Some(v);
282    }
283    if let Some(v) = crate::env("jfgui_BUTTON_LAYOUT") {
284        s.titlebar.buttons = Some(titlebar_layout_parse(&v));
285    }
286    let act = |n: &str| crate::env(n).as_deref().and_then(titlebar_action_parse);
287    if let Some(a) = act("jfgui_TITLEBAR_DOUBLE_CLICK") {
288        s.titlebar.double = a;
289    }
290    if let Some(a) = act("jfgui_TITLEBAR_MIDDLE_CLICK") {
291        s.titlebar.middle = a;
292    }
293    if let Some(a) = act("jfgui_TITLEBAR_RIGHT_CLICK") {
294        s.titlebar.right = a;
295    }
296}