use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct InputState {
pub mouse: MouseState,
pub gamepad: Option<GamepadState>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MouseState {
pub x: i32,
pub y: i32,
pub screen_width: i32,
pub screen_height: i32,
}
#[derive(Debug, Clone, Serialize)]
pub struct GamepadState {
pub connected: bool,
pub left_stick: (f32, f32),
pub right_stick: (f32, f32),
pub left_trigger: f32,
pub right_trigger: f32,
pub buttons: u16,
}
pub fn normalize_stick(raw: i16) -> f32 {
let v = raw as f32 / 32767.0;
v.clamp(-1.0, 1.0)
}
pub fn normalize_trigger(raw: u8) -> f32 {
let v = raw as f32 / 255.0;
v.clamp(0.0, 1.0)
}
#[cfg(target_os = "windows")]
mod platform {
use super::*;
use windows::Win32::Foundation::POINT;
use windows::Win32::UI::Input::XboxController::{
XInputGetState, XINPUT_STATE,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetCursorPos, GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN,
};
pub fn read_input_state_impl() -> InputState {
let mouse = read_mouse();
let gamepad = read_gamepad();
InputState { mouse, gamepad }
}
fn read_mouse() -> MouseState {
let mut pt = POINT { x: 0, y: 0 };
unsafe {
let _ = GetCursorPos(&mut pt);
}
let screen_width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
let screen_height = unsafe { GetSystemMetrics(SM_CYSCREEN) };
MouseState {
x: pt.x,
y: pt.y,
screen_width,
screen_height,
}
}
fn read_gamepad() -> Option<GamepadState> {
let mut state = XINPUT_STATE::default();
let result = unsafe { XInputGetState(0, &mut state) };
if result != 0 {
return None;
}
let gp = state.Gamepad;
Some(GamepadState {
connected: true,
left_stick: (
normalize_stick(gp.sThumbLX),
normalize_stick(gp.sThumbLY),
),
right_stick: (
normalize_stick(gp.sThumbRX),
normalize_stick(gp.sThumbRY),
),
left_trigger: normalize_trigger(gp.bLeftTrigger),
right_trigger: normalize_trigger(gp.bRightTrigger),
buttons: gp.wButtons.0,
})
}
}
#[cfg(not(target_os = "windows"))]
mod platform {
use super::*;
pub fn read_input_state_impl() -> InputState {
InputState {
mouse: MouseState {
x: 0,
y: 0,
screen_width: 0,
screen_height: 0,
},
gamepad: None,
}
}
}
pub fn read_input_state() -> InputState {
platform::read_input_state_impl()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_stick_zero() {
assert_eq!(normalize_stick(0), 0.0);
}
#[test]
fn normalize_stick_max() {
let v = normalize_stick(i16::MAX);
assert!((v - 1.0).abs() < 1e-5);
}
#[test]
fn normalize_stick_min() {
let v = normalize_stick(i16::MIN);
assert!(v >= -1.0);
assert!(v <= -1.0 + 1e-5);
}
#[test]
fn normalize_trigger_zero() {
assert_eq!(normalize_trigger(0), 0.0);
}
#[test]
fn normalize_trigger_max() {
let v = normalize_trigger(255);
assert!((v - 1.0).abs() < 1e-5);
}
#[test]
fn read_input_state_returns_valid() {
let state = read_input_state();
assert!(state.mouse.screen_width >= 0);
assert!(state.mouse.screen_height >= 0);
}
mod prop {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn stick_normalization_bounds(raw in proptest::num::i16::ANY) {
let normalized = normalize_stick(raw);
prop_assert!(
normalized >= -1.0 && normalized <= 1.0,
"normalize_stick({}) = {} is out of [-1.0, 1.0]",
raw, normalized
);
}
#[test]
fn trigger_normalization_bounds(raw in proptest::num::u8::ANY) {
let normalized = normalize_trigger(raw);
prop_assert!(
normalized >= 0.0 && normalized <= 1.0,
"normalize_trigger({}) = {} is out of [0.0, 1.0]",
raw, normalized
);
}
}
}
}