use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputProfile {
Desktop,
MobileIOS,
MobileAndroid,
MobileOther,
}
impl InputProfile {
pub fn is_mobile_touch(self) -> bool {
matches!(
self,
InputProfile::MobileIOS | InputProfile::MobileAndroid | InputProfile::MobileOther
)
}
pub fn recommended_ux_scale(self) -> f64 {
match self {
InputProfile::Desktop => 1.0,
InputProfile::MobileIOS | InputProfile::MobileAndroid | InputProfile::MobileOther => {
1.7
}
}
}
}
static CURRENT: AtomicU8 = AtomicU8::new(profile_code(InputProfile::Desktop));
pub fn set_input_profile(profile: InputProfile) {
CURRENT.store(profile_code(profile), Ordering::Relaxed);
}
pub fn current_input_profile() -> InputProfile {
profile_from_code(CURRENT.load(Ordering::Relaxed))
}
pub fn is_mobile_touch() -> bool {
current_input_profile().is_mobile_touch()
}
pub fn touch_ui_active() -> bool {
is_mobile_touch() || crate::touch_state::touch_seen_this_session()
}
pub fn input_profile_from_hint(user_agent_or_platform: &str, pointer_coarse: bool) -> InputProfile {
let ua = user_agent_or_platform.to_ascii_lowercase();
if ua.contains("iphone") || ua.contains("ipad") || ua.contains("ipod") {
return InputProfile::MobileIOS;
}
if ua.contains("android") {
return InputProfile::MobileAndroid;
}
if pointer_coarse && (ua.contains("mac") || ua.contains("darwin")) {
return InputProfile::MobileIOS;
}
if pointer_coarse {
return InputProfile::MobileOther;
}
InputProfile::Desktop
}
const fn profile_code(p: InputProfile) -> u8 {
match p {
InputProfile::Desktop => 0,
InputProfile::MobileIOS => 1,
InputProfile::MobileAndroid => 2,
InputProfile::MobileOther => 3,
}
}
fn profile_from_code(c: u8) -> InputProfile {
match c {
1 => InputProfile::MobileIOS,
2 => InputProfile::MobileAndroid,
3 => InputProfile::MobileOther,
_ => InputProfile::Desktop,
}
}
#[cfg(test)]
pub(crate) fn profile_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ua_routes_to_correct_profile() {
assert_eq!(
input_profile_from_hint("Mozilla/5.0 (iPhone; CPU iPhone OS 17_4)", true),
InputProfile::MobileIOS
);
assert_eq!(
input_profile_from_hint("Mozilla/5.0 (Linux; Android 14; Pixel 8)", true),
InputProfile::MobileAndroid
);
assert_eq!(
input_profile_from_hint(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit",
true
),
InputProfile::MobileIOS
);
assert_eq!(
input_profile_from_hint(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit",
false
),
InputProfile::Desktop
);
assert_eq!(
input_profile_from_hint("CrOS x86_64", true),
InputProfile::MobileOther
);
}
#[test]
fn is_mobile_touch_helper() {
assert!(!InputProfile::Desktop.is_mobile_touch());
assert!(InputProfile::MobileIOS.is_mobile_touch());
assert!(InputProfile::MobileAndroid.is_mobile_touch());
assert!(InputProfile::MobileOther.is_mobile_touch());
}
#[test]
fn touch_ui_active_latches_on_a_real_touch_even_on_desktop_profile() {
let _guard = profile_test_lock();
set_input_profile(InputProfile::Desktop);
crate::touch_state::clear_last_touch_event_for_testing();
assert!(
!touch_ui_active(),
"desktop profile with no touch yet must not force touch sizing"
);
crate::touch_state::note_touch_event();
assert!(
touch_ui_active(),
"a real touch event must activate touch sizing regardless of profile"
);
crate::touch_state::clear_last_touch_event_for_testing();
}
#[test]
fn touch_ui_active_follows_mobile_profile_without_any_touch() {
let _guard = profile_test_lock();
crate::touch_state::clear_last_touch_event_for_testing();
set_input_profile(InputProfile::MobileIOS);
assert!(touch_ui_active());
set_input_profile(InputProfile::Desktop);
assert!(!touch_ui_active());
}
}