use std::collections::hash_map::RandomState;
use std::time::Instant;
use std::{collections::HashSet, mem};
use rio_window::event::{ElementState, MouseButton, Touch, TouchPhase};
use crate::bindings::FontSizeAction;
use crate::event::ClickState;
use crate::router::Route;
#[derive(Debug, Default)]
pub enum TouchPurpose {
#[default]
None,
Select(Touch),
Scroll(Touch),
Zoom(TouchZoom),
Tap(Touch),
Invalid(HashSet<u64, RandomState>),
}
const FONT_SIZE_STEP: f32 = 1.00;
const TOUCH_ZOOM_FACTOR: f32 = 1.0;
pub const MAX_TAP_DISTANCE: f64 = 5.;
#[derive(Debug)]
pub struct TouchZoom {
slots: (Touch, Touch),
fractions: f32,
}
impl TouchZoom {
pub fn new(slots: (Touch, Touch)) -> Self {
Self {
slots,
fractions: Default::default(),
}
}
pub fn font_delta(&mut self, slot: Touch) -> f32 {
let old_distance = self.distance();
if slot.id == self.slots.0.id {
self.slots.0 = slot;
} else {
self.slots.1 = slot;
}
let delta = (self.distance() - old_distance) * TOUCH_ZOOM_FACTOR + self.fractions;
let font_delta =
(delta.abs() / FONT_SIZE_STEP).floor() * FONT_SIZE_STEP * delta.signum();
self.fractions = delta - font_delta;
font_delta
}
pub fn slots(&self) -> HashSet<u64, RandomState> {
let mut set = HashSet::default();
set.insert(self.slots.0.id);
set.insert(self.slots.1.id);
set
}
fn distance(&self) -> f32 {
let delta_x = self.slots.0.location.x - self.slots.1.location.x;
let delta_y = self.slots.0.location.y - self.slots.1.location.y;
delta_x.hypot(delta_y) as f32
}
}
#[inline]
pub fn on_touch(route: &mut Route, touch: Touch) {
match touch.phase {
TouchPhase::Started => {
on_touch_start(route, touch);
}
TouchPhase::Moved => on_touch_motion(route, touch),
TouchPhase::Ended | TouchPhase::Cancelled => on_touch_end(route, touch),
}
}
#[inline]
fn on_touch_start(route: &mut Route, touch: Touch) {
let touch_purpose = route.window.screen.touch_purpose();
*touch_purpose = match mem::take(touch_purpose) {
TouchPurpose::None => TouchPurpose::Tap(touch),
TouchPurpose::Tap(start) => TouchPurpose::Zoom(TouchZoom::new((start, touch))),
TouchPurpose::Zoom(zoom) => TouchPurpose::Invalid(zoom.slots()),
TouchPurpose::Scroll(event) | TouchPurpose::Select(event) => {
let mut set = HashSet::default();
set.insert(event.id);
TouchPurpose::Invalid(set)
}
TouchPurpose::Invalid(mut slots) => {
slots.insert(touch.id);
TouchPurpose::Invalid(slots)
}
};
}
#[inline]
fn on_touch_motion(route: &mut Route, touch: Touch) {
let touch_purpose = route.window.screen.touch_purpose();
match touch_purpose {
TouchPurpose::None => (),
TouchPurpose::Tap(start) => {
let delta_x = touch.location.x - start.location.x;
let delta_y = touch.location.y - start.location.y;
if delta_x.abs() > MAX_TAP_DISTANCE {
tracing::info!("tap to select");
let start_location = start.location;
*touch_purpose = TouchPurpose::Select(*start);
let layout = route.window.screen.sugarloaf.window_size();
let x = start_location.x.clamp(0.0, layout.width.into()) as usize;
let y = start_location.y.clamp(0.0, layout.height.into()) as usize;
route.window.screen.mouse.x = x;
route.window.screen.mouse.y = y;
let now = Instant::now();
route.window.screen.mouse.last_click_timestamp = now;
route.window.screen.mouse.last_click_button = MouseButton::Left;
route.window.screen.mouse.click_state = ClickState::Click;
route.window.screen.mouse.left_button_state = ElementState::Pressed;
route
.window
.screen
.on_left_click(route.window.screen.mouse_position(0));
on_touch_motion(route, touch);
} else if delta_y.abs() > MAX_TAP_DISTANCE {
tracing::info!("tap to scroll");
*touch_purpose = TouchPurpose::Scroll(*start);
on_touch_motion(route, touch);
} else {
tracing::info!("tap normal");
}
}
TouchPurpose::Zoom(zoom) => {
let font_delta = zoom.font_delta(touch);
if font_delta >= 0. {
route
.window
.screen
.change_font_size(FontSizeAction::Increase);
} else {
route
.window
.screen
.change_font_size(FontSizeAction::Decrease);
}
tracing::info!("zoom motion: {}", font_delta);
}
TouchPurpose::Scroll(last_touch) => {
let delta_y = touch.location.y - last_touch.location.y;
*touch_purpose = TouchPurpose::Scroll(touch);
route.window.screen.scroll(0., delta_y);
tracing::info!("scroll motion: {}", delta_y);
}
TouchPurpose::Select(_) => {
let layout = route.window.screen.sugarloaf.window_size();
let x = touch.location.x.clamp(0.0, layout.width.into()) as usize;
let y = touch.location.y.clamp(0.0, layout.height.into()) as usize;
route.window.screen.mouse.x = x;
route.window.screen.mouse.y = y;
tracing::info!("select motion");
}
TouchPurpose::Invalid(_) => (),
}
}
#[inline]
fn on_touch_end(route: &mut Route, touch: Touch) {
on_touch_motion(route, touch);
let touch_purpose = route.window.screen.touch_purpose();
match touch_purpose {
TouchPurpose::Tap(start) => {
let start_location = start.location;
*touch_purpose = Default::default();
let layout = route.window.screen.sugarloaf.window_size();
let x = start_location.x.clamp(0.0, layout.width.into()) as usize;
let y = start_location.y.clamp(0.0, layout.height.into()) as usize;
route.window.screen.mouse.x = x;
route.window.screen.mouse.y = y;
let now = Instant::now();
route.window.screen.mouse.last_click_timestamp = now;
route.window.screen.mouse.last_click_button = MouseButton::Left;
route.window.screen.mouse.click_state = ClickState::Click;
route.window.screen.mouse.left_button_state = ElementState::Pressed;
route
.window
.screen
.on_left_click(route.window.screen.mouse_position(0));
route.window.screen.mouse.click_state = ClickState::None;
route.window.screen.mouse.left_button_state = ElementState::Released;
tracing::info!("tap end");
}
TouchPurpose::Zoom(zoom) => {
let mut slots = zoom.slots();
slots.remove(&touch.id);
*touch_purpose = TouchPurpose::Invalid(slots);
tracing::info!("zoom end");
}
TouchPurpose::Invalid(slots) => {
slots.remove(&touch.id);
if slots.is_empty() {
*touch_purpose = Default::default();
}
}
TouchPurpose::Select(_) => {
*touch_purpose = Default::default();
route.window.screen.mouse.click_state = ClickState::None;
route.window.screen.mouse.left_button_state = ElementState::Released;
tracing::info!("select end");
}
TouchPurpose::Scroll(_) => {
*touch_purpose = Default::default();
tracing::info!("scroll end");
}
TouchPurpose::None => (),
}
}