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