use bevy::prelude::*;
pub mod dualshock4_touchpad {
pub const WIDTH: u16 = 1920;
pub const HEIGHT: u16 = 943;
}
pub mod dualsense_touchpad {
pub const WIDTH: u16 = 1920;
pub const HEIGHT: u16 = 1080;
}
pub mod steam_touchpad {
pub const WIDTH: u16 = 32767;
pub const HEIGHT: u16 = 32767;
}
#[must_use]
pub fn normalize_coords(x: u16, y: u16, max_x: u16, max_y: u16) -> (f32, f32) {
(
f32::from(x) / f32::from(max_x),
f32::from(y) / f32::from(max_y),
)
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "intentional conversion to u16 touchpad coordinates"
)]
pub fn denormalize_coords(x: f32, y: f32, max_x: u16, max_y: u16) -> (u16, u16) {
((x * f32::from(max_x)) as u16, (y * f32::from(max_y)) as u16)
}
#[derive(Debug, Clone, Copy, Default, Reflect)]
pub struct TouchFinger {
pub x: f32,
pub y: f32,
pub active: bool,
pub id: u8,
}
impl TouchFinger {
#[must_use]
pub fn new(id: u8, x: f32, y: f32) -> Self {
Self {
x: x.clamp(0.0, 1.0),
y: y.clamp(0.0, 1.0),
active: true,
id,
}
}
#[must_use]
pub fn position(&self) -> Vec2 {
Vec2::new(self.x, self.y)
}
}
#[derive(Debug, Clone, Component, Reflect)]
pub struct TouchpadData {
pub finger1: TouchFinger,
pub finger2: TouchFinger,
pub button_pressed: bool,
pub prev_finger1: Vec2,
pub prev_finger2: Vec2,
}
impl Default for TouchpadData {
fn default() -> Self {
Self {
finger1: TouchFinger::default(),
finger2: TouchFinger::default(),
button_pressed: false,
prev_finger1: Vec2::ZERO,
prev_finger2: Vec2::ZERO,
}
}
}
impl TouchpadData {
#[must_use]
pub fn finger1_delta(&self) -> Vec2 {
if !self.finger1.active {
return Vec2::ZERO;
}
self.finger1.position() - self.prev_finger1
}
#[must_use]
pub fn finger2_delta(&self) -> Vec2 {
if !self.finger2.active {
return Vec2::ZERO;
}
self.finger2.position() - self.prev_finger2
}
#[must_use]
pub fn is_swiping(&self, threshold: f32) -> bool {
self.finger1_delta().length() > threshold
}
#[must_use]
pub fn is_pinching(&self) -> Option<f32> {
if !self.finger1.active || !self.finger2.active {
return None;
}
let current_dist = self.finger1.position().distance(self.finger2.position());
let prev_dist = self.prev_finger1.distance(self.prev_finger2);
let delta = current_dist - prev_dist;
if delta.abs() > 0.01 {
Some(delta) } else {
None
}
}
#[must_use]
pub fn active_fingers(&self) -> u8 {
let mut count = 0;
if self.finger1.active {
count += 1;
}
if self.finger2.active {
count += 1;
}
count
}
pub fn set_finger(&mut self, finger_index: usize, x: f32, y: f32, active: bool) {
let finger = if finger_index == 0 {
&mut self.finger1
} else {
&mut self.finger2
};
finger.x = x.clamp(0.0, 1.0);
finger.y = y.clamp(0.0, 1.0);
finger.active = active;
}
pub fn set_finger_raw(
&mut self,
finger_index: usize,
raw_x: u16,
raw_y: u16,
max_x: u16,
max_y: u16,
active: bool,
) {
let (x, y) = normalize_coords(raw_x, raw_y, max_x, max_y);
self.set_finger(finger_index, x, y, active);
}
pub fn update_frame(&mut self) {
self.prev_finger1 = self.finger1.position();
self.prev_finger2 = self.finger2.position();
}
#[cfg(feature = "motion-backends")]
pub fn update_from_backend(&mut self, data: &crate::motion::backend::TouchpadData) {
self.finger1.x = data.finger1.x.clamp(0.0, 1.0);
self.finger1.y = data.finger1.y.clamp(0.0, 1.0);
self.finger1.active = data.finger1.active;
self.finger1.id = data.finger1.id;
self.finger2.x = data.finger2.x.clamp(0.0, 1.0);
self.finger2.y = data.finger2.y.clamp(0.0, 1.0);
self.finger2.active = data.finger2.active;
self.finger2.id = data.finger2.id;
self.button_pressed = data.button_pressed;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
pub enum TouchpadGesture {
Tap,
TwoFingerTap,
SwipeLeft,
SwipeRight,
SwipeUp,
SwipeDown,
PinchIn,
PinchOut,
}
#[derive(Debug, Clone, Message)]
pub struct TouchpadGestureEvent {
pub gamepad: Entity,
pub gesture: TouchpadGesture,
pub position: Vec2,
pub intensity: f32,
}
#[derive(Debug, Clone, Resource)]
pub struct TouchpadConfig {
pub swipe_threshold: f32,
pub tap_time_window: f32,
pub enabled: bool,
}
impl Default for TouchpadConfig {
fn default() -> Self {
Self {
swipe_threshold: 0.15,
tap_time_window: 0.2,
enabled: true,
}
}
}
pub fn update_touchpad_data(
mut gamepads: Query<(Entity, &Gamepad, Option<&mut TouchpadData>)>,
mut commands: Commands,
) {
for (entity, _gamepad, touchpad) in &mut gamepads {
if touchpad.is_none() {
commands.entity(entity).insert(TouchpadData::default());
}
}
}
pub fn detect_touchpad_gestures(
mut gamepads: Query<(Entity, &mut TouchpadData)>,
config: Res<TouchpadConfig>,
mut gesture_events: MessageWriter<TouchpadGestureEvent>,
) {
if !config.enabled {
return;
}
for (entity, mut touchpad) in &mut gamepads {
let delta = touchpad.finger1_delta();
if delta.length() > config.swipe_threshold {
let gesture = if delta.x.abs() > delta.y.abs() {
if delta.x > 0.0 {
TouchpadGesture::SwipeRight
} else {
TouchpadGesture::SwipeLeft
}
} else if delta.y > 0.0 {
TouchpadGesture::SwipeDown
} else {
TouchpadGesture::SwipeUp
};
gesture_events.write(TouchpadGestureEvent {
gamepad: entity,
gesture,
position: touchpad.finger1.position(),
intensity: delta.length(),
});
}
if let Some(pinch_delta) = touchpad.is_pinching() {
let gesture = if pinch_delta > 0.0 {
TouchpadGesture::PinchOut
} else {
TouchpadGesture::PinchIn
};
gesture_events.write(TouchpadGestureEvent {
gamepad: entity,
gesture,
position: (touchpad.finger1.position() + touchpad.finger2.position()) / 2.0,
intensity: pinch_delta.abs(),
});
}
touchpad.prev_finger1 = touchpad.finger1.position();
touchpad.prev_finger2 = touchpad.finger2.position();
}
}
pub(crate) fn register_touchpad_types(app: &mut App) {
app.register_type::<TouchFinger>()
.register_type::<TouchpadData>()
.register_type::<TouchpadGesture>()
.init_resource::<TouchpadConfig>()
.add_message::<TouchpadGestureEvent>();
}
pub(crate) fn add_touchpad_systems(app: &mut App) {
app.add_systems(
Update,
(update_touchpad_data, detect_touchpad_gestures).chain(),
);
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_touch_finger_new() {
let finger = TouchFinger::new(1, 0.5, 0.7);
assert_eq!(finger.id, 1);
assert_relative_eq!(finger.x, 0.5);
assert_relative_eq!(finger.y, 0.7);
assert!(finger.active);
}
#[test]
fn test_touch_finger_new_clamps_values() {
let finger = TouchFinger::new(0, -0.5, 1.5);
assert_relative_eq!(finger.x, 0.0);
assert_relative_eq!(finger.y, 1.0);
}
#[test]
fn test_touch_finger_position() {
let finger = TouchFinger::new(0, 0.3, 0.8);
let pos = finger.position();
assert_relative_eq!(pos.x, 0.3);
assert_relative_eq!(pos.y, 0.8);
}
#[test]
fn test_touch_finger_default() {
let finger = TouchFinger::default();
assert_eq!(finger.id, 0);
assert_relative_eq!(finger.x, 0.0);
assert_relative_eq!(finger.y, 0.0);
assert!(!finger.active);
}
#[test]
fn test_touchpad_data_default() {
let data = TouchpadData::default();
assert!(!data.finger1.active);
assert!(!data.finger2.active);
assert!(!data.button_pressed);
assert_eq!(data.prev_finger1, Vec2::ZERO);
assert_eq!(data.prev_finger2, Vec2::ZERO);
}
#[test]
fn test_touchpad_data_finger1_delta_inactive() {
let data = TouchpadData::default();
assert_eq!(data.finger1_delta(), Vec2::ZERO);
}
#[test]
fn test_touchpad_data_finger1_delta_active() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.5, 0.5);
data.prev_finger1 = Vec2::new(0.3, 0.3);
let delta = data.finger1_delta();
assert_relative_eq!(delta.x, 0.2, epsilon = 0.001);
assert_relative_eq!(delta.y, 0.2, epsilon = 0.001);
}
#[test]
fn test_touchpad_data_finger2_delta_inactive() {
let data = TouchpadData::default();
assert_eq!(data.finger2_delta(), Vec2::ZERO);
}
#[test]
fn test_touchpad_data_finger2_delta_active() {
let mut data = TouchpadData::default();
data.finger2 = TouchFinger::new(1, 0.8, 0.6);
data.prev_finger2 = Vec2::new(0.4, 0.2);
let delta = data.finger2_delta();
assert_relative_eq!(delta.x, 0.4, epsilon = 0.001);
assert_relative_eq!(delta.y, 0.4, epsilon = 0.001);
}
#[test]
fn test_touchpad_data_is_swiping_true() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.8, 0.5);
data.prev_finger1 = Vec2::new(0.3, 0.5);
assert!(data.is_swiping(0.1));
}
#[test]
fn test_touchpad_data_is_swiping_false() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.35, 0.5);
data.prev_finger1 = Vec2::new(0.3, 0.5);
assert!(!data.is_swiping(0.1));
}
#[test]
fn test_touchpad_data_is_pinching_none_when_single_finger() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.5, 0.5);
assert!(data.is_pinching().is_none());
}
#[test]
fn test_touchpad_data_is_pinching_out() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.2, 0.5);
data.finger2 = TouchFinger::new(1, 0.8, 0.5);
data.prev_finger1 = Vec2::new(0.3, 0.5);
data.prev_finger2 = Vec2::new(0.7, 0.5);
let pinch = data.is_pinching();
assert!(pinch.is_some());
assert!(pinch.unwrap() > 0.0); }
#[test]
fn test_touchpad_data_is_pinching_in() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.4, 0.5);
data.finger2 = TouchFinger::new(1, 0.6, 0.5);
data.prev_finger1 = Vec2::new(0.2, 0.5);
data.prev_finger2 = Vec2::new(0.8, 0.5);
let pinch = data.is_pinching();
assert!(pinch.is_some());
assert!(pinch.unwrap() < 0.0); }
#[test]
fn test_touchpad_data_is_pinching_none_when_no_movement() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.3, 0.5);
data.finger2 = TouchFinger::new(1, 0.7, 0.5);
data.prev_finger1 = Vec2::new(0.3, 0.5);
data.prev_finger2 = Vec2::new(0.7, 0.5);
assert!(data.is_pinching().is_none());
}
#[test]
fn test_touchpad_data_active_fingers_none() {
let data = TouchpadData::default();
assert_eq!(data.active_fingers(), 0);
}
#[test]
fn test_touchpad_data_active_fingers_one() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.5, 0.5);
assert_eq!(data.active_fingers(), 1);
}
#[test]
fn test_touchpad_data_active_fingers_two() {
let mut data = TouchpadData::default();
data.finger1 = TouchFinger::new(0, 0.5, 0.5);
data.finger2 = TouchFinger::new(1, 0.7, 0.7);
assert_eq!(data.active_fingers(), 2);
}
#[test]
fn test_touchpad_gesture_equality() {
assert_eq!(TouchpadGesture::Tap, TouchpadGesture::Tap);
assert_ne!(TouchpadGesture::Tap, TouchpadGesture::TwoFingerTap);
}
#[test]
fn test_touchpad_gesture_variants() {
let gestures = [
TouchpadGesture::Tap,
TouchpadGesture::TwoFingerTap,
TouchpadGesture::SwipeLeft,
TouchpadGesture::SwipeRight,
TouchpadGesture::SwipeUp,
TouchpadGesture::SwipeDown,
TouchpadGesture::PinchIn,
TouchpadGesture::PinchOut,
];
assert_eq!(gestures.len(), 8);
}
#[test]
fn test_touchpad_config_default() {
let config = TouchpadConfig::default();
assert_relative_eq!(config.swipe_threshold, 0.15);
assert_relative_eq!(config.tap_time_window, 0.2);
assert!(config.enabled);
}
#[test]
fn test_touchpad_config_custom() {
let config = TouchpadConfig {
swipe_threshold: 0.25,
tap_time_window: 0.3,
enabled: false,
};
assert_relative_eq!(config.swipe_threshold, 0.25);
assert_relative_eq!(config.tap_time_window, 0.3);
assert!(!config.enabled);
}
#[test]
fn test_touchpad_gesture_event_creation() {
let event = TouchpadGestureEvent {
gamepad: Entity::PLACEHOLDER,
gesture: TouchpadGesture::SwipeRight,
position: Vec2::new(0.5, 0.5),
intensity: 0.3,
};
assert_eq!(event.gesture, TouchpadGesture::SwipeRight);
assert_relative_eq!(event.intensity, 0.3);
}
}