Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced pre-existing formatting drift across the agent and server crates. Apply rustfmt across the workspace so the codebase meets its own CI gate. Pure formatting; no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
221 lines
6.5 KiB
Rust
221 lines
6.5 KiB
Rust
//! Mouse input simulation using Windows SendInput API
|
|
|
|
use super::MouseButton;
|
|
use anyhow::Result;
|
|
|
|
#[cfg(windows)]
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_HWHEEL,
|
|
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP,
|
|
MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK,
|
|
MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT,
|
|
};
|
|
|
|
// X button constants (not exported in windows crate 0.58+)
|
|
#[cfg(windows)]
|
|
const XBUTTON1: u32 = 0x0001;
|
|
#[cfg(windows)]
|
|
const XBUTTON2: u32 = 0x0002;
|
|
|
|
#[cfg(windows)]
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
|
|
};
|
|
|
|
/// Mouse input controller
|
|
pub struct MouseController {
|
|
/// Virtual screen dimensions for coordinate translation
|
|
#[cfg(windows)]
|
|
virtual_screen: VirtualScreen,
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
struct VirtualScreen {
|
|
x: i32,
|
|
y: i32,
|
|
width: i32,
|
|
height: i32,
|
|
}
|
|
|
|
impl MouseController {
|
|
/// Create a new mouse controller
|
|
pub fn new() -> Result<Self> {
|
|
#[cfg(windows)]
|
|
{
|
|
let virtual_screen = unsafe {
|
|
VirtualScreen {
|
|
x: GetSystemMetrics(SM_XVIRTUALSCREEN),
|
|
y: GetSystemMetrics(SM_YVIRTUALSCREEN),
|
|
width: GetSystemMetrics(SM_CXVIRTUALSCREEN),
|
|
height: GetSystemMetrics(SM_CYVIRTUALSCREEN),
|
|
}
|
|
};
|
|
|
|
Ok(Self { virtual_screen })
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
anyhow::bail!("Mouse input only supported on Windows")
|
|
}
|
|
}
|
|
|
|
/// Move mouse to absolute screen coordinates
|
|
#[cfg(windows)]
|
|
pub fn move_to(&mut self, x: i32, y: i32) -> Result<()> {
|
|
// Convert screen coordinates to normalized absolute coordinates (0-65535)
|
|
let norm_x = ((x - self.virtual_screen.x) * 65535) / self.virtual_screen.width;
|
|
let norm_y = ((y - self.virtual_screen.y) * 65535) / self.virtual_screen.height;
|
|
|
|
let input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: norm_x,
|
|
dy: norm_y,
|
|
mouseData: 0,
|
|
dwFlags: MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
self.send_input(&[input])
|
|
}
|
|
|
|
/// Press mouse button down
|
|
#[cfg(windows)]
|
|
pub fn button_down(&mut self, button: MouseButton) -> Result<()> {
|
|
let (flags, data) = match button {
|
|
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, 0),
|
|
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, 0),
|
|
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, 0),
|
|
MouseButton::X1 => (MOUSEEVENTF_XDOWN, XBUTTON1),
|
|
MouseButton::X2 => (MOUSEEVENTF_XDOWN, XBUTTON2),
|
|
};
|
|
|
|
let input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: 0,
|
|
dy: 0,
|
|
mouseData: data,
|
|
dwFlags: flags,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
self.send_input(&[input])
|
|
}
|
|
|
|
/// Release mouse button
|
|
#[cfg(windows)]
|
|
pub fn button_up(&mut self, button: MouseButton) -> Result<()> {
|
|
let (flags, data) = match button {
|
|
MouseButton::Left => (MOUSEEVENTF_LEFTUP, 0),
|
|
MouseButton::Right => (MOUSEEVENTF_RIGHTUP, 0),
|
|
MouseButton::Middle => (MOUSEEVENTF_MIDDLEUP, 0),
|
|
MouseButton::X1 => (MOUSEEVENTF_XUP, XBUTTON1),
|
|
MouseButton::X2 => (MOUSEEVENTF_XUP, XBUTTON2),
|
|
};
|
|
|
|
let input = INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: 0,
|
|
dy: 0,
|
|
mouseData: data,
|
|
dwFlags: flags,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
};
|
|
|
|
self.send_input(&[input])
|
|
}
|
|
|
|
/// Scroll mouse wheel
|
|
#[cfg(windows)]
|
|
pub fn scroll(&mut self, delta_x: i32, delta_y: i32) -> Result<()> {
|
|
let mut inputs = Vec::new();
|
|
|
|
// Vertical scroll
|
|
if delta_y != 0 {
|
|
inputs.push(INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: 0,
|
|
dy: 0,
|
|
mouseData: delta_y as u32,
|
|
dwFlags: MOUSEEVENTF_WHEEL,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Horizontal scroll
|
|
if delta_x != 0 {
|
|
inputs.push(INPUT {
|
|
r#type: INPUT_MOUSE,
|
|
Anonymous: INPUT_0 {
|
|
mi: MOUSEINPUT {
|
|
dx: 0,
|
|
dy: 0,
|
|
mouseData: delta_x as u32,
|
|
dwFlags: MOUSEEVENTF_HWHEEL,
|
|
time: 0,
|
|
dwExtraInfo: 0,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
if !inputs.is_empty() {
|
|
self.send_input(&inputs)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Send input events
|
|
#[cfg(windows)]
|
|
fn send_input(&self, inputs: &[INPUT]) -> Result<()> {
|
|
let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
|
|
|
|
if sent as usize != inputs.len() {
|
|
anyhow::bail!("SendInput failed: sent {} of {} inputs", sent, inputs.len());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn move_to(&mut self, _x: i32, _y: i32) -> Result<()> {
|
|
anyhow::bail!("Mouse input only supported on Windows")
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn button_down(&mut self, _button: MouseButton) -> Result<()> {
|
|
anyhow::bail!("Mouse input only supported on Windows")
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn button_up(&mut self, _button: MouseButton) -> Result<()> {
|
|
anyhow::bail!("Mouse input only supported on Windows")
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
pub fn scroll(&mut self, _delta_x: i32, _delta_y: i32) -> Result<()> {
|
|
anyhow::bail!("Mouse input only supported on Windows")
|
|
}
|
|
}
|