Skip to main content

use_touch_target/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive touch and click target size helpers.
3//!
4//! These helpers use a practical `44px` minimum dimension for recommended
5//! target sizing.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_touch_target::{
11//!     TouchTarget, is_touch_target_recommended, minimum_touch_target_px, touch_target_area,
12//! };
13//!
14//! let target = TouchTarget::new(48.0, 44.0).unwrap();
15//!
16//! assert_eq!(minimum_touch_target_px(), 44.0);
17//! assert_eq!(target.area_px(), 2_112.0);
18//! assert_eq!(target.min_dimension_px(), 44.0);
19//! assert!(target.is_recommended_size());
20//! assert!(is_touch_target_recommended(48.0, 44.0).unwrap());
21//! assert_eq!(touch_target_area(48.0, 44.0).unwrap(), 2_112.0);
22//! ```
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct TouchTarget {
26    width_px: f64,
27    height_px: f64,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum TouchTargetError {
32    InvalidWidth,
33    InvalidHeight,
34}
35
36fn validate_positive(value: f64, error: TouchTargetError) -> Result<f64, TouchTargetError> {
37    if !value.is_finite() || value <= 0.0 {
38        Err(error)
39    } else {
40        Ok(value)
41    }
42}
43
44#[must_use]
45pub fn minimum_touch_target_px() -> f64 {
46    44.0
47}
48
49impl TouchTarget {
50    pub fn new(width_px: f64, height_px: f64) -> Result<Self, TouchTargetError> {
51        Ok(Self {
52            width_px: validate_positive(width_px, TouchTargetError::InvalidWidth)?,
53            height_px: validate_positive(height_px, TouchTargetError::InvalidHeight)?,
54        })
55    }
56
57    #[must_use]
58    pub fn area_px(&self) -> f64 {
59        self.width_px * self.height_px
60    }
61
62    #[must_use]
63    pub fn min_dimension_px(&self) -> f64 {
64        self.width_px.min(self.height_px)
65    }
66
67    #[must_use]
68    pub fn is_recommended_size(&self) -> bool {
69        self.min_dimension_px() >= minimum_touch_target_px()
70    }
71}
72
73pub fn is_touch_target_recommended(
74    width_px: f64,
75    height_px: f64,
76) -> Result<bool, TouchTargetError> {
77    Ok(TouchTarget::new(width_px, height_px)?.is_recommended_size())
78}
79
80pub fn touch_target_area(width_px: f64, height_px: f64) -> Result<f64, TouchTargetError> {
81    Ok(TouchTarget::new(width_px, height_px)?.area_px())
82}
83
84#[cfg(test)]
85mod tests {
86    use super::{
87        TouchTarget, TouchTargetError, is_touch_target_recommended, minimum_touch_target_px,
88        touch_target_area,
89    };
90
91    #[test]
92    fn validates_touch_target_sizing() {
93        let target = TouchTarget::new(48.0, 44.0).unwrap();
94
95        assert_eq!(minimum_touch_target_px(), 44.0);
96        assert_eq!(target.area_px(), 2_112.0);
97        assert_eq!(target.min_dimension_px(), 44.0);
98        assert!(target.is_recommended_size());
99        assert!(is_touch_target_recommended(48.0, 44.0).unwrap());
100        assert_eq!(touch_target_area(48.0, 44.0).unwrap(), 2_112.0);
101        assert!(!is_touch_target_recommended(40.0, 44.0).unwrap());
102    }
103
104    #[test]
105    fn rejects_invalid_target_dimensions() {
106        assert_eq!(
107            TouchTarget::new(0.0, 44.0),
108            Err(TouchTargetError::InvalidWidth)
109        );
110        assert_eq!(
111            TouchTarget::new(44.0, f64::NAN),
112            Err(TouchTargetError::InvalidHeight)
113        );
114    }
115}