use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TargetPlatform {
Android,
Fuchsia,
Ios,
Linux,
MacOs,
Windows,
}
impl TargetPlatform {
pub const ALL: [TargetPlatform; 6] = [
TargetPlatform::Android,
TargetPlatform::Fuchsia,
TargetPlatform::Ios,
TargetPlatform::Linux,
TargetPlatform::MacOs,
TargetPlatform::Windows,
];
pub const fn of_build() -> Self {
if cfg!(target_os = "ios") {
TargetPlatform::Ios
} else if cfg!(target_os = "macos") {
TargetPlatform::MacOs
} else if cfg!(target_os = "windows") {
TargetPlatform::Windows
} else if cfg!(target_os = "fuchsia") {
TargetPlatform::Fuchsia
} else if cfg!(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
)) {
TargetPlatform::Linux
} else {
TargetPlatform::Android
}
}
pub fn from_browser(platform: &str, user_agent: &str, max_touch_points: i32) -> Self {
let lower = platform.to_ascii_lowercase();
if platform.starts_with("Mac") {
if max_touch_points > 2 {
TargetPlatform::Ios
} else {
TargetPlatform::MacOs
}
} else if lower.contains("iphone") || lower.contains("ipad") || lower.contains("ipod") {
TargetPlatform::Ios
} else if user_agent.contains("Android") {
TargetPlatform::Android
} else if platform.starts_with("Linux") {
TargetPlatform::Linux
} else if platform.starts_with("Win") {
TargetPlatform::Windows
} else {
TargetPlatform::Android
}
}
pub const fn is_apple(self) -> bool {
matches!(self, TargetPlatform::Ios | TargetPlatform::MacOs)
}
pub const fn is_mobile(self) -> bool {
matches!(
self,
TargetPlatform::Android | TargetPlatform::Fuchsia | TargetPlatform::Ios
)
}
pub const fn is_desktop(self) -> bool {
!self.is_mobile()
}
pub const fn name(self) -> &'static str {
match self {
TargetPlatform::Android => "Android",
TargetPlatform::Fuchsia => "Fuchsia",
TargetPlatform::Ios => "iOS",
TargetPlatform::Linux => "Linux",
TargetPlatform::MacOs => "macOS",
TargetPlatform::Windows => "Windows",
}
}
const fn code(self) -> u8 {
match self {
TargetPlatform::Android => 1,
TargetPlatform::Fuchsia => 2,
TargetPlatform::Ios => 3,
TargetPlatform::Linux => 4,
TargetPlatform::MacOs => 5,
TargetPlatform::Windows => 6,
}
}
const fn from_code(code: u8) -> Option<Self> {
match code {
1 => Some(TargetPlatform::Android),
2 => Some(TargetPlatform::Fuchsia),
3 => Some(TargetPlatform::Ios),
4 => Some(TargetPlatform::Linux),
5 => Some(TargetPlatform::MacOs),
6 => Some(TargetPlatform::Windows),
_ => None,
}
}
}
impl std::fmt::Display for TargetPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
pub const IS_WEB: bool = cfg!(target_arch = "wasm32");
static OVERRIDE: AtomicU8 = AtomicU8::new(0);
static DETECTED: AtomicU8 = AtomicU8::new(0);
pub fn default_target_platform() -> TargetPlatform {
debug_default_target_platform_override()
.or_else(|| TargetPlatform::from_code(DETECTED.load(Ordering::Relaxed)))
.unwrap_or(TargetPlatform::of_build())
}
pub fn debug_default_target_platform_override() -> Option<TargetPlatform> {
TargetPlatform::from_code(OVERRIDE.load(Ordering::Relaxed))
}
pub fn set_debug_default_target_platform_override(platform: Option<TargetPlatform>) {
if !cfg!(debug_assertions) {
panic!(
"the default target platform can only be overridden in a debug build; \
set `Theme::platform` instead"
);
}
OVERRIDE.store(platform.map_or(0, TargetPlatform::code), Ordering::Relaxed);
}
#[doc(hidden)]
pub fn __set_detected_target_platform(platform: TargetPlatform) {
DETECTED.store(platform.code(), Ordering::Relaxed);
}
pub struct Platform;
impl Platform {
pub const fn operating_system() -> &'static str {
std::env::consts::OS
}
pub const fn is_android() -> bool {
cfg!(target_os = "android")
}
pub const fn is_ios() -> bool {
cfg!(target_os = "ios")
}
pub const fn is_linux() -> bool {
cfg!(target_os = "linux")
}
pub const fn is_macos() -> bool {
cfg!(target_os = "macos")
}
pub const fn is_windows() -> bool {
cfg!(target_os = "windows")
}
pub const fn is_fuchsia() -> bool {
cfg!(target_os = "fuchsia")
}
pub const fn is_web() -> bool {
IS_WEB
}
pub fn number_of_processors() -> usize {
std::thread::available_parallelism().map_or(1, |n| n.get())
}
pub const fn path_separator() -> char {
std::path::MAIN_SEPARATOR
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_is_the_platform_built_for() {
let expected = if cfg!(target_os = "windows") {
TargetPlatform::Windows
} else if cfg!(target_os = "macos") {
TargetPlatform::MacOs
} else if cfg!(target_os = "linux") {
TargetPlatform::Linux
} else {
TargetPlatform::of_build()
};
assert_eq!(TargetPlatform::of_build(), expected);
assert_eq!(Platform::operating_system(), std::env::consts::OS);
assert_eq!(Platform::is_web(), IS_WEB);
}
#[test]
fn the_debug_override_wins_until_it_is_cleared() {
let native = default_target_platform();
for platform in TargetPlatform::ALL {
set_debug_default_target_platform_override(Some(platform));
assert_eq!(default_target_platform(), platform);
assert_eq!(debug_default_target_platform_override(), Some(platform));
}
set_debug_default_target_platform_override(None);
assert_eq!(default_target_platform(), native);
assert_eq!(debug_default_target_platform_override(), None);
}
#[test]
fn a_browser_reports_the_system_it_runs_on() {
let cases = [
(
"Win32",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
0,
TargetPlatform::Windows,
),
(
"MacIntel",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
0,
TargetPlatform::MacOs,
),
(
"MacIntel",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
5,
TargetPlatform::Ios,
),
(
"iPhone",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
5,
TargetPlatform::Ios,
),
(
"Linux armv8l",
"Mozilla/5.0 (Linux; Android 14; Pixel 8)",
5,
TargetPlatform::Android,
),
(
"Linux x86_64",
"Mozilla/5.0 (X11; Linux x86_64; rv:120.0)",
0,
TargetPlatform::Linux,
),
("", "", 0, TargetPlatform::Android),
];
for (platform, agent, touch, expected) in cases {
assert_eq!(
TargetPlatform::from_browser(platform, agent, touch),
expected,
"{platform:?} / {agent:?} / {touch}"
);
}
}
#[test]
fn platforms_fall_into_families() {
let apple: Vec<_> = TargetPlatform::ALL
.into_iter()
.filter(|p| p.is_apple())
.collect();
assert_eq!(apple, [TargetPlatform::Ios, TargetPlatform::MacOs]);
let mobile: Vec<_> = TargetPlatform::ALL
.into_iter()
.filter(|p| p.is_mobile())
.collect();
assert_eq!(
mobile,
[
TargetPlatform::Android,
TargetPlatform::Fuchsia,
TargetPlatform::Ios
]
);
for platform in TargetPlatform::ALL {
assert_ne!(platform.is_mobile(), platform.is_desktop());
assert_eq!(TargetPlatform::from_code(platform.code()), Some(platform));
}
assert_eq!(TargetPlatform::from_code(0), None);
assert_eq!(TargetPlatform::Ios.to_string(), "iOS");
assert_eq!(TargetPlatform::MacOs.name(), "macOS");
}
}