use crate::buttonlike::{MouseMotionDirection, MouseWheelDirection};
use crate::orientation::{Direction, Rotation};
use crate::user_input::InputKind;
use bevy::input::{
gamepad::{GamepadAxisType, GamepadButtonType},
keyboard::KeyCode,
};
use bevy::math::Vec2;
use bevy::reflect::Reflect;
use bevy::utils::FloatOrd;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
pub struct SingleAxis {
pub axis_type: AxisType,
pub positive_low: f32,
pub negative_low: f32,
pub inverted: bool,
pub sensitivity: f32,
pub value: Option<f32>,
}
impl SingleAxis {
#[must_use]
pub const fn with_threshold(
axis_type: AxisType,
negative_low: f32,
positive_low: f32,
) -> SingleAxis {
SingleAxis {
axis_type,
positive_low,
negative_low,
inverted: false,
sensitivity: 1.0,
value: None,
}
}
#[must_use]
pub fn symmetric(axis_type: impl Into<AxisType>, threshold: f32) -> SingleAxis {
Self::with_threshold(axis_type.into(), -threshold, threshold)
}
#[must_use]
pub fn from_value(axis_type: impl Into<AxisType>, value: f32) -> SingleAxis {
SingleAxis {
axis_type: axis_type.into(),
positive_low: 0.0,
negative_low: 0.0,
inverted: false,
sensitivity: 1.0,
value: Some(value),
}
}
#[must_use]
pub const fn mouse_wheel_x() -> SingleAxis {
let axis_type = AxisType::MouseWheel(MouseWheelAxisType::X);
SingleAxis::with_threshold(axis_type, 0.0, 0.0)
}
#[must_use]
pub const fn mouse_wheel_y() -> SingleAxis {
let axis_type = AxisType::MouseWheel(MouseWheelAxisType::Y);
SingleAxis::with_threshold(axis_type, 0.0, 0.0)
}
#[must_use]
pub const fn mouse_motion_x() -> SingleAxis {
let axis_type = AxisType::MouseMotion(MouseMotionAxisType::X);
SingleAxis::with_threshold(axis_type, 0.0, 0.0)
}
#[must_use]
pub const fn mouse_motion_y() -> SingleAxis {
let axis_type = AxisType::MouseMotion(MouseMotionAxisType::Y);
SingleAxis::with_threshold(axis_type, 0.0, 0.0)
}
pub fn negative_only(axis_type: impl Into<AxisType>, threshold: f32) -> SingleAxis {
SingleAxis::with_threshold(axis_type.into(), threshold, f32::MAX)
}
pub fn positive_only(axis_type: impl Into<AxisType>, threshold: f32) -> SingleAxis {
SingleAxis::with_threshold(axis_type.into(), f32::MIN, threshold)
}
#[must_use]
pub fn with_deadzone(mut self, deadzone: f32) -> SingleAxis {
self.negative_low = -deadzone;
self.positive_low = deadzone;
self
}
#[must_use]
pub fn with_sensitivity(mut self, sensitivity: f32) -> SingleAxis {
self.sensitivity = sensitivity;
self
}
#[must_use]
pub fn inverted(mut self) -> Self {
self.inverted = !self.inverted;
self
}
}
impl PartialEq for SingleAxis {
fn eq(&self, other: &Self) -> bool {
self.axis_type == other.axis_type
&& FloatOrd(self.positive_low) == FloatOrd(other.positive_low)
&& FloatOrd(self.negative_low) == FloatOrd(other.negative_low)
&& FloatOrd(self.sensitivity) == FloatOrd(other.sensitivity)
}
}
impl Eq for SingleAxis {}
impl std::hash::Hash for SingleAxis {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.axis_type.hash(state);
FloatOrd(self.positive_low).hash(state);
FloatOrd(self.negative_low).hash(state);
FloatOrd(self.sensitivity).hash(state);
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Reflect)]
pub struct DualAxis {
pub x: SingleAxis,
pub y: SingleAxis,
pub deadzone: DeadZoneShape,
}
impl DualAxis {
pub const DEFAULT_DEADZONE: f32 = 0.1;
pub const DEFAULT_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Ellipse {
radius_x: Self::DEFAULT_DEADZONE,
radius_y: Self::DEFAULT_DEADZONE,
};
pub const ZERO_DEADZONE_SHAPE: DeadZoneShape = DeadZoneShape::Ellipse {
radius_x: 0.0,
radius_y: 0.0,
};
#[must_use]
pub fn symmetric(
x_axis_type: impl Into<AxisType>,
y_axis_type: impl Into<AxisType>,
deadzone_shape: DeadZoneShape,
) -> DualAxis {
DualAxis {
x: SingleAxis::symmetric(x_axis_type, 0.0),
y: SingleAxis::symmetric(y_axis_type, 0.0),
deadzone: deadzone_shape,
}
}
#[must_use]
pub fn from_value(
x_axis_type: impl Into<AxisType>,
y_axis_type: impl Into<AxisType>,
x_value: f32,
y_value: f32,
) -> DualAxis {
DualAxis {
x: SingleAxis::from_value(x_axis_type, x_value),
y: SingleAxis::from_value(y_axis_type, y_value),
deadzone: Self::DEFAULT_DEADZONE_SHAPE,
}
}
#[must_use]
pub const fn left_stick() -> DualAxis {
let axis_x = AxisType::Gamepad(GamepadAxisType::LeftStickX);
let axis_y = AxisType::Gamepad(GamepadAxisType::LeftStickY);
DualAxis {
x: SingleAxis::with_threshold(axis_x, 0.0, 0.0),
y: SingleAxis::with_threshold(axis_y, 0.0, 0.0),
deadzone: Self::DEFAULT_DEADZONE_SHAPE,
}
}
#[must_use]
pub const fn right_stick() -> DualAxis {
let axis_x = AxisType::Gamepad(GamepadAxisType::RightStickX);
let axis_y = AxisType::Gamepad(GamepadAxisType::RightStickY);
DualAxis {
x: SingleAxis::with_threshold(axis_x, 0.0, 0.0),
y: SingleAxis::with_threshold(axis_y, 0.0, 0.0),
deadzone: Self::DEFAULT_DEADZONE_SHAPE,
}
}
pub const fn mouse_wheel() -> DualAxis {
DualAxis {
x: SingleAxis::mouse_wheel_x(),
y: SingleAxis::mouse_wheel_y(),
deadzone: Self::ZERO_DEADZONE_SHAPE,
}
}
pub const fn mouse_motion() -> DualAxis {
DualAxis {
x: SingleAxis::mouse_motion_x(),
y: SingleAxis::mouse_motion_y(),
deadzone: Self::ZERO_DEADZONE_SHAPE,
}
}
#[must_use]
pub fn with_deadzone(mut self, deadzone: DeadZoneShape) -> DualAxis {
self.deadzone = deadzone;
self
}
#[must_use]
pub fn with_sensitivity(mut self, x_sensitivity: f32, y_sensitivity: f32) -> DualAxis {
self.x.sensitivity = x_sensitivity;
self.y.sensitivity = y_sensitivity;
self
}
#[must_use]
pub fn inverted_x(mut self) -> DualAxis {
self.x = self.x.inverted();
self
}
#[must_use]
pub fn inverted_y(mut self) -> DualAxis {
self.y = self.y.inverted();
self
}
#[must_use]
pub fn inverted(mut self) -> DualAxis {
self.x = self.x.inverted();
self.y = self.y.inverted();
self
}
}
#[allow(clippy::doc_markdown)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub struct VirtualDPad {
pub up: InputKind,
pub down: InputKind,
pub left: InputKind,
pub right: InputKind,
}
impl VirtualDPad {
pub const fn arrow_keys() -> VirtualDPad {
VirtualDPad {
up: InputKind::PhysicalKey(KeyCode::ArrowUp),
down: InputKind::PhysicalKey(KeyCode::ArrowDown),
left: InputKind::PhysicalKey(KeyCode::ArrowLeft),
right: InputKind::PhysicalKey(KeyCode::ArrowRight),
}
}
pub const fn wasd() -> VirtualDPad {
VirtualDPad {
up: InputKind::PhysicalKey(KeyCode::KeyW),
down: InputKind::PhysicalKey(KeyCode::KeyS),
left: InputKind::PhysicalKey(KeyCode::KeyA),
right: InputKind::PhysicalKey(KeyCode::KeyD),
}
}
#[allow(clippy::doc_markdown)] pub const fn dpad() -> VirtualDPad {
VirtualDPad {
up: InputKind::GamepadButton(GamepadButtonType::DPadUp),
down: InputKind::GamepadButton(GamepadButtonType::DPadDown),
left: InputKind::GamepadButton(GamepadButtonType::DPadLeft),
right: InputKind::GamepadButton(GamepadButtonType::DPadRight),
}
}
pub const fn gamepad_face_buttons() -> VirtualDPad {
VirtualDPad {
up: InputKind::GamepadButton(GamepadButtonType::North),
down: InputKind::GamepadButton(GamepadButtonType::South),
left: InputKind::GamepadButton(GamepadButtonType::West),
right: InputKind::GamepadButton(GamepadButtonType::East),
}
}
pub const fn mouse_wheel() -> VirtualDPad {
VirtualDPad {
up: InputKind::MouseWheel(MouseWheelDirection::Up),
down: InputKind::MouseWheel(MouseWheelDirection::Down),
left: InputKind::MouseWheel(MouseWheelDirection::Left),
right: InputKind::MouseWheel(MouseWheelDirection::Right),
}
}
pub const fn mouse_motion() -> VirtualDPad {
VirtualDPad {
up: InputKind::MouseMotion(MouseMotionDirection::Up),
down: InputKind::MouseMotion(MouseMotionDirection::Down),
left: InputKind::MouseMotion(MouseMotionDirection::Left),
right: InputKind::MouseMotion(MouseMotionDirection::Right),
}
}
pub fn inverted_y(mut self) -> Self {
std::mem::swap(&mut self.up, &mut self.down);
self
}
pub fn inverted_x(mut self) -> Self {
std::mem::swap(&mut self.left, &mut self.right);
self
}
pub fn inverted(mut self) -> Self {
std::mem::swap(&mut self.up, &mut self.down);
std::mem::swap(&mut self.left, &mut self.right);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub struct VirtualAxis {
pub negative: InputKind,
pub positive: InputKind,
}
impl VirtualAxis {
pub const fn from_keys(negative: KeyCode, positive: KeyCode) -> VirtualAxis {
VirtualAxis {
negative: InputKind::PhysicalKey(negative),
positive: InputKind::PhysicalKey(positive),
}
}
pub const fn horizontal_arrow_keys() -> VirtualAxis {
VirtualAxis::from_keys(KeyCode::ArrowLeft, KeyCode::ArrowRight)
}
pub const fn vertical_arrow_keys() -> VirtualAxis {
VirtualAxis::from_keys(KeyCode::ArrowDown, KeyCode::ArrowUp)
}
pub const fn ad() -> VirtualAxis {
VirtualAxis::from_keys(KeyCode::KeyA, KeyCode::KeyD)
}
pub const fn ws() -> VirtualAxis {
VirtualAxis::from_keys(KeyCode::KeyS, KeyCode::KeyW)
}
#[allow(clippy::doc_markdown)]
pub const fn horizontal_dpad() -> VirtualAxis {
VirtualAxis {
negative: InputKind::GamepadButton(GamepadButtonType::DPadLeft),
positive: InputKind::GamepadButton(GamepadButtonType::DPadRight),
}
}
#[allow(clippy::doc_markdown)]
pub const fn vertical_dpad() -> VirtualAxis {
VirtualAxis {
negative: InputKind::GamepadButton(GamepadButtonType::DPadDown),
positive: InputKind::GamepadButton(GamepadButtonType::DPadUp),
}
}
#[allow(clippy::doc_markdown)]
pub const fn horizontal_gamepad_face_buttons() -> VirtualAxis {
VirtualAxis {
negative: InputKind::GamepadButton(GamepadButtonType::West),
positive: InputKind::GamepadButton(GamepadButtonType::East),
}
}
#[allow(clippy::doc_markdown)]
pub const fn vertical_gamepad_face_buttons() -> VirtualAxis {
VirtualAxis {
negative: InputKind::GamepadButton(GamepadButtonType::South),
positive: InputKind::GamepadButton(GamepadButtonType::North),
}
}
#[must_use]
pub fn inverted(mut self) -> Self {
std::mem::swap(&mut self.positive, &mut self.negative);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub enum AxisType {
Gamepad(GamepadAxisType),
MouseWheel(MouseWheelAxisType),
MouseMotion(MouseMotionAxisType),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub enum MouseWheelAxisType {
X,
Y,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub enum MouseMotionAxisType {
X,
Y,
}
impl From<GamepadAxisType> for AxisType {
fn from(axis_type: GamepadAxisType) -> Self {
AxisType::Gamepad(axis_type)
}
}
impl From<MouseWheelAxisType> for AxisType {
fn from(axis_type: MouseWheelAxisType) -> Self {
AxisType::MouseWheel(axis_type)
}
}
impl From<MouseMotionAxisType> for AxisType {
fn from(axis_type: MouseMotionAxisType) -> Self {
AxisType::MouseMotion(axis_type)
}
}
impl TryFrom<AxisType> for GamepadAxisType {
type Error = AxisConversionError;
fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
match axis_type {
AxisType::Gamepad(inner) => Ok(inner),
_ => Err(AxisConversionError),
}
}
}
impl TryFrom<AxisType> for MouseWheelAxisType {
type Error = AxisConversionError;
fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
match axis_type {
AxisType::MouseWheel(inner) => Ok(inner),
_ => Err(AxisConversionError),
}
}
}
impl TryFrom<AxisType> for MouseMotionAxisType {
type Error = AxisConversionError;
fn try_from(axis_type: AxisType) -> Result<Self, AxisConversionError> {
match axis_type {
AxisType::MouseMotion(inner) => Ok(inner),
_ => Err(AxisConversionError),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AxisConversionError;
#[derive(Debug, Copy, Clone, PartialEq, Default, Deserialize, Serialize, Reflect)]
pub struct DualAxisData {
xy: Vec2,
}
impl DualAxisData {
pub fn new(x: f32, y: f32) -> DualAxisData {
DualAxisData {
xy: Vec2::new(x, y),
}
}
pub fn from_xy(xy: Vec2) -> DualAxisData {
DualAxisData { xy }
}
pub fn merged_with(&self, other: DualAxisData) -> DualAxisData {
DualAxisData::from_xy(self.xy() + other.xy())
}
}
impl DualAxisData {
#[must_use]
#[inline]
pub fn x(&self) -> f32 {
self.xy.x
}
#[must_use]
#[inline]
pub fn y(&self) -> f32 {
self.xy.y
}
#[must_use]
#[inline]
pub fn xy(&self) -> Vec2 {
self.xy
}
#[must_use]
#[inline]
pub fn direction(&self) -> Option<Direction> {
(self.xy.length() > 0.00001).then(|| Direction::new(self.xy))
}
#[must_use]
#[inline]
pub fn rotation(&self) -> Option<Rotation> {
Rotation::from_xy(self.xy).ok()
}
#[must_use]
#[inline]
pub fn length(&self) -> f32 {
self.xy.length()
}
#[must_use]
#[inline]
pub fn length_squared(&self) -> f32 {
self.xy.length_squared()
}
pub fn clamp_length(&mut self, max: f32) {
self.xy = self.xy.clamp_length_max(max);
}
}
impl From<DualAxisData> for Vec2 {
fn from(data: DualAxisData) -> Vec2 {
data.xy
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Reflect)]
pub enum DeadZoneShape {
Cross {
horizontal_width: f32,
vertical_width: f32,
},
Ellipse {
radius_x: f32,
radius_y: f32,
},
}
impl Eq for DeadZoneShape {}
impl std::hash::Hash for DeadZoneShape {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
DeadZoneShape::Cross {
horizontal_width,
vertical_width,
} => {
FloatOrd(*horizontal_width).hash(state);
FloatOrd(*vertical_width).hash(state);
}
DeadZoneShape::Ellipse { radius_x, radius_y } => {
FloatOrd(*radius_x).hash(state);
FloatOrd(*radius_y).hash(state);
}
}
}
}
impl DeadZoneShape {
pub fn deadzone_input_value(&self, x: f32, y: f32) -> Option<DualAxisData> {
match self {
DeadZoneShape::Cross {
horizontal_width,
vertical_width,
} => self.cross_deadzone_value(x, y, *horizontal_width, *vertical_width),
DeadZoneShape::Ellipse { radius_x, radius_y } => {
self.ellipse_deadzone_value(x, y, *radius_x, *radius_y)
}
}
}
fn cross_deadzone_value(
&self,
x: f32,
y: f32,
horizontal_width: f32,
vertical_width: f32,
) -> Option<DualAxisData> {
let new_x = deadzone_axis_value(x, vertical_width);
let new_y = deadzone_axis_value(y, horizontal_width);
let is_outside_deadzone = new_x != 0.0 || new_y != 0.0;
is_outside_deadzone.then(|| DualAxisData::new(new_x, new_y))
}
fn ellipse_deadzone_value(
&self,
x: f32,
y: f32,
radius_x: f32,
radius_y: f32,
) -> Option<DualAxisData> {
let x_ratio = x / radius_x.max(f32::EPSILON);
let y_ratio = y / radius_y.max(f32::EPSILON);
let is_outside_deadzone = x_ratio.powi(2) + y_ratio.powi(2) >= 1.0;
is_outside_deadzone.then(|| {
let new_x = deadzone_axis_value(x, radius_x);
let new_y = deadzone_axis_value(y, radius_y);
DualAxisData::new(new_x, new_y)
})
}
}
pub(crate) fn deadzone_axis_value(axis_value: f32, deadzone: f32) -> f32 {
let abs_axis_value = axis_value.abs();
if abs_axis_value <= deadzone {
0.0
} else {
axis_value.signum() * (abs_axis_value - deadzone) / (1.0 - deadzone)
}
}