Archived
feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user