blinc_platform_ios 0.5.1

Blinc iOS platform - UIKit integration, Metal rendering, and touch input
Documentation
//! iOS touch input handling
//!
//! Converts UITouch events to Blinc input events.

use blinc_platform::{InputEvent, TouchEvent};

/// Touch phase from UITouch
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchPhase {
    /// Touch began
    Began,
    /// Touch moved
    Moved,
    /// Touch ended
    Ended,
    /// Touch cancelled
    Cancelled,
}

/// A single touch point
#[derive(Clone, Debug)]
pub struct Touch {
    /// Unique identifier for this touch
    pub id: u64,
    /// X position in logical pixels
    pub x: f32,
    /// Y position in logical pixels
    pub y: f32,
    /// Touch phase
    pub phase: TouchPhase,
    /// Force of the touch (0.0 - 1.0 on 3D Touch devices)
    pub force: f32,
}

impl Touch {
    /// Create a new touch event
    pub fn new(id: u64, x: f32, y: f32, phase: TouchPhase) -> Self {
        Self {
            id,
            x,
            y,
            phase,
            force: 0.0,
        }
    }

    /// Create a touch with force (3D Touch)
    pub fn with_force(id: u64, x: f32, y: f32, phase: TouchPhase, force: f32) -> Self {
        Self {
            id,
            x,
            y,
            phase,
            force,
        }
    }
}

/// Convert an iOS touch to a Blinc input event
pub fn convert_touch(touch: &Touch) -> InputEvent {
    match touch.phase {
        TouchPhase::Began => InputEvent::Touch(TouchEvent::Started {
            id: touch.id,
            x: touch.x,
            y: touch.y,
            pressure: touch.force,
        }),
        TouchPhase::Moved => InputEvent::Touch(TouchEvent::Moved {
            id: touch.id,
            x: touch.x,
            y: touch.y,
            pressure: touch.force,
        }),
        TouchPhase::Ended => InputEvent::Touch(TouchEvent::Ended {
            id: touch.id,
            x: touch.x,
            y: touch.y,
        }),
        TouchPhase::Cancelled => InputEvent::Touch(TouchEvent::Cancelled { id: touch.id }),
    }
}

/// Convert multiple touches to Blinc input events
pub fn convert_touches(touches: &[Touch]) -> Vec<InputEvent> {
    touches.iter().map(convert_touch).collect()
}

/// Gesture detector for common iOS gestures
#[derive(Debug, Default)]
pub struct GestureDetector {
    /// Active touches
    active_touches: Vec<Touch>,
    /// Whether a tap is in progress
    tap_in_progress: bool,
    /// Start position of potential tap
    tap_start: Option<(f32, f32)>,
    /// Last pinch span between two touches
    pinch_span: Option<f32>,
}

impl GestureDetector {
    /// Create a new gesture detector
    pub fn new() -> Self {
        Self::default()
    }

    fn pinch_span_and_center(&self) -> Option<(f32, (f32, f32))> {
        if self.active_touches.len() < 2 {
            return None;
        }

        let first = &self.active_touches[0];
        let second = &self.active_touches[1];
        let dx = second.x - first.x;
        let dy = second.y - first.y;
        let span = (dx * dx + dy * dy).sqrt();
        let center = ((first.x + second.x) * 0.5, (first.y + second.y) * 0.5);
        Some((span, center))
    }

    /// Process a touch event and detect gestures
    pub fn process(&mut self, touch: &Touch) -> Option<Gesture> {
        match touch.phase {
            TouchPhase::Began => {
                self.active_touches.push(touch.clone());
                if self.active_touches.len() == 1 {
                    self.tap_in_progress = true;
                    self.tap_start = Some((touch.x, touch.y));
                } else {
                    self.tap_in_progress = false;
                    self.tap_start = None;
                    if self.active_touches.len() == 2 {
                        self.pinch_span = self.pinch_span_and_center().map(|(span, _)| span);
                    } else {
                        self.pinch_span = None;
                    }
                }
                None
            }
            TouchPhase::Moved => {
                // Update touch position
                if let Some(existing) = self.active_touches.iter_mut().find(|t| t.id == touch.id) {
                    // Check if moved too far for a tap
                    if let Some((start_x, start_y)) = self.tap_start {
                        let dx = touch.x - start_x;
                        let dy = touch.y - start_y;
                        if dx * dx + dy * dy > 100.0 {
                            // 10pt threshold
                            self.tap_in_progress = false;
                        }
                    }
                    existing.x = touch.x;
                    existing.y = touch.y;
                }
                let touch_count = self.active_touches.len();
                if touch_count == 2 {
                    self.tap_in_progress = false;
                    self.tap_start = None;
                    if let Some((span, center)) = self.pinch_span_and_center() {
                        if !span.is_finite() {
                            self.pinch_span = None;
                            return None;
                        }
                        let scale = match self.pinch_span {
                            Some(previous) if previous.is_finite() && previous > 0.0 => {
                                span / previous
                            }
                            _ => 1.0,
                        };
                        self.pinch_span = Some(span);
                        if !scale.is_finite() {
                            return None;
                        }
                        return Some(Gesture::Pinch {
                            scale: scale.clamp(0.90, 1.10),
                            center,
                        });
                    }
                } else {
                    if touch_count > 1 {
                        self.tap_in_progress = false;
                        self.tap_start = None;
                    }
                    self.pinch_span = None;
                }
                None
            }
            TouchPhase::Ended => {
                self.active_touches.retain(|t| t.id != touch.id);
                if self.active_touches.len() < 2 {
                    self.pinch_span = None;
                }

                // Check for tap
                if self.tap_in_progress {
                    self.tap_in_progress = false;
                    self.tap_start = None;
                    return Some(Gesture::Tap {
                        x: touch.x,
                        y: touch.y,
                    });
                }

                self.tap_start = None;
                None
            }
            TouchPhase::Cancelled => {
                self.active_touches.retain(|t| t.id != touch.id);
                self.tap_in_progress = false;
                self.tap_start = None;
                self.pinch_span = None;
                None
            }
        }
    }

    /// Get the number of active touches
    pub fn active_touch_count(&self) -> usize {
        self.active_touches.len()
    }
}

/// Detected gestures
#[derive(Clone, Debug)]
pub enum Gesture {
    /// Single tap
    Tap { x: f32, y: f32 },
    /// Long press
    LongPress { x: f32, y: f32 },
    /// Pan/drag gesture
    Pan {
        dx: f32,
        dy: f32,
        velocity: (f32, f32),
    },
    /// Pinch gesture (for zoom)
    Pinch { scale: f32, center: (f32, f32) },
    /// Rotation gesture
    Rotation { angle: f32, center: (f32, f32) },
}