diff --git a/docs/config.adoc b/docs/config.adoc index 660f27a44..679c2a1a4 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1475,8 +1475,7 @@ to remap scroll events as if they were keys, corresponding to up, down, left, right respectively: `mwu`, `mwd`, `mwl`, `mwr`. -The remapping of mouse button events is effective on Linux, Windows, and macOS. -Mouse wheel remapping is currently only effective on Linux and Windows. +The remapping of mouse button and wheel events is effective on Linux, Windows, and macOS. NOTE: On Windows and macOS, the Kanata process must be restarted diff --git a/docs/platform-known-issues.adoc b/docs/platform-known-issues.adoc index 1059fec32..851141347 100644 --- a/docs/platform-known-issues.adoc +++ b/docs/platform-known-issues.adoc @@ -89,4 +89,3 @@ and explicitly map keys in `defsrc` instead == MacOS * Mouse input processing requires Accessibility or Input Monitoring permission in System Settings > Privacy & Security -* Mouse wheel input is not yet supported in `defsrc` diff --git a/src/oskbd/macos.rs b/src/oskbd/macos.rs index 15f113b87..71d1882e2 100644 --- a/src/oskbd/macos.rs +++ b/src/oskbd/macos.rs @@ -640,6 +640,34 @@ impl TryFrom<(CGEventType, i64)> for KeyEvent { } } +/// Decode a `ScrollWheel` `CGEvent` into a kanata `KeyEvent`. A scroll event +/// may carry both axes simultaneously (diagonal scroll on a trackpad); we +/// pick the dominant axis with vertical winning ties, matching how Linux +/// processes one `REL_WHEEL`/`REL_HWHEEL` at a time. The axis/sign convention +/// mirrors `OsKbdOut::scroll`. +fn scroll_event_to_key_event(event: &CGEvent) -> Option { + use OsCode::*; + let dy = event.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1); + let dx = event.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2); + let code = if dy.abs() >= dx.abs() { + match dy.signum() { + 1 => MouseWheelDown, + -1 => MouseWheelUp, + _ => return None, + } + } else { + match dx.signum() { + 1 => MouseWheelLeft, + -1 => MouseWheelRight, + _ => return None, + } + }; + Some(KeyEvent { + code, + value: KeyValue::Tap, + }) +} + /// Start a CGEventTap on a background thread to intercept mouse button events. /// macOS equivalent of the Windows mouse hook in `windows/llhook.rs`. /// @@ -653,7 +681,17 @@ pub fn start_mouse_listener( mapped_keys: &MappedKeys, ) -> Option> { use OsCode::*; - let mouse_oscodes = [BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA]; + let mouse_oscodes = [ + BTN_LEFT, + BTN_RIGHT, + BTN_MIDDLE, + BTN_SIDE, + BTN_EXTRA, + MouseWheelUp, + MouseWheelDown, + MouseWheelLeft, + MouseWheelRight, + ]; // Copy only the mouse-relevant mapped keys for the callback closure. let mapped: MappedKeys = mapped_keys .iter() @@ -661,7 +699,7 @@ pub fn start_mouse_listener( .filter(|k| mouse_oscodes.contains(k)) .collect(); if mapped.is_empty() { - log::info!("No mouse buttons in defsrc. Not installing mouse event tap."); + log::info!("No mouse buttons or wheel in defsrc. Not installing mouse event tap."); return None; } @@ -675,6 +713,7 @@ pub fn start_mouse_listener( CGEventType::RightMouseUp, CGEventType::OtherMouseDown, CGEventType::OtherMouseUp, + CGEventType::ScrollWheel, ]; let tap = match CGEventTap::new( @@ -685,6 +724,21 @@ pub fn start_mouse_listener( // Callback receives &CGEvent; return Some(clone) to pass through, // None to suppress the event. move |_proxy, event_type, event| { + if matches!(event_type, CGEventType::ScrollWheel) { + let Some(key_event) = scroll_event_to_key_event(event) else { + return Some(event.clone()); + }; + if !mapped.contains(&key_event.code) { + return Some(event.clone()); + } + log::debug!("mouse tap (wheel): {key_event:?}"); + if let Err(e) = tx.try_send(key_event) { + log::warn!("mouse tap: failed to send wheel event: {e}"); + return Some(event.clone()); + } + return None; + } + let button_number = event.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER); let mut key_event = match KeyEvent::try_from((event_type, button_number)) {