blob: 00853ab7c322c19324e2ebc133f908bc4f658759 [file]
// CREDIT (VTE Reader): <https://github.com/wezterm/wezterm/blob/a87358516004a652ad840bc1661bdf65ffc89b43/termwiz/src/input.rs#L676-L885>
// I have dropped the legacy Console API handling however and switched to the `AsciiChar` part of
// the key record. I suspect that Termwiz may be incorrect here as the Microsoft docs say that the
// proper way to read UTF-8 is to use the `A` variant (`ReadConsoleInputA` while WezTerm uses
// `ReadConsoleInputW`) to read a byte.
//
// CREDIT (Console API):
// Most legacy input handling comes from crossterm <https://github.com/crossterm-rs/crossterm/blob/4f08595ef4477de2d504dcced24060ed9e3d582a/src/event/sys/windows/parse.rs>
// with some bits coming from crossterm-winapi <https://github.com/crossterm-rs/crossterm-winapi/blob/49bc68d73e82374224284baf0ba51ed3a29c0d81/src/structs/input.rs>
// The Windows API functions have been converted from winapi to the windows-sys crate.
use super::*;
use windows_sys::Win32::System::Console;
#[cfg(feature = "windows-legacy")]
pub use legacy::cursor_position;
/// Mode to use for reading Windows input events.
///
/// VTE mode asks the Windows console to emit virtual-terminal input and then parses those bytes
/// with [`crate::Parser`]. Legacy mode reads `INPUT_RECORD` values from the classic console API and
/// translates them directly into [`crate::Event`] values.
///
/// [`crate::PlatformTerminal`] uses [`Self::Vte`] by default. The `windows-legacy` feature must be
/// enabled to construct a terminal with a custom input reader mode.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum InputReaderMode {
/// Read input as virtual-terminal escape sequences.
///
/// This is the default mode. It matches Unix terminal input more closely and supports terminal
/// protocol responses that arrive as escape sequences.
Vte,
/// Read input through the classic Windows console API.
///
/// This mode is available only with the `windows-legacy` feature. It can be useful in console
/// environments where virtual-terminal input is unavailable or unreliable.
Legacy,
}
impl Parser {
pub(crate) fn decode_input_records(
&mut self,
records: &[Console::INPUT_RECORD],
) -> io::Result<()> {
self.expire_reply_at(Instant::now())?;
for record in records {
match record.EventType as u32 {
Console::KEY_EVENT => {
let record = unsafe { record.Event.KeyEvent };
match self.mode {
InputReaderMode::Vte => {
// This skips 'down's. IIRC Termwiz skips 'down's and Crossterm skips
// 'up's. If we skip 'up's we don't seem to get key events at all.
if record.bKeyDown == 0 {
continue;
}
let byte = unsafe { record.uChar.AsciiChar } as u8;
// The zero byte is sent when the input record is not VT.
if byte == 0 {
continue;
}
// `read_console_input` uses `ReadConsoleInputA` so we should treat the
// key code as a byte and add it to the buffer.
self.push_byte(byte, true)?;
}
InputReaderMode::Legacy => {
#[cfg(feature = "windows-legacy")]
if let Some(event) =
legacy::handle_key_event(record, &mut self.surrogate_buffer)
{
self.events.push_back(event);
}
}
}
}
Console::WINDOW_BUFFER_SIZE_EVENT => {
// NOTE: the `WINDOW_BUFFER_SIZE_EVENT` coordinates are one-based, even
// though `GetConsoleScreenBufferInfo` is zero-based.
use crate::{OneBased, WindowSize};
let record = unsafe { record.Event.WindowBufferSizeEvent };
let Some(rows) = OneBased::new(record.dwSize.Y as u16) else {
continue;
};
let Some(cols) = OneBased::new(record.dwSize.X as u16) else {
continue;
};
self.events.push_back(Event::WindowResized(WindowSize {
rows: rows.get(),
cols: cols.get(),
pixel_width: None,
pixel_height: None,
}));
}
Console::FOCUS_EVENT => {
#[cfg(feature = "windows-legacy")]
self.events
.push_back(legacy::handle_focus(unsafe { record.Event.FocusEvent }));
}
Console::MOUSE_EVENT => {
#[cfg(feature = "windows-legacy")]
{
let record = unsafe { record.Event.MouseEvent };
let button_state: legacy::ButtonState = record.dwButtonState.into();
let mouse_event =
legacy::handle_mouse_event(record, &self.mouse_buttons_pressed);
self.mouse_buttons_pressed = legacy::MouseButtonsPressed {
left: button_state.left_button(),
right: button_state.right_button(),
middle: button_state.middle_button(),
};
if let Some(event) = mouse_event {
self.events.push_back(event);
}
}
}
_ => (),
}
}
if self.mode == InputReaderMode::Vte {
self.process_bytes(false)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::KeyCode;
fn key_record(byte: u8) -> Console::INPUT_RECORD {
Console::INPUT_RECORD {
EventType: Console::KEY_EVENT as u16,
Event: Console::INPUT_RECORD_0 {
KeyEvent: Console::KEY_EVENT_RECORD {
bKeyDown: 1,
wRepeatCount: 1,
wVirtualKeyCode: 0,
wVirtualScanCode: 0,
uChar: Console::KEY_EVENT_RECORD_0 {
AsciiChar: byte as i8,
},
dwControlKeyState: 0,
},
},
}
}
#[test]
fn vte_records_propagate_sticky_budget_errors_without_replaying_tail() {
let mut parser = Parser::with_mode(InputReaderMode::Vte);
parser.set_max_pending_bytes(8).unwrap();
let records: Vec<_> = b"a\x1b[6;2;0000~z"
.iter()
.copied()
.map(key_record)
.collect();
assert_eq!(
parser.decode_input_records(&records).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('a').into())));
assert_eq!(parser.pop(), None);
assert_eq!(
parser
.decode_input_records(&[key_record(b'z')])
.unwrap_err()
.kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), None);
}
}
#[cfg(feature = "windows-legacy")]
pub(crate) mod legacy {
use std::{io, ptr};
use crate::event::{
KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseButton, MouseEvent,
MouseEventKind,
};
use crate::{Event, OneBased};
use windows_sys::Win32::Foundation::{
CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows_sys::Win32::System::Console::{
self, CAPSLOCK_ON, CONSOLE_SCREEN_BUFFER_INFO, DOUBLE_CLICK, FOCUS_EVENT_RECORD,
FROM_LEFT_1ST_BUTTON_PRESSED, FROM_LEFT_2ND_BUTTON_PRESSED, FROM_LEFT_3RD_BUTTON_PRESSED,
FROM_LEFT_4TH_BUTTON_PRESSED, KEY_EVENT_RECORD, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED,
MOUSE_EVENT_RECORD, MOUSE_HWHEELED, MOUSE_MOVED, MOUSE_WHEELED, RIGHTMOST_BUTTON_PRESSED,
RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
};
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
GetKeyboardLayout, ToUnicodeEx, VK_BACK, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE,
VK_F1, VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9,
VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_TAB, VK_UP,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowThreadProcessId,
};
enum WindowsKeyEvent {
KeyEvent(KeyEvent),
Surrogate(u16),
}
enum CharCase {
LowerCase,
UpperCase,
}
#[derive(Debug, Default)]
pub(crate) struct MouseButtonsPressed {
pub(crate) left: bool,
pub(crate) right: bool,
pub(crate) middle: bool,
}
/// The status of the mouse buttons.
/// The least significant bit corresponds to the leftmost mouse button.
/// The next least significant bit corresponds to the rightmost mouse button.
/// The next bit indicates the next-to-leftmost mouse button.
/// The bits then correspond left to right to the mouse buttons.
/// A bit is 1 if the button was pressed.
///
/// The state can be one of the following:
///
/// ```
/// # enum __ {
/// Release = 0x0000,
/// /// The leftmost mouse button.
/// FromLeft1stButtonPressed = 0x0001,
/// /// The second button from the left.
/// FromLeft2ndButtonPressed = 0x0004,
/// /// The third button from the left.
/// FromLeft3rdButtonPressed = 0x0008,
/// /// The fourth button from the left.
/// FromLeft4thButtonPressed = 0x0010,
/// /// The rightmost mouse button.
/// RightmostButtonPressed = 0x0002,
/// /// This button state is not recognized.
/// Unknown = 0x0021,
/// /// The wheel was rotated backward, toward the user.
/// /// This is active only for `MOUSE_WHEELED` from `dwEventFlags`.
/// Negative = 0x0020,
/// # }
/// ```
///
/// [Ms Docs](https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str#members)
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) struct ButtonState {
state: i32,
}
impl From<u32> for ButtonState {
#[inline]
fn from(event: u32) -> Self {
let state = event as i32;
ButtonState { state }
}
}
impl ButtonState {
/// Get whether no buttons are being pressed.
fn release_button(&self) -> bool {
self.state == 0
}
/// Returns whether the left button was pressed.
pub(super) fn left_button(&self) -> bool {
self.state as u32 & FROM_LEFT_1ST_BUTTON_PRESSED != 0
}
/// Returns whether the right button was pressed.
pub(super) fn right_button(&self) -> bool {
self.state as u32
& (RIGHTMOST_BUTTON_PRESSED
| FROM_LEFT_3RD_BUTTON_PRESSED
| FROM_LEFT_4TH_BUTTON_PRESSED)
!= 0
}
/// Returns whether the right button was pressed.
pub(super) fn middle_button(&self) -> bool {
self.state as u32 & FROM_LEFT_2ND_BUTTON_PRESSED != 0
}
/// Returns whether there is a down scroll.
fn scroll_down(&self) -> bool {
self.state < 0
}
/// Returns whether there is a up scroll.
fn scroll_up(&self) -> bool {
self.state > 0
}
/// Returns whether there is a horizontal scroll to the right.
fn scroll_right(&self) -> bool {
self.state > 0
}
/// Returns whether there is a horizontal scroll to the left.
fn scroll_left(&self) -> bool {
self.state < 0
}
}
pub(super) fn handle_key_event(
key_event: KEY_EVENT_RECORD,
surrogate_buffer: &mut Option<u16>,
) -> Option<Event> {
let windows_key_event = parse_key_event_record(&key_event)?;
match windows_key_event {
WindowsKeyEvent::KeyEvent(key_event) => {
// Discard any buffered surrogate value if another valid key event comes before the
// next surrogate value.
*surrogate_buffer = None;
Some(Event::Key(key_event))
}
WindowsKeyEvent::Surrogate(new_surrogate) => {
let ch = handle_surrogate(surrogate_buffer, new_surrogate)?;
let modifiers = handle_control_key_state(key_event.dwControlKeyState);
let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers);
Some(Event::Key(key_event))
}
}
}
pub(super) fn handle_focus(record: FOCUS_EVENT_RECORD) -> Event {
if record.bSetFocus > 0 {
Event::FocusIn
} else {
Event::FocusOut
}
}
fn parse_key_event_record(key_event: &KEY_EVENT_RECORD) -> Option<WindowsKeyEvent> {
let modifiers = handle_control_key_state(key_event.dwControlKeyState);
let virtual_key_code = key_event.wVirtualKeyCode as i32;
// We normally ignore all key release events, but we will make an exception for an Alt key
// release if it carries a u_char value, as this indicates an Alt code.
let is_alt_code = virtual_key_code == VK_MENU as i32
&& key_event.bKeyDown != 1
&& unsafe { key_event.uChar.UnicodeChar } != 0;
if is_alt_code {
let utf16 = unsafe { key_event.uChar.UnicodeChar };
match utf16 {
surrogate @ 0xD800..=0xDFFF => {
return Some(WindowsKeyEvent::Surrogate(surrogate));
}
unicode_scalar_value => {
// Unwrap is safe: We tested for surrogate values above and those are the only
// u16 values that are invalid when directly interpreted as unicode scalar
// values.
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
let key_code = KeyCode::Char(ch);
let kind = if key_event.bKeyDown == 1 {
KeyEventKind::Press
} else {
KeyEventKind::Release
};
let key_event = KeyEvent {
code: key_code,
modifiers,
kind,
state: KeyEventState::empty(),
};
return Some(WindowsKeyEvent::KeyEvent(key_event));
}
}
}
// Don't generate events for numpad key presses when they're producing Alt codes.
let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&(virtual_key_code as u16));
let is_only_alt_modifier = modifiers.contains(Modifiers::ALT)
&& !modifiers.contains(Modifiers::SHIFT | Modifiers::CONTROL);
if is_only_alt_modifier && is_numpad_numeric_key {
return None;
}
let parse_result = match virtual_key_code as u16 {
VK_SHIFT | VK_CONTROL | VK_MENU => None,
VK_BACK => Some(KeyCode::Backspace),
VK_ESCAPE => Some(KeyCode::Escape),
VK_RETURN => Some(KeyCode::Enter),
VK_F1..=VK_F24 => Some(KeyCode::Function((key_event.wVirtualKeyCode - 111) as u8)),
VK_LEFT => Some(KeyCode::Left),
VK_UP => Some(KeyCode::Up),
VK_RIGHT => Some(KeyCode::Right),
VK_DOWN => Some(KeyCode::Down),
VK_PRIOR => Some(KeyCode::PageUp),
VK_NEXT => Some(KeyCode::PageDown),
VK_HOME => Some(KeyCode::Home),
VK_END => Some(KeyCode::End),
VK_DELETE => Some(KeyCode::Delete),
VK_INSERT => Some(KeyCode::Insert),
VK_TAB if modifiers.contains(Modifiers::SHIFT) => Some(KeyCode::BackTab),
VK_TAB => Some(KeyCode::Tab),
_ => {
let utf16 = unsafe { key_event.uChar.UnicodeChar };
match utf16 {
0x00..=0x1f => {
// Some key combinations generate either no u_char value or generate control
// codes. To deliver back a KeyCode::Char(...) event we want to know which
// character the key normally maps to on the user's keyboard layout.
// The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.)
// are handled by their virtual key codes above.
get_char_for_key(key_event).map(KeyCode::Char)
}
surrogate @ 0xD800..=0xDFFF => {
return Some(WindowsKeyEvent::Surrogate(surrogate));
}
unicode_scalar_value => {
// Unwrap is safe: We tested for surrogate values above and those are the only
// u16 values that are invalid when directly interpreted as unicode scalar
// values.
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
Some(KeyCode::Char(ch))
}
}
}
};
if let Some(key_code) = parse_result {
let kind = if key_event.bKeyDown == 1 {
KeyEventKind::Press
} else {
KeyEventKind::Release
};
let key_event = KeyEvent {
code: key_code,
modifiers,
kind,
state: KeyEventState::empty(),
};
return Some(WindowsKeyEvent::KeyEvent(key_event));
}
None
}
fn handle_surrogate(surrogate_buffer: &mut Option<u16>, new_surrogate: u16) -> Option<char> {
match *surrogate_buffer {
Some(buffered_surrogate) => {
*surrogate_buffer = None;
std::char::decode_utf16([buffered_surrogate, new_surrogate])
.next()
.unwrap()
.ok()
}
None => {
*surrogate_buffer = Some(new_surrogate);
None
}
}
}
fn handle_control_key_state(state: u32) -> Modifiers {
let mut modifier = Modifiers::empty();
if has_state(state, SHIFT_PRESSED) {
modifier |= Modifiers::SHIFT;
}
if has_state(state, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) {
modifier |= Modifiers::CONTROL;
}
if has_state(state, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) {
modifier |= Modifiers::ALT;
}
modifier
}
fn has_state(a: u32, b: u32) -> bool {
(a & b) != 0
}
// Attempts to return the character for a key event accounting for the user's keyboard layout.
// The returned character (if any) is capitalized (if applicable) based on shift and capslock state.
// Returns None if the key doesn't map to a character or if it is a dead key.
// We use the *currently* active keyboard layout (if it can be determined). This layout may not
// correspond to the keyboard layout that was active when the user typed their input, since console
// applications get their input asynchronously from the terminal. By the time a console application
// can process a key input, the user may have changed the active layout. In this case, the character
// returned might not correspond to what the user expects, but there is no way for a console
// application to know what the keyboard layout actually was for a key event, so this is our best
// effort. If a console application processes input in a timely fashion, then it is unlikely that a
// user has time to change their keyboard layout before a key event is processed.
fn get_char_for_key(key_event: &KEY_EVENT_RECORD) -> Option<char> {
let virtual_key_code = key_event.wVirtualKeyCode as u32;
let virtual_scan_code = key_event.wVirtualScanCode as u32;
let key_state = [0u8; 256];
let mut utf16_buf = [0u16; 16];
let dont_change_kernel_keyboard_state = 0x4;
// Best-effort attempt at determining the currently active keyboard layout.
// At the time of writing, this works for a console application running in Windows Terminal, but
// doesn't work under a Conhost terminal. For Conhost, the window handle returned by
// GetForegroundWindow() does not appear to actually be the foreground window which has the
// keyboard layout associated with it (or perhaps it is, but also has special protection that
// doesn't allow us to query it).
// When this determination fails, the returned keyboard layout handle will be null, which is an
// acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx
// appears to use the keyboard layout associated with the current thread, which will be the
// layout that was inherited when the console application started (or possibly when the current
// thread was spawned). This is then unfortunately not updated when the user changes their
// keyboard layout in the terminal, but it's what we get.
let active_keyboard_layout = unsafe {
let foreground_window = GetForegroundWindow();
let foreground_thread =
GetWindowThreadProcessId(foreground_window, std::ptr::null_mut());
GetKeyboardLayout(foreground_thread)
};
let ret = unsafe {
ToUnicodeEx(
virtual_key_code,
virtual_scan_code,
key_state.as_ptr(),
utf16_buf.as_mut_ptr(),
utf16_buf.len() as i32,
dont_change_kernel_keyboard_state,
active_keyboard_layout,
)
};
// -1 indicates a dead key.
// 0 indicates no character for this key.
if ret < 1 {
return None;
}
let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize));
let mut ch = ch_iter.next()?.ok()?;
if ch_iter.next().is_some() {
// Key doesn't map to a single char.
return None;
}
let is_shift_pressed = has_state(key_event.dwControlKeyState, SHIFT_PRESSED);
let is_capslock_on = has_state(key_event.dwControlKeyState, CAPSLOCK_ON);
let desired_case = if is_shift_pressed ^ is_capslock_on {
CharCase::UpperCase
} else {
CharCase::LowerCase
};
ch = try_ensure_char_case(ch, desired_case);
Some(ch)
}
fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char {
match desired_case {
CharCase::LowerCase if ch.is_uppercase() => {
let mut iter = ch.to_lowercase();
// Unwrap is safe; iterator yields one or more chars.
let ch_lower = iter.next().unwrap();
if iter.next().is_none() {
ch_lower
} else {
ch
}
}
CharCase::UpperCase if ch.is_lowercase() => {
let mut iter = ch.to_uppercase();
// Unwrap is safe; iterator yields one or more chars.
let ch_upper = iter.next().unwrap();
if iter.next().is_none() {
ch_upper
} else {
ch
}
}
_ => ch,
}
}
pub(super) fn handle_mouse_event(
mouse_event: Console::MOUSE_EVENT_RECORD,
buttons_pressed: &MouseButtonsPressed,
) -> Option<Event> {
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event, buttons_pressed) {
return Some(Event::Mouse(event));
}
None
}
fn screen_buffer() -> CONSOLE_SCREEN_BUFFER_INFO {
unsafe {
let utf16: Vec<u16> = "CONOUT$\0".encode_utf16().collect();
let utf16_ptr: *const u16 = utf16.as_ptr();
let handle = CreateFileW(
utf16_ptr,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
);
let mut buffer_info = CONSOLE_SCREEN_BUFFER_INFO::default();
if handle != INVALID_HANDLE_VALUE {
Console::GetConsoleScreenBufferInfo(handle, &mut buffer_info);
// `CreateFileW` returns an owned handle; close it so each mouse event and cursor
// query doesn't leak a `CONOUT$` handle.
CloseHandle(handle);
}
buffer_info
}
}
// The 'y' position of a mouse event or resize event is not relative to the window but absolute to screen buffer.
// This means that when the mouse cursor is at the top left it will be x: 0, y: 2295 (e.g. y = number of cells counting from the absolute buffer height) instead of relative x: 0, y: 0 to the window.
fn parse_relative_y(y: i16) -> std::io::Result<i16> {
let window_size = screen_buffer().srWindow;
Ok((y - window_size.Top).max(0))
}
pub fn cursor_position() -> io::Result<(OneBased, OneBased)> {
let buffer = screen_buffer();
let position = buffer.dwCursorPosition;
Ok((
OneBased::from_zero_based(position.X as u16),
OneBased::from_zero_based((position.Y - buffer.srWindow.Top).max(0) as u16),
))
}
fn parse_mouse_event_record(
event: &MOUSE_EVENT_RECORD,
buttons_pressed: &MouseButtonsPressed,
) -> std::io::Result<Option<MouseEvent>> {
let modifiers = handle_control_key_state(event.dwControlKeyState);
let xpos = event.dwMousePosition.X as u16;
let ypos = parse_relative_y(event.dwMousePosition.Y)? as u16;
let button_state: ButtonState = event.dwButtonState.into();
let kind = match event.dwEventFlags {
0x0000 | DOUBLE_CLICK => {
if button_state.left_button() && !buttons_pressed.left {
Some(MouseEventKind::Down(MouseButton::Left))
} else if !button_state.left_button() && buttons_pressed.left {
Some(MouseEventKind::Up(MouseButton::Left))
} else if button_state.right_button() && !buttons_pressed.right {
Some(MouseEventKind::Down(MouseButton::Right))
} else if !button_state.right_button() && buttons_pressed.right {
Some(MouseEventKind::Up(MouseButton::Right))
} else if button_state.middle_button() && !buttons_pressed.middle {
Some(MouseEventKind::Down(MouseButton::Middle))
} else if !button_state.middle_button() && buttons_pressed.middle {
Some(MouseEventKind::Up(MouseButton::Middle))
} else {
None
}
}
MOUSE_MOVED => {
let button = if button_state.right_button() {
MouseButton::Right
} else if button_state.middle_button() {
MouseButton::Middle
} else {
MouseButton::Left
};
if button_state.release_button() {
Some(MouseEventKind::Moved)
} else {
Some(MouseEventKind::Drag(button))
}
}
MOUSE_WHEELED => {
// Vertical scroll
// from https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str
// if `button_state` is negative then the wheel was rotated backward, toward the user.
if button_state.scroll_down() {
Some(MouseEventKind::ScrollDown)
} else if button_state.scroll_up() {
Some(MouseEventKind::ScrollUp)
} else {
None
}
}
MOUSE_HWHEELED => {
if button_state.scroll_left() {
Some(MouseEventKind::ScrollLeft)
} else if button_state.scroll_right() {
Some(MouseEventKind::ScrollRight)
} else {
None
}
}
_ => None,
};
Ok(kind.map(|kind| MouseEvent {
kind,
column: xpos,
row: ypos,
modifiers,
}))
}
}