use crate::core::DeviceClass;
use crate::core::Size;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScreenOrientation {
Portrait,
Landscape,
ReversePortrait,
ReverseLandscape,
}
#[derive(Debug, Clone)]
pub struct DeviceEnvironment {
pub device_class: DeviceClass,
pub touch_capable: bool,
pub screen_size: Size,
pub dpi_scale: f32,
pub projection_mode: bool,
pub touch_target_min: Size,
pub touch_spacing: u32,
pub orientation: ScreenOrientation,
pub high_contrast: bool,
pub reduced_motion: bool,
pub font_scale: f32,
}
impl DeviceEnvironment {
pub fn detect(screen_size: Size, dpi_scale: f32) -> Self {
let device_class = Self::resolve_device_class(screen_size, dpi_scale);
let touch_capable = Self::resolve_touch_capability(&device_class);
let projection_mode = device_class == DeviceClass::Projector;
let (touch_target_min, touch_spacing) = Self::touch_target_params(device_class);
let orientation = Self::resolve_orientation(screen_size);
Self {
device_class,
touch_capable,
screen_size,
dpi_scale,
projection_mode,
touch_target_min,
touch_spacing,
orientation,
high_contrast: false,
reduced_motion: false,
font_scale: 1.0,
}
}
fn resolve_device_class(_screen_size: Size, _dpi_scale: f32) -> DeviceClass {
#[cfg(feature = "tablet")]
{
DeviceClass::Tablet
}
#[cfg(all(not(feature = "tablet"), feature = "mobile"))]
{
DeviceClass::Mobile
}
#[cfg(all(not(feature = "tablet"), not(feature = "mobile"), feature = "embedded"))]
{
DeviceClass::Embedded
}
#[cfg(all(not(feature = "tablet"), not(feature = "mobile"), not(feature = "embedded")))]
{
let width = _screen_size.width.max(320);
if width < 480 {
DeviceClass::Mobile
} else if width < 1024 {
DeviceClass::Tablet
} else if _dpi_scale >= 2.0 && width < 1440 {
DeviceClass::Tablet
} else {
DeviceClass::Desktop
}
}
}
fn resolve_touch_capability(class: &DeviceClass) -> bool {
#[cfg(feature = "touch")]
{
let _ = class;
!matches!(class, DeviceClass::Projector)
}
#[cfg(not(feature = "touch"))]
{
let _ = class;
false
}
}
const fn touch_target_params(class: DeviceClass) -> (Size, u32) {
match class {
DeviceClass::Desktop => (Size::new(32, 32), 8),
DeviceClass::Tablet => (Size::new(44, 44), 12),
DeviceClass::Mobile => (Size::new(48, 48), 16),
DeviceClass::Embedded => (Size::new(40, 40), 10),
DeviceClass::Projector => (Size::new(24, 24), 6),
}
}
pub fn is_touch_device(&self) -> bool {
self.touch_capable
}
pub fn is_projection(&self) -> bool {
self.projection_mode
}
pub fn min_touch_target(&self) -> Size {
self.touch_target_min
}
pub fn touch_spacing(&self) -> u32 {
self.touch_spacing
}
fn resolve_orientation(screen_size: Size) -> ScreenOrientation {
if screen_size.width > screen_size.height {
ScreenOrientation::Landscape
} else {
ScreenOrientation::Portrait
}
}
pub fn set_orientation(&mut self, orientation: ScreenOrientation) {
self.orientation = orientation;
}
pub fn recheck(&mut self, screen_size: Size, dpi_scale: f32) {
self.screen_size = screen_size;
self.dpi_scale = dpi_scale;
self.orientation = Self::resolve_orientation(screen_size);
}
pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.dpi_scale = dpi_scale;
}
pub fn set_high_contrast(&mut self, enabled: bool) {
self.high_contrast = enabled;
}
pub fn set_reduced_motion(&mut self, enabled: bool) {
self.reduced_motion = enabled;
}
pub fn set_font_scale(&mut self, scale: f32) {
self.font_scale = scale.clamp(0.5, 3.0);
}
pub fn layout_scale(&self) -> f32 {
match self.device_class {
DeviceClass::Projector => 1.2,
_ => 1.0,
}
}
}
impl Default for DeviceEnvironment {
fn default() -> Self {
Self {
device_class: DeviceClass::Desktop,
touch_capable: false,
screen_size: Size::new(1024, 768),
dpi_scale: 1.0,
projection_mode: false,
touch_target_min: Size::new(32, 32),
touch_spacing: 8,
orientation: ScreenOrientation::Landscape,
high_contrast: false,
reduced_motion: false,
font_scale: 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_desktop_no_touch() {
let env = DeviceEnvironment::default();
assert_eq!(env.device_class, DeviceClass::Desktop);
assert!(!env.touch_capable);
assert!(!env.is_projection());
assert_eq!(env.touch_target_min, Size::new(32, 32));
assert_eq!(env.touch_spacing, 8);
}
#[test]
fn large_screen_heuristic_is_desktop() {
let env = DeviceEnvironment::detect(Size::new(1920, 1080), 1.0);
if cfg!(not(any(feature = "tablet", feature = "mobile", feature = "embedded"))) {
assert_eq!(env.device_class, DeviceClass::Desktop);
} else {
assert!(matches!(
env.device_class,
DeviceClass::Desktop
| DeviceClass::Tablet
| DeviceClass::Mobile
| DeviceClass::Embedded
));
}
}
#[test]
fn small_screen_heuristic_is_mobile() {
let env = DeviceEnvironment::detect(Size::new(360, 640), 2.0);
if cfg!(not(any(feature = "tablet", feature = "mobile", feature = "embedded"))) {
assert_eq!(env.device_class, DeviceClass::Mobile);
} else {
assert!(matches!(
env.device_class,
DeviceClass::Desktop
| DeviceClass::Tablet
| DeviceClass::Mobile
| DeviceClass::Embedded
));
}
}
#[test]
fn medium_screen_heuristic_is_tablet() {
let env = DeviceEnvironment::detect(Size::new(768, 1024), 1.0);
if cfg!(not(any(feature = "tablet", feature = "mobile", feature = "embedded"))) {
assert_eq!(env.device_class, DeviceClass::Tablet);
} else {
assert!(matches!(
env.device_class,
DeviceClass::Desktop
| DeviceClass::Tablet
| DeviceClass::Mobile
| DeviceClass::Embedded
));
}
}
#[test]
fn projection_mode_layout_scale() {
let env = DeviceEnvironment {
device_class: DeviceClass::Projector,
projection_mode: true,
..Default::default()
};
assert!(env.is_projection());
assert!((env.layout_scale() - 1.2).abs() < f32::EPSILON);
}
#[test]
fn touch_target_params_match_device_class() {
let make_env = |class: DeviceClass| -> DeviceEnvironment {
let (min, spacing) = DeviceEnvironment::touch_target_params(class);
DeviceEnvironment {
device_class: class,
touch_target_min: min,
touch_spacing: spacing,
..Default::default()
}
};
let desktop = make_env(DeviceClass::Desktop);
let tablet = make_env(DeviceClass::Tablet);
let mobile = make_env(DeviceClass::Mobile);
let embedded = make_env(DeviceClass::Embedded);
let projector = make_env(DeviceClass::Projector);
assert_eq!(desktop.touch_target_min, Size::new(32, 32));
assert_eq!(tablet.touch_target_min, Size::new(44, 44));
assert_eq!(mobile.touch_target_min, Size::new(48, 48));
assert_eq!(embedded.touch_target_min, Size::new(40, 40));
assert_eq!(projector.touch_target_min, Size::new(24, 24));
}
#[test]
fn touch_capable_when_feature_enabled() {
let env = DeviceEnvironment::default();
assert!(!env.touch_capable);
}
#[test]
fn touch_spacing_values() {
assert_eq!(DeviceEnvironment::touch_target_params(DeviceClass::Desktop).1, 8);
assert_eq!(DeviceEnvironment::touch_target_params(DeviceClass::Tablet).1, 12);
assert_eq!(DeviceEnvironment::touch_target_params(DeviceClass::Mobile).1, 16);
assert_eq!(DeviceEnvironment::touch_target_params(DeviceClass::Embedded).1, 10);
assert_eq!(DeviceEnvironment::touch_target_params(DeviceClass::Projector).1, 6);
}
#[test]
fn orientation_landscape_when_width_greater_than_height() {
let env = DeviceEnvironment::detect(Size::new(1920, 1080), 1.0);
assert_eq!(env.orientation, ScreenOrientation::Landscape);
}
#[test]
fn orientation_portrait_when_height_greater_than_width() {
let env = DeviceEnvironment::detect(Size::new(1080, 1920), 1.0);
assert_eq!(env.orientation, ScreenOrientation::Portrait);
}
#[test]
fn orientation_square_defaults_to_portrait() {
let env = DeviceEnvironment::detect(Size::new(1024, 1024), 1.0);
assert_eq!(env.orientation, ScreenOrientation::Portrait);
}
#[test]
fn set_orientation_changes_value() {
let mut env = DeviceEnvironment::detect(Size::new(1920, 1080), 1.0);
assert_eq!(env.orientation, ScreenOrientation::Landscape);
env.set_orientation(ScreenOrientation::Portrait);
assert_eq!(env.orientation, ScreenOrientation::Portrait);
env.set_orientation(ScreenOrientation::ReverseLandscape);
assert_eq!(env.orientation, ScreenOrientation::ReverseLandscape);
env.set_orientation(ScreenOrientation::ReversePortrait);
assert_eq!(env.orientation, ScreenOrientation::ReversePortrait);
}
#[test]
fn default_orientation_is_landscape() {
let env = DeviceEnvironment::default();
assert_eq!(env.orientation, ScreenOrientation::Landscape);
}
}