forgewright 0.2.0

Standalone UI automation — CDP for browsers, UIA for Windows desktop apps
Documentation
//! Input state reader — mouse position + gamepad state for Visual Gate reports.
//!
//! Uses Win32 GetCursorPos / GetSystemMetrics for mouse,
//! XInput for gamepad with graceful degradation (gamepad = None if unavailable).

use serde::Serialize;

/// Current input device state.
#[derive(Debug, Clone, Serialize)]
pub struct InputState {
    pub mouse: MouseState,
    pub gamepad: Option<GamepadState>,
}

/// Mouse position and screen dimensions.
#[derive(Debug, Clone, Serialize)]
pub struct MouseState {
    pub x: i32,
    pub y: i32,
    pub screen_width: i32,
    pub screen_height: i32,
}

/// Gamepad state with normalized axes and triggers.
#[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,
}

// ── Normalization helpers (public for property testing) ──────────────────────

/// Normalize a raw XInput stick axis value (i16, range [-32768, 32767])
/// to [-1.0, 1.0].
pub fn normalize_stick(raw: i16) -> f32 {
    let v = raw as f32 / 32767.0;
    v.clamp(-1.0, 1.0)
}

/// Normalize a raw XInput trigger value (u8, range [0, 255])
/// to [0.0, 1.0].
pub fn normalize_trigger(raw: u8) -> f32 {
    let v = raw as f32 / 255.0;
    v.clamp(0.0, 1.0)
}


// ── Platform implementation ──────────────────────────────────────────────────

#[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,
    };

    /// Read current input state from Windows APIs.
    ///
    /// - Mouse: GetCursorPos + GetSystemMetrics
    /// - Gamepad: XInputGetState for controller 0
    /// - Graceful degradation: gamepad = None if XInput fails or no controller
    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();
        // Try controller index 0
        let result = unsafe { XInputGetState(0, &mut state) };
        if result != 0 {
            // ERROR_DEVICE_NOT_CONNECTED (1167) or other failure
            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::*;

    /// Stub for non-Windows: returns zeroed mouse and no gamepad.
    pub fn read_input_state_impl() -> InputState {
        InputState {
            mouse: MouseState {
                x: 0,
                y: 0,
                screen_width: 0,
                screen_height: 0,
            },
            gamepad: None,
        }
    }
}

/// Read current input state from the OS.
///
/// On Windows: queries GetCursorPos, GetSystemMetrics, and XInputGetState.
/// On other platforms: returns zeroed state (ForgeWright is Windows-native).
pub fn read_input_state() -> InputState {
    platform::read_input_state_impl()
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[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();
        // Mouse coordinates can be anything, just verify struct is populated
        assert!(state.mouse.screen_width >= 0);
        assert!(state.mouse.screen_height >= 0);
        // Gamepad may or may not be connected — both are valid
    }

    // ── Property 15: Gamepad normalization bounds ────────────────────────────
    // **Validates: Requirement 6.4**
    //
    // For any raw XInput stick value (i16), verify normalized output in [-1.0, 1.0].
    // For any raw trigger value (u8), verify normalized output in [0.0, 1.0].

    mod prop {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            /// **Validates: Requirements 6.4**
            #[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
                );
            }

            /// **Validates: Requirements 6.4**
            #[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
                );
            }
        }
    }
}