/// Platform-aware sizing constants. /// /// Platform HIG minimum touch targets, safe area handling, and /// density-independent pixel conventions. /// Which platform family the app is running on. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PlatformFamily { /// iOS / iPadOS Ios, /// Android Android, /// macOS Macos, /// Windows Windows, /// Linux desktop Linux, /// Web browser Web, } /// Platform-specific sizing constants. #[derive(Debug, Clone)] pub struct PlatformSizing { /// Minimum touch target dimension in dp (height and width). pub min_touch_target: f32, /// Standard icon size in dp. pub icon_size: f32, /// Standard small icon size in dp. pub icon_size_sm: f32, /// Standard navigation bar height in dp. pub nav_bar_height: f32, /// Standard tab bar height in dp. pub tab_bar_height: f32, /// Standard status bar height in dp (approximate; actual is platform-provided). pub status_bar_height: f32, } impl PlatformSizing { /// Sizing constants for a given platform. pub fn for_platform(platform: PlatformFamily) -> Self { match platform { PlatformFamily::Ios => Self { min_touch_target: 44.0, // Apple HIG icon_size: 24.0, icon_size_sm: 16.0, nav_bar_height: 44.0, tab_bar_height: 49.0, status_bar_height: 44.0, // approximate; varies with notch }, PlatformFamily::Android => Self { min_touch_target: 48.0, // Material Design icon_size: 24.0, icon_size_sm: 18.0, nav_bar_height: 56.0, tab_bar_height: 56.0, status_bar_height: 24.0, }, PlatformFamily::Macos => Self { min_touch_target: 44.0, // macOS HIG icon_size: 16.0, icon_size_sm: 12.0, nav_bar_height: 28.0, tab_bar_height: 36.0, status_bar_height: 0.0, // macOS status bar is system chrome }, PlatformFamily::Windows => Self { min_touch_target: 44.0, icon_size: 16.0, icon_size_sm: 12.0, nav_bar_height: 40.0, tab_bar_height: 40.0, status_bar_height: 0.0, }, PlatformFamily::Linux => Self { min_touch_target: 44.0, icon_size: 16.0, icon_size_sm: 12.0, nav_bar_height: 36.0, tab_bar_height: 36.0, status_bar_height: 0.0, }, PlatformFamily::Web => Self { min_touch_target: 44.0, // WCAG 2.5.5 recommended icon_size: 20.0, icon_size_sm: 16.0, nav_bar_height: 64.0, tab_bar_height: 48.0, status_bar_height: 0.0, }, } } /// Is the given width adequate for a touch target? pub fn is_touch_adequate(&self, width: f32, height: f32) -> bool { width >= self.min_touch_target && height >= self.min_touch_target } } /// Safe area insets — space reserved by system chrome. /// /// These are provided at runtime by the platform. The values here /// are conservative defaults for simulation. #[derive(Debug, Clone, Copy, Default)] pub struct SafeAreaInsets { pub top: f32, pub right: f32, pub bottom: f32, pub left: f32, } impl SafeAreaInsets { /// No safe area insets (desktop platforms). pub fn none() -> Self { Self::default() } /// Typical iPhone safe area (notch at top, home indicator at bottom). pub fn iphone_notch() -> Self { Self { top: 44.0, right: 0.0, bottom: 34.0, left: 0.0, } } /// Dynamic island (iPhone 14 Pro+). pub fn iphone_dynamic_island() -> Self { Self { top: 59.0, right: 0.0, bottom: 34.0, left: 0.0, } } /// Total horizontal safe area. pub fn horizontal(&self) -> f32 { self.left + self.right } /// Total vertical safe area. pub fn vertical(&self) -> f32 { self.top + self.bottom } } #[cfg(test)] mod tests { use super::*; #[test] fn ios_min_touch_target() { let sizing = PlatformSizing::for_platform(PlatformFamily::Ios); assert_eq!(sizing.min_touch_target, 44.0); } #[test] fn android_min_touch_target() { let sizing = PlatformSizing::for_platform(PlatformFamily::Android); assert_eq!(sizing.min_touch_target, 48.0); } #[test] fn touch_adequate_check() { let sizing = PlatformSizing::for_platform(PlatformFamily::Ios); assert!(sizing.is_touch_adequate(44.0, 44.0)); assert!(!sizing.is_touch_adequate(40.0, 44.0)); assert!(!sizing.is_touch_adequate(44.0, 40.0)); } #[test] fn safe_area_horizontal() { let sa = SafeAreaInsets { top: 44.0, right: 16.0, bottom: 34.0, left: 16.0, }; assert_eq!(sa.horizontal(), 32.0); } #[test] fn safe_area_vertical() { let sa = SafeAreaInsets::iphone_notch(); assert_eq!(sa.vertical(), 78.0); } #[test] fn no_safe_area_is_zero() { let sa = SafeAreaInsets::none(); assert_eq!(sa.horizontal(), 0.0); assert_eq!(sa.vertical(), 0.0); } }