Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
//! Mouse input handling for Windows.
|
||||
//!
|
||||
//! Absolute positioning is done via `SetCursorPos` (physical pixel coordinates on DPI-aware
|
||||
//! processes; logical coordinates otherwise), which avoids the normalized-coordinate math
|
||||
//! required by `SendInput` with `MOUSEEVENTF_ABSOLUTE`. Button presses, releases, and wheel
|
||||
//! scrolls go through `SendInput`.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use pathfinder_geometry::vector::Vector2I;
|
||||
use windows::Win32::Foundation::{GetLastError, POINT};
|
||||
use windows::Win32::Graphics::Gdi::{MONITOR_DEFAULTTONEAREST, MonitorFromPoint};
|
||||
use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, 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, SendInput,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetCursorPos, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
|
||||
SM_YVIRTUALSCREEN, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
|
||||
SYSTEM_PARAMETERS_INFO_ACTION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SetCursorPos,
|
||||
SystemParametersInfoW,
|
||||
};
|
||||
|
||||
use super::dpi::DpiAwarenessGuard;
|
||||
use crate::{MouseButton, ScrollDirection, ScrollDistance};
|
||||
|
||||
/// One wheel "click" in `MOUSEEVENTF_WHEEL`/`MOUSEEVENTF_HWHEEL` units.
|
||||
/// See the Win32 `WHEEL_DELTA` constant.
|
||||
const WHEEL_DELTA: i32 = 120;
|
||||
|
||||
/// `XBUTTON1` / `XBUTTON2` values for `mouseData` when sending X-button events. These match the
|
||||
/// Win32 header values and are not currently exposed through the `windows` crate's
|
||||
/// `KeyboardAndMouse` module.
|
||||
const XBUTTON1: u32 = 0x0001;
|
||||
const XBUTTON2: u32 = 0x0002;
|
||||
|
||||
/// Nominal line height (in logical pixels at 100% scale) used as the baseline when translating
|
||||
/// the user's `SPI_GETWHEELSCROLLLINES` setting into a pixel-per-click factor. The actual line
|
||||
/// height we use is this value scaled by the cursor-monitor DPI over `USER_DEFAULT_SCREEN_DPI`,
|
||||
/// so `ScrollDistance::Pixels` stays proportional to what the user sees on HiDPI displays (~20px
|
||||
/// at 125% scale, ~24px at 150%) — including secondary monitors in mixed-DPI setups.
|
||||
const NOMINAL_LINE_HEIGHT_PX: i32 = 16;
|
||||
|
||||
/// The "default" (1x) DPI value Windows reports; matches `USER_DEFAULT_SCREEN_DPI`.
|
||||
const DEFAULT_DPI: u32 = 96;
|
||||
|
||||
/// Fallback used if `SPI_GETWHEELSCROLLLINES` is unavailable or returns a sentinel value (e.g.,
|
||||
/// `WHEEL_PAGESCROLL`). Matches the documented Windows default of three lines per wheel click.
|
||||
const DEFAULT_WHEEL_SCROLL_LINES: u32 = 3;
|
||||
|
||||
/// Upper bound applied to the user's `SPI_GETWHEELSCROLL{LINES,CHARS}` setting before it's
|
||||
/// multiplied by `NOMINAL_LINE_HEIGHT_PX`. Without this clamp an unusually large configured value
|
||||
/// (or a corrupt value written by a partial `SystemParametersInfoW` call) would produce a
|
||||
/// huge pixels-per-click factor, forcing every small pixel scroll to round up to a single click.
|
||||
const MAX_WHEEL_SCROLL_LINES: u32 = 100;
|
||||
|
||||
/// Manages mouse state and posts mouse events to the system.
|
||||
pub struct Mouse;
|
||||
|
||||
impl Default for Mouse {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mouse {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, target: Vector2I) -> Result<(), String> {
|
||||
// Ensure this thread is per-monitor-v2 DPI aware so `SetCursorPos` receives coordinates in
|
||||
// physical pixels rather than being scaled.
|
||||
let _dpi_guard = DpiAwarenessGuard::enter_per_monitor_v2();
|
||||
// SAFETY: SetCursorPos accepts any i32 coordinates; it will clamp to the available display
|
||||
// region. This has no preconditions.
|
||||
unsafe { SetCursorPos(target.x(), target.y()) }.map_err(|e| {
|
||||
format!(
|
||||
"Failed to move cursor to ({}, {}): {e}",
|
||||
target.x(),
|
||||
target.y()
|
||||
)
|
||||
})?;
|
||||
// Also emit a `SendInput` mouse-move so consumers of raw input (`WM_INPUT`) and low-level
|
||||
// mouse hooks (`WH_MOUSE_LL`) — common in games, anti-cheat, and some remote-desktop
|
||||
// clients — see the motion. `SetCursorPos` alone only posts `WM_MOUSEMOVE` to the window
|
||||
// under the cursor. Best-effort: we ignore a `SendInput` failure here because the cursor
|
||||
// is already at the target position from `SetCursorPos` above.
|
||||
match normalized_virtual_desk_coords(target) {
|
||||
Some((dx, dy)) => {
|
||||
let _ = send_mouse_event_with_coords(
|
||||
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK,
|
||||
0,
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
log::warn!(
|
||||
"Skipping WM_INPUT-visible cursor move for ({}, {}): invalid virtual-screen \
|
||||
metrics",
|
||||
target.x(),
|
||||
target.y(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn button_down(&mut self, button: &MouseButton) -> Result<(), String> {
|
||||
let (flags, mouse_data) = button_down_event(button);
|
||||
send_mouse_event(flags, mouse_data)
|
||||
}
|
||||
|
||||
pub fn button_up(&mut self, button: &MouseButton) -> Result<(), String> {
|
||||
let (flags, mouse_data) = button_up_event(button);
|
||||
send_mouse_event(flags, mouse_data)
|
||||
}
|
||||
|
||||
pub fn current_position(&mut self) -> Result<Vector2I, String> {
|
||||
// Match the DPI awareness used by `move_to` so the reported position is in the same
|
||||
// coordinate space as the coordinates we send.
|
||||
let _dpi_guard = DpiAwarenessGuard::enter_per_monitor_v2();
|
||||
let mut point = POINT { x: 0, y: 0 };
|
||||
// SAFETY: `point` is a valid, writable `POINT`.
|
||||
unsafe { GetCursorPos(&mut point) }
|
||||
.map_err(|e| format!("Failed to get cursor position: {e}"))?;
|
||||
Ok(Vector2I::new(point.x, point.y))
|
||||
}
|
||||
|
||||
pub fn scroll(
|
||||
&mut self,
|
||||
direction: &ScrollDirection,
|
||||
distance: &ScrollDistance,
|
||||
) -> Result<(), String> {
|
||||
// Match the DPI awareness used by `move_to` / `current_position` so
|
||||
// `cursor_monitor_dpi` (invoked via `pixels_per_click` → `scaled_line_height_px`) resolves
|
||||
// the cursor's monitor in physical pixels even when the host process is not manifest-
|
||||
// declared per-monitor-v2 DPI aware.
|
||||
let _dpi_guard = DpiAwarenessGuard::enter_per_monitor_v2();
|
||||
// Windows expresses wheel amounts in multiples of WHEEL_DELTA (120 per "click"). Positive
|
||||
// values scroll forward (up/right); negative values scroll backward (down/left).
|
||||
// Both `Clicks` and `Pixels` are treated as unsigned magnitudes here; the direction is
|
||||
// encoded separately in `ScrollDirection`, so we `saturating_abs()` either branch to avoid
|
||||
// a negative distance canceling out `ScrollDirection` and scrolling the wrong way.
|
||||
//
|
||||
// Resolve axis flags and sign from `direction` in a single match so the
|
||||
// vertical/horizontal decision lives in exactly one place.
|
||||
let (flags, sign) = match direction {
|
||||
ScrollDirection::Up => (MOUSEEVENTF_WHEEL, 1),
|
||||
ScrollDirection::Down => (MOUSEEVENTF_WHEEL, -1),
|
||||
// Horizontal wheel: positive = right, negative = left.
|
||||
ScrollDirection::Right => (MOUSEEVENTF_HWHEEL, 1),
|
||||
ScrollDirection::Left => (MOUSEEVENTF_HWHEEL, -1),
|
||||
};
|
||||
let is_horizontal = flags == MOUSEEVENTF_HWHEEL;
|
||||
|
||||
let magnitude: i32 = match distance {
|
||||
ScrollDistance::Clicks(clicks) => clicks.saturating_abs().saturating_mul(WHEEL_DELTA),
|
||||
ScrollDistance::Pixels(pixels) => {
|
||||
// Derive pixels-per-click from the user's actual system setting
|
||||
// (`SPI_GETWHEELSCROLLLINES` for vertical, `SPI_GETWHEELSCROLLCHARS` for
|
||||
// horizontal) so we respect mouse / trackpad driver configuration instead of a
|
||||
// hard-coded constant.
|
||||
//
|
||||
// `pixels` is treated as a magnitude because the direction is encoded separately
|
||||
// in `ScrollDirection`. A zero-pixel request is a no-op; non-zero requests below
|
||||
// `pixels_per_click` round up to a single click so the scroll is still observable.
|
||||
let abs_pixels = pixels.saturating_abs();
|
||||
if abs_pixels == 0 {
|
||||
0
|
||||
} else {
|
||||
let per_click = pixels_per_click(is_horizontal);
|
||||
let clicks = (abs_pixels / per_click).clamp(1, i32::MAX / WHEEL_DELTA);
|
||||
clicks.saturating_mul(WHEEL_DELTA)
|
||||
}
|
||||
}
|
||||
};
|
||||
let signed_amount = magnitude.saturating_mul(sign);
|
||||
|
||||
// Skip zero-delta wheel events (e.g., `Clicks(0)` or `Pixels(0)`). Windows would still
|
||||
// dispatch them as observable `WM_MOUSEWHEEL`s even though no scrolling happens.
|
||||
if signed_amount == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
// `mouseData` is declared as a `u32` but `MOUSEEVENTF_WHEEL`/`HWHEEL` reinterpret the
|
||||
// bits as a signed `i32` (positive scrolls up/right, negative scrolls down/left). `as u32`
|
||||
// on an `i32` is the well-defined two's-complement reinterpretation we want here.
|
||||
send_mouse_event(flags, signed_amount as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of pixels that correspond to one wheel "click" on the requested axis,
|
||||
/// derived from the user's `SPI_GETWHEELSCROLL{LINES,CHARS}` setting. Horizontal wheel on Windows
|
||||
/// is conventionally driven by `SPI_GETWHEELSCROLLCHARS`, not `SPI_GETWHEELSCROLLLINES`. Falls
|
||||
/// back to the Windows default (3 lines/chars) if the setting is unavailable or set to the
|
||||
/// `WHEEL_PAGESCROLL` sentinel.
|
||||
fn pixels_per_click(is_horizontal: bool) -> i32 {
|
||||
let spi: SYSTEM_PARAMETERS_INFO_ACTION = if is_horizontal {
|
||||
SPI_GETWHEELSCROLLCHARS
|
||||
} else {
|
||||
SPI_GETWHEELSCROLLLINES
|
||||
};
|
||||
let mut units: u32 = DEFAULT_WHEEL_SCROLL_LINES;
|
||||
// SAFETY: `units` is a valid writable u32 and we pass its size implicitly via the fixed-layout
|
||||
// `SPI_GETWHEELSCROLL*` contract. The call does not retain the pointer beyond the call.
|
||||
let result = unsafe {
|
||||
SystemParametersInfoW(
|
||||
spi,
|
||||
0,
|
||||
Some(&mut units as *mut u32 as *mut c_void),
|
||||
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
|
||||
)
|
||||
};
|
||||
// If the call fails, or the user has configured page scrolling (WHEEL_PAGESCROLL == u32::MAX),
|
||||
// fall back to the documented default.
|
||||
if result.is_err() || units == 0 || units == u32::MAX {
|
||||
units = DEFAULT_WHEEL_SCROLL_LINES;
|
||||
}
|
||||
// Clamp to `[1, MAX_WHEEL_SCROLL_LINES]` so this function's caller can always divide by the
|
||||
// result without risking a divide-by-zero (even if a future refactor drops the `== 0` guard
|
||||
// above) and so an unusually large setting can't produce a huge pixels-per-click factor.
|
||||
let units = units.clamp(1, MAX_WHEEL_SCROLL_LINES);
|
||||
(units as i32).saturating_mul(scaled_line_height_px())
|
||||
}
|
||||
|
||||
/// Returns the nominal line height in *physical* pixels for the monitor the cursor is currently
|
||||
/// on, so `ScrollDistance::Pixels` translations stay proportional to the user's display scaling
|
||||
/// even on mixed-DPI multi-monitor setups (which `GetDpiForSystem` can't express).
|
||||
fn scaled_line_height_px() -> i32 {
|
||||
let dpi = cursor_monitor_dpi();
|
||||
// `NOMINAL_LINE_HEIGHT_PX * dpi / 96`, saturating; integer math is sufficient at the
|
||||
// precision we care about here.
|
||||
let scaled = (NOMINAL_LINE_HEIGHT_PX as i64).saturating_mul(dpi as i64) / DEFAULT_DPI as i64;
|
||||
// Re-clamp back into `i32` range and ensure at least 1 so callers can divide safely.
|
||||
scaled.clamp(1, i32::MAX as i64) as i32
|
||||
}
|
||||
|
||||
/// Returns the effective DPI of the monitor currently containing the cursor, falling back to
|
||||
/// `DEFAULT_DPI` if any step of the query fails. Using the cursor's monitor (rather than the
|
||||
/// primary) keeps `ScrollDistance::Pixels` proportional to the display the user is actually
|
||||
/// scrolling on.
|
||||
fn cursor_monitor_dpi() -> u32 {
|
||||
let mut point = POINT { x: 0, y: 0 };
|
||||
// SAFETY: `point` is a valid, writable `POINT`.
|
||||
if unsafe { GetCursorPos(&mut point) }.is_err() {
|
||||
return DEFAULT_DPI;
|
||||
}
|
||||
// SAFETY: `MonitorFromPoint` has no preconditions; `MONITOR_DEFAULTTONEAREST` guarantees a
|
||||
// non-null handle when any monitor exists.
|
||||
let hmonitor = unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) };
|
||||
if hmonitor.is_invalid() {
|
||||
return DEFAULT_DPI;
|
||||
}
|
||||
let mut dpi_x: u32 = 0;
|
||||
let mut dpi_y: u32 = 0;
|
||||
// SAFETY: `hmonitor` is valid; `dpi_x`/`dpi_y` are writable u32s.
|
||||
if unsafe { GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }.is_err() {
|
||||
return DEFAULT_DPI;
|
||||
}
|
||||
// Guard against the (unexpected) 0 return so we never produce a 0-pixel line height.
|
||||
if dpi_x == 0 { DEFAULT_DPI } else { dpi_x }
|
||||
}
|
||||
|
||||
/// Translates a virtual-screen pixel coordinate into the `[0, 65535]` normalized absolute
|
||||
/// coordinates `SendInput` expects when `MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK` is set.
|
||||
/// Returns `None` if the virtual screen metrics are unusable.
|
||||
fn normalized_virtual_desk_coords(target: Vector2I) -> Option<(i32, i32)> {
|
||||
// SAFETY: `GetSystemMetrics` has no preconditions.
|
||||
let virt_x = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) };
|
||||
let virt_y = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) };
|
||||
let virt_w = unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) };
|
||||
let virt_h = unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) };
|
||||
if virt_w <= 0 || virt_h <= 0 {
|
||||
return None;
|
||||
}
|
||||
// Normalize into `[0, 65535]` across the virtual desktop. Use i64 to avoid overflow when the
|
||||
// virtual screen is large.
|
||||
let dx = (target.x() as i64 - virt_x as i64) * 65535 / virt_w as i64;
|
||||
let dy = (target.y() as i64 - virt_y as i64) * 65535 / virt_h as i64;
|
||||
Some((dx.clamp(0, 65535) as i32, dy.clamp(0, 65535) as i32))
|
||||
}
|
||||
|
||||
/// Returns the `(flags, mouseData)` pair for a mouse button-down event.
|
||||
fn button_down_event(button: &MouseButton) -> (MOUSE_EVENT_FLAGS, u32) {
|
||||
match button {
|
||||
MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, 0),
|
||||
MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, 0),
|
||||
MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, 0),
|
||||
MouseButton::Back => (MOUSEEVENTF_XDOWN, XBUTTON1),
|
||||
MouseButton::Forward => (MOUSEEVENTF_XDOWN, XBUTTON2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `(flags, mouseData)` pair for a mouse button-up event.
|
||||
fn button_up_event(button: &MouseButton) -> (MOUSE_EVENT_FLAGS, u32) {
|
||||
match button {
|
||||
MouseButton::Left => (MOUSEEVENTF_LEFTUP, 0),
|
||||
MouseButton::Right => (MOUSEEVENTF_RIGHTUP, 0),
|
||||
MouseButton::Middle => (MOUSEEVENTF_MIDDLEUP, 0),
|
||||
MouseButton::Back => (MOUSEEVENTF_XUP, XBUTTON1),
|
||||
MouseButton::Forward => (MOUSEEVENTF_XUP, XBUTTON2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches a single mouse event via `SendInput` with `dx` = `dy` = 0 (i.e., at the current
|
||||
/// cursor position). Use [`send_mouse_event_with_coords`] for absolute-positioned events such as
|
||||
/// `MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE`.
|
||||
fn send_mouse_event(flags: MOUSE_EVENT_FLAGS, mouse_data: u32) -> Result<(), String> {
|
||||
send_mouse_event_with_coords(flags, mouse_data, 0, 0)
|
||||
}
|
||||
|
||||
/// Dispatches a single mouse event via `SendInput`. `dx`/`dy` are interpreted per Win32 docs:
|
||||
/// absolute `[0, 65535]` normalized coordinates when `MOUSEEVENTF_ABSOLUTE` is set, otherwise
|
||||
/// relative movement.
|
||||
fn send_mouse_event_with_coords(
|
||||
flags: MOUSE_EVENT_FLAGS,
|
||||
mouse_data: u32,
|
||||
dx: i32,
|
||||
dy: i32,
|
||||
) -> Result<(), String> {
|
||||
let input = INPUT {
|
||||
r#type: INPUT_MOUSE,
|
||||
Anonymous: INPUT_0 {
|
||||
mi: MOUSEINPUT {
|
||||
dx,
|
||||
dy,
|
||||
mouseData: mouse_data,
|
||||
dwFlags: flags,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// SAFETY: `input` is a valid `INPUT` of mouse type, with the correct size passed to
|
||||
// `SendInput`. The call does not retain any pointer beyond the call.
|
||||
let sent = unsafe { SendInput(&[input], size_of::<INPUT>() as i32) };
|
||||
if sent != 1 {
|
||||
// SAFETY: `GetLastError` has no preconditions; reads the calling thread's last-error.
|
||||
let last_error = unsafe { GetLastError() }.0;
|
||||
return Err(format!(
|
||||
"SendInput failed to dispatch mouse event (flags={:#x}, GetLastError={last_error})",
|
||||
flags.0,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user