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
// web_system -- what the browser / Android host says about the system: the user's settings
// (settings.rs), the permission for notifications and whether the screen can be kept on
// (web_notify.rs, web_power.rs), the answers of the notifications
//
// jfgui_system(len) -> count the inbox: "key=value" lines -> how many keys were taken; a
// key that is missing keeps its value. The host sends all of
// them before jfgui_start and the changed ones after every
// change; once the app runs, a change of what sys_settings()
// answers is E_SETTINGS with its SET_* bits (dark, accent:
// SET_THEME; contrast, forced, colors: SET_CONTRAST; scale:
// SET_FONTS; motion: SET_OTHER). Keys:
// dark=1|0|- a dark / light scheme, "-" no preference (prefers-color-scheme;
// Android: Configuration.uiMode, UI_MODE_NIGHT_YES)
// contrast=1|0 more contrast (prefers-contrast: more; Android: high-contrast text, the
// contrast level of API 34 at "high")
// forced=1|0 the browser's forced colours (forced-colors: active, Windows' contrast
// themes in the browser): high contrast with the colours of `colors`
// colors=RRGGBB,.. eight system colours of the forced scheme: Canvas, CanvasText,
// Highlight, HighlightText, ButtonFace, ButtonText, GrayText, LinkText
// (HcColors in that order); empty: none
// motion=1|0 fewer animations (prefers-reduced-motion: reduce; Android:
// Settings.Global.ANIMATOR_DURATION_SCALE 0)
// scale=PERCENT the text size (the browser's default font size against 16 px;
// Android: Configuration.fontScale)
// accent=RRGGBB|- the system's accent colour (Android 12+: the dynamic colours,
// system_accent1_600); "-" none
// notify=granted|default|denied|unsupported (browser) the Notification permission:
// default = asked at the first notification (from the user's gesture)
// wakelock=1|0 (browser) navigator.wakeLock exists (Screen Wake Lock)
// jfgui_note(id, clicked) -> 1 a notification of tray::notify was clicked (1; the host
// brought the tab / the activity to the front) or went away
// unclicked (0: closed, swiped away, refused) -> TrayEvent
// sys_settings_host(&mut s) (settings.rs) the host's values; the jfgui_* overrides follow
// dark() -> Option<bool> (web.rs theme())
// notify_state() / wakelock() (web_notify.rs, web_power.rs)
// back_trap(on) (Android) the back key is Esc for the app while on (lib.rs)
// jfgui_touch_reached() -> 0|1 (Android) the last finger's gesture reached a widget (a tap, a
// drag), not only a scroll: the keyboard may follow it
//
// The lifecycle needs no export of its own: the host sends E_POWER through jfgui_event (data 2:
// the tab was hidden / Android's onStop, 3: visible / started again) and the runtime turns it
// into UIC_SUSPEND.
use core::sync::atomic::{AtomicU8, Ordering};
use rt::sync::SpinLock;
use crate::{HcColors, SysSettings, SET_CONTRAST, SET_FONTS, SET_OTHER, SET_THEME};
/// notify=..: the browser's permission for notifications
pub(crate) const NOTIFY_UNSUPPORTED: u8 = 0;
pub(crate) const NOTIFY_DEFAULT: u8 = 1;
pub(crate) const NOTIFY_GRANTED: u8 = 2;
pub(crate) const NOTIFY_DENIED: u8 = 3;
#[derive(Clone, Copy, PartialEq)]
struct Host {
dark: Option<bool>,
contrast: bool,
forced: bool,
colors: Option<HcColors>,
motion: bool,
scale: u32,
accent: Option<u32>,
}
static HOST: SpinLock<Host> =
SpinLock::new(Host { dark: None, contrast: false, forced: false, colors: None, motion: false, scale: 100, accent: None });
// the browser tells (notify=); Android's host asks at every notification itself
static NOTIFY: AtomicU8 = AtomicU8::new(if cfg!(target_os = "android") { NOTIFY_DEFAULT } else { NOTIFY_UNSUPPORTED });
static WAKELOCK: AtomicU8 = AtomicU8::new(0);
fn hex(v: &str) -> Option<u32> {
let v = v.trim().trim_start_matches('#');
if v.len() != 6 {
return None;
}
u32::from_str_radix(v, 16).ok()
}
// colors=: eight RRGGBB values in HcColors' order
fn colors(v: &str) -> Option<HcColors> {
let mut c = [0u32; 8];
let mut it = v.split(',');
for x in c.iter_mut() {
*x = hex(it.next()?)?;
}
if it.next().is_some() {
return None;
}
Some(HcColors { bg: c[0], text: c[1], highlight: c[2], highlight_text: c[3], button: c[4], button_text: c[5], disabled: c[6], link: c[7] })
}
// the SET_* bits of the difference
fn bits(a: &Host, b: &Host) -> u32 {
let mut m = 0;
if a.dark != b.dark || a.accent != b.accent {
m |= SET_THEME;
}
if a.contrast != b.contrast || a.forced != b.forced || (b.forced && a.colors != b.colors) {
m |= SET_CONTRAST;
}
if a.scale != b.scale {
m |= SET_FONTS;
}
if a.motion != b.motion {
m |= SET_OTHER;
}
m
}
/// The host's system settings and states (inbox: "key=value" lines, see the module header)
/// -> the keys taken. A change once the app runs is E_SETTINGS.
#[unsafe(no_mangle)]
pub extern "C" fn jfgui_system(len: u32) -> u32 {
let Some(bytes) = super::input(len) else { return 0 };
let Ok(text) = core::str::from_utf8(&bytes) else { return 0 };
let mut taken = 0;
let (before, after) = {
let mut h = HOST.lock();
let before = *h;
for line in text.lines() {
let Some((key, v)) = line.split_once('=') else { continue };
let v = v.trim();
let flag = || match v {
"1" => Some(true),
"0" => Some(false),
_ => None,
};
let ok = match key.trim() {
"dark" => {
h.dark = flag();
flag().is_some() || v == "-" || v.is_empty()
}
"contrast" => flag().map(|f| h.contrast = f).is_some(),
"forced" => flag().map(|f| h.forced = f).is_some(),
"colors" => {
h.colors = colors(v);
h.colors.is_some() || v.is_empty()
}
"motion" => flag().map(|f| h.motion = f).is_some(),
"scale" => v.parse::<u32>().ok().filter(|p| (25..=500).contains(p)).map(|p| h.scale = p).is_some(),
"accent" => {
h.accent = hex(v);
h.accent.is_some() || v == "-" || v.is_empty()
}
"notify" => {
let s = match v {
"granted" => Some(NOTIFY_GRANTED),
"default" | "prompt" => Some(NOTIFY_DEFAULT),
"denied" => Some(NOTIFY_DENIED),
"unsupported" => Some(NOTIFY_UNSUPPORTED),
_ => None,
};
s.map(|s| NOTIFY.store(s, Ordering::Relaxed)).is_some()
}
"wakelock" => flag().map(|f| WAKELOCK.store(f as u8, Ordering::Relaxed)).is_some(),
_ => false,
};
taken += ok as u32;
}
(before, *h)
};
let changed = bits(&before, &after);
if changed != 0 && super::STARTED.load(Ordering::Relaxed) {
let _ = crate::evq_push(crate::Event { typ: crate::E_SETTINGS, data: changed, ..crate::Event::default() });
super::wake();
}
taken
}
/// A notification of tray::notify was clicked (clicked 1) or went away without a click (0)
/// -> 1 taken (0: the app has not started).
#[unsafe(no_mangle)]
pub extern "C" fn jfgui_note(id: u32, clicked: u32) -> u32 {
if id == 0 || !super::STARTED.load(Ordering::Relaxed) {
return 0;
}
crate::tray::push(if clicked != 0 { crate::tray::TrayEvent::NoteClicked(id) } else { crate::tray::TrayEvent::NoteClosed(id) });
1
}
/// (settings.rs) the host's settings: scheme, accent, contrast (the forced colours with their
/// scheme), animations, text scale
pub(crate) fn sys_settings_host(s: &mut SysSettings) {
let h = *HOST.lock();
s.dark = h.dark;
s.accent = h.accent;
s.high_contrast = h.contrast || h.forced;
s.contrast_colors = if h.forced { h.colors } else { None };
s.reduce_motion = h.motion;
s.text_scale = h.scale.clamp(50, 300);
}
// (Android) the host call of GallerySystem (android_host.rs: CALL_*)
#[cfg(target_os = "android")]
const CALL_BACK: u32 = 55;
#[cfg(target_os = "android")]
const CALL_LONG_PRESS: u32 = 56;
#[cfg(target_os = "android")]
const CALL_FINISH: u32 = 57;
/// (Android) while on, the back key reaches the app as Esc instead of ending the activity
#[cfg(target_os = "android")]
pub(crate) fn back_trap(on: bool) {
let _ = crate::web::android_host::call(CALL_BACK, &[on as u8]);
}
/// (Android) the runtime ended (ui.quit): the activity goes and its process with it, a new start
/// needs a new runtime
#[cfg(target_os = "android")]
pub(crate) fn app_finish() {
let _ = crate::web::android_host::call(CALL_FINISH, &[]);
}
// (Android) the finger's gesture reached a widget: plat::touch_reached, jfgui_touch_reached
#[cfg(target_os = "android")]
static TOUCH_REACHED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
#[cfg(target_os = "android")]
pub(crate) fn touch_reached(reached: bool) {
TOUCH_REACHED.store(reached, Ordering::Relaxed);
}
/// (Android) the last finger's gesture reached a widget -> 1
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
pub extern "C" fn jfgui_touch_reached() -> u32 {
TOUCH_REACHED.load(Ordering::Relaxed) as u32
}
/// (Android) a finger rested long enough: GalleryActivity gives the system's feedback
#[cfg(target_os = "android")]
pub(crate) fn touch_long_press() {
let _ = crate::web::android_host::call(CALL_LONG_PRESS, &[]);
}
/// the host's scheme: Some(true) dark, Some(false) light, None no preference
pub(crate) fn dark() -> Option<bool> {
HOST.lock().dark
}
/// NOTIFY_*: what the browser's permission for notifications is (Android: NOTIFY_DEFAULT,
/// the host asks itself)
#[cfg_attr(target_os = "android", allow(dead_code))]
pub(crate) fn notify_state() -> u8 {
NOTIFY.load(Ordering::Relaxed)
}
/// (browser) the host can keep the screen on (Screen Wake Lock)
#[allow(dead_code)]
pub(crate) fn wakelock() -> bool {
WAKELOCK.load(Ordering::Relaxed) != 0
}