use arc_swap::ArcSwap;
use core::ptr::NonNull;
use objc2::msg_send;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_app_kit::{NSEvent, NSEventType, NSTouch, NSTouchPhase};
use objc2_core_foundation::{
CFMachPort, CFRetained, CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes,
};
use objc2_core_graphics::{
CGEvent, CGEventField, CGEventFlags, CGEventTapLocation, CGEventTapOptions,
CGEventTapPlacement, CGEventTapProxy, CGEventType,
};
use objc2_foundation::NSSet;
use scopeguard::ScopeGuard;
use std::ffi::c_void;
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::null_mut;
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use stdext::function_name;
use tracing::{error, info, warn};
use crate::commands::{Command, Direction, Operation};
use crate::config::Config;
use crate::config::swipe::SwipeGestureDirection;
use crate::errors::{Error, Result};
use crate::events::{Event, EventSender};
use crate::platform::Modifiers;
const NX_DEVICEFNKEYMASK: u64 = 0x0080_0100;
static FOCUSED_PASSTHROUGH: LazyLock<ArcSwap<Vec<(u8, Modifiers)>>> =
LazyLock::new(|| ArcSwap::from_pointee(Vec::new()));
pub fn set_focused_passthrough(keys: Vec<(u8, Modifiers)>) {
FOCUSED_PASSTHROUGH.store(Arc::new(keys));
}
const VERTICAL_GESTURE_SCROLL_SUPPRESS: Duration = Duration::from_millis(1200);
const WINDOW_SCROLL_INTERVAL: Duration = Duration::from_millis(180);
struct Touch {
identity: Retained<AnyObject>,
x: f64,
y: f64,
began: bool,
}
impl Touch {
fn sample(touches: &NSSet<NSTouch>) -> Vec<Self> {
touches
.iter()
.map(|touch| {
let position = touch.normalizedPosition();
Self {
identity: touch.identity(),
x: position.x,
y: position.y,
began: touch.phase() == NSTouchPhase::Began,
}
})
.collect()
}
fn is(&self, other: &Self) -> bool {
unsafe { msg_send![&*self.identity, isEqual: &*other.identity] }
}
}
static LUA_KEYBINDS: LazyLock<ArcSwap<Vec<(u8, Modifiers, u32)>>> =
LazyLock::new(|| ArcSwap::from_pointee(Vec::new()));
#[cfg(feature = "lua")]
pub fn set_lua_keybinds(keys: Vec<(u8, Modifiers, u32)>) {
LUA_KEYBINDS.store(Arc::new(keys));
}
const SWIPE_THRESHOLD: f64 = 0.001;
const GESTURE_MINIMAL_FINGERS: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TapHealth {
Healthy,
Reenabled,
Rebuilt,
Failed,
}
pub(super) struct InputHandler {
events: Option<EventSender>,
config: Config,
finger_position: Option<Vec<Touch>>,
tap_port: Option<CFRetained<CFMachPort>>,
run_loop_source: Option<CFRetained<CFRunLoopSource>>,
last_swipe_time: Option<Instant>,
last_window_scroll: Option<(Instant, Direction)>,
_pin: PhantomPinned,
}
pub(super) type PinnedInputHandler =
ScopeGuard<Pin<Box<InputHandler>>, Box<dyn FnOnce(Pin<Box<InputHandler>>)>>;
impl InputHandler {
pub(super) fn new(events: EventSender, config: Config) -> Self {
InputHandler {
events: Some(events),
config,
finger_position: None,
tap_port: None,
run_loop_source: None,
last_swipe_time: None,
last_window_scroll: None,
_pin: PhantomPinned,
}
}
pub(super) fn start(self) -> Result<PinnedInputHandler> {
let mut pinned = Box::pin(self);
unsafe { pinned.as_mut().get_unchecked_mut() }.install_tap()?;
Ok(scopeguard::guard(
pinned,
Box::new(|mut handler: Pin<Box<Self>>| {
info!("Unregistering event_handler");
unsafe { handler.as_mut().get_unchecked_mut() }.uninstall_tap();
}),
))
}
fn install_tap(&mut self) -> Result<()> {
let mouse_event_mask = (1 << CGEventType::MouseMoved.0)
| (1 << CGEventType::LeftMouseDown.0)
| (1 << CGEventType::LeftMouseUp.0)
| (1 << CGEventType::LeftMouseDragged.0)
| (1 << CGEventType::RightMouseDown.0)
| (1 << CGEventType::RightMouseUp.0)
| (1 << CGEventType::RightMouseDragged.0)
| (1 << CGEventType::ScrollWheel.0)
| (1 << NSEventType::Gesture.0)
| (1 << CGEventType::KeyDown.0);
let this = NonNull::from(&mut *self).as_ptr();
let port = unsafe {
CGEvent::tap_create(
CGEventTapLocation::HIDEventTap,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
mouse_event_mask,
Some(Self::callback),
this.cast(),
)
}
.ok_or_else(|| {
Error::PermissionDenied(format!("{}: Can not create EventTap.", function_name!()))
})?;
let (run_loop_source, main_loop) = CFMachPort::new_run_loop_source(None, Some(&port), 0)
.zip(CFRunLoop::main())
.ok_or_else(|| {
Error::PermissionDenied(format!(
"{}: Unable to create run loop source",
function_name!()
))
})?;
CFRunLoop::add_source(&main_loop, Some(&run_loop_source), unsafe {
kCFRunLoopCommonModes
});
self.tap_port = Some(port);
self.run_loop_source = Some(run_loop_source);
Ok(())
}
fn uninstall_tap(&mut self) {
if let Some(run_loop_source) = self.run_loop_source.take()
&& let Some(main_loop) = CFRunLoop::main()
{
CFRunLoop::remove_source(&main_loop, Some(&run_loop_source), unsafe {
kCFRunLoopCommonModes
});
}
if let Some(port) = self.tap_port.take()
&& port.is_valid()
{
CGEvent::tap_enable(&port, false);
CFMachPort::invalidate(&port);
}
}
pub(super) fn ensure_tap_alive(&mut self) -> TapHealth {
let Some(port) = self.tap_port.clone().filter(|port| port.is_valid()) else {
return self.rebuild_tap();
};
if CGEvent::tap_is_enabled(&port) {
return TapHealth::Healthy;
}
CGEvent::tap_enable(&port, true);
if CGEvent::tap_is_enabled(&port) {
warn!("event tap was disabled, re-enabled it");
TapHealth::Reenabled
} else {
self.rebuild_tap()
}
}
fn rebuild_tap(&mut self) -> TapHealth {
self.uninstall_tap();
match self.install_tap() {
Ok(()) => {
warn!("event tap was unusable, rebuilt it");
TapHealth::Rebuilt
}
Err(err) => {
error!("unable to rebuild event tap: {err}");
TapHealth::Failed
}
}
}
extern "C-unwind" fn callback(
_: CGEventTapProxy,
event_type: CGEventType,
mut event_ref: NonNull<CGEvent>,
this: *mut c_void,
) -> *mut CGEvent {
if let Some(this) =
NonNull::new(this).map(|this| unsafe { this.cast::<InputHandler>().as_mut() })
{
let intercept = this.input_handler(event_type, unsafe { event_ref.as_ref() });
if intercept {
return null_mut();
}
} else {
error!("Zero passed to Event Handler.");
}
unsafe { event_ref.as_mut() }
}
fn input_handler(&mut self, event_type: CGEventType, event: &CGEvent) -> bool {
if matches!(
event_type,
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
) {
warn!("Tap Disabled, re-enabling");
if let Some(port) = &self.tap_port {
CGEvent::tap_enable(port, true);
}
return false;
}
let Some(events) = &self.events else {
return false;
};
let flags = CGEvent::flags(Some(event));
let modifiers = get_modifiers(flags);
let result = match event_type {
CGEventType::LeftMouseDown | CGEventType::RightMouseDown => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseDown { point, modifiers })
}
CGEventType::LeftMouseUp | CGEventType::RightMouseUp => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseUp { point, modifiers })
}
CGEventType::LeftMouseDragged | CGEventType::RightMouseDragged => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseDragged { point, modifiers })
}
CGEventType::MouseMoved => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseMoved { point, modifiers })
}
CGEventType::KeyDown => {
let keycode =
CGEvent::integer_value_field(Some(event), CGEventField::KeyboardEventKeycode);
return self.handle_keypress(keycode, flags);
}
CGEventType::ScrollWheel => {
return self.handle_scroll_wheel(event);
}
_ => {
return self.handle_swipe(event);
}
};
if let Err(err) = result {
error!("error sending event: {err}");
self.events = None;
}
false
}
fn handle_scroll_wheel(&mut self, event: &CGEvent) -> bool {
if self
.last_swipe_time
.is_some_and(|t| t.elapsed() < VERTICAL_GESTURE_SCROLL_SUPPRESS)
{
return true;
}
let flags = CGEvent::flags(Some(event));
let modifiers = get_scroll_modifiers(flags);
let target_modifier = self.config.swipe_scroll_modifier();
let vertical_mod = self.config.swipe_scroll_vertical_modifier();
let base_match = target_modifier.matches(modifiers);
let combined_match =
vertical_mod.is_some_and(|vm| (target_modifier | vm).matches(modifiers));
if !combined_match && !base_match {
return false;
}
if let Some(events) = &self.events {
let h_delta = CGEvent::double_value_field(
Some(event),
CGEventField::ScrollWheelEventFixedPtDeltaAxis2,
);
let v_delta = CGEvent::double_value_field(
Some(event),
CGEventField::ScrollWheelEventFixedPtDeltaAxis1,
);
if combined_match && v_delta.abs() > 0.001 {
_ = events.send(Event::VerticalScrollTick { delta: v_delta });
return true;
}
let delta = if h_delta.abs() > 0.001 {
h_delta
} else if v_delta.abs() > 0.001 {
v_delta
} else {
0.0
};
if delta.abs() > 0.001 {
if self.config.swipe_scroll_window_step() {
let direction = scroll_step_direction(&self.config, delta);
let momentum = CGEvent::integer_value_field(
Some(event),
CGEventField::ScrollWheelEventMomentumPhase,
);
if accept_window_scroll(
&mut self.last_window_scroll,
&direction,
momentum != 0,
Instant::now(),
) {
_ = events.send(Event::Command {
command: Command::Window(Operation::Focus(direction)),
});
}
} else {
_ = events.send(Event::Scroll { delta });
}
return true; }
}
false
}
fn handle_swipe(&mut self, event: &CGEvent) -> bool {
const NS_EVENT_PHASE_ENDED: usize = 1 << 3; const NS_EVENT_PHASE_CANCELLED: usize = 1 << 4;
let Some(configured_fingers) = self
.config
.swipe_gesture_fingers()
.filter(|fingers| *fingers >= GESTURE_MINIMAL_FINGERS)
else {
return false;
};
objc2::rc::autoreleasepool(|_| {
let Some(ns_event) = NSEvent::eventWithCGEvent(event) else {
error!("{}: Unable to convert CGEvent to NSEvent", function_name!());
return false;
};
if ns_event.r#type() != NSEventType::Gesture {
return false;
}
let phase = ns_event.phase();
if (phase.0 & NS_EVENT_PHASE_CANCELLED != 0 || phase.0 & NS_EVENT_PHASE_ENDED != 0)
&& let Some(events) = &self.events
{
_ = events.send(Event::TouchpadUp);
return false;
}
let fingers = Touch::sample(&ns_event.allTouches());
if !gesture_should_intercept(Some(configured_fingers), fingers.len()) {
return false;
}
if fingers.iter().any(|finger| finger.began) {
if let Some(events) = &self.events {
_ = events.send(Event::TouchpadDown);
}
}
if fingers.len() < GESTURE_MINIMAL_FINGERS {
return false;
}
if fingers.iter().all(|finger| !finger.began)
&& let Some(prev) = &self.finger_position
{
let (x_deltas, y_deltas): (Vec<f64>, Vec<f64>) = fingers
.iter()
.filter_map(|current| {
prev.iter()
.find(|previous| previous.is(current))
.map(|previous| (previous.x - current.x, previous.y - current.y))
})
.unzip();
if let Some(events) = &self.events {
let x_sum: f64 = x_deltas.iter().sum();
let y_sum: f64 = y_deltas.iter().sum();
if x_sum.abs() >= y_sum.abs() {
if x_deltas.iter().all(|p| p.abs() > SWIPE_THRESHOLD) {
_ = events.send(Event::Swipe {
delta: x_sum,
fingers: x_deltas.len(),
});
self.last_swipe_time = Some(Instant::now());
}
} else if y_deltas.iter().all(|p| p.abs() > SWIPE_THRESHOLD) {
if !self.config.swipe_vertical() {
return false;
}
_ = events.send(Event::VerticalSwipe {
delta: y_sum,
fingers: y_deltas.len(),
});
self.last_swipe_time = Some(Instant::now());
}
}
}
self.finger_position = Some(fingers);
true
})
}
fn handle_keypress(&self, keycode: i64, eventflags: CGEventFlags) -> bool {
let Some(events) = &self.events else {
return false;
};
let mask = get_modifiers(eventflags);
let keycode = keycode.try_into().ok();
keycode
.and_then(|keycode| {
let passthrough = FOCUSED_PASSTHROUGH.load();
if passthrough
.iter()
.any(|(c, m)| *c == keycode && m.matches(mask))
{
return None;
}
let lua_binds = LUA_KEYBINDS.load();
if let Some((_, _, id)) = lua_binds
.iter()
.find(|(c, m, _)| *c == keycode && m.matches(mask))
{
return Some(Command::Lua(*id));
}
self.config.find_keybind(keycode, mask)
})
.and_then(|command| {
events
.send(Event::Command { command })
.inspect_err(|err| error!("Error sending command: {err}"))
.ok()
})
.is_some()
}
}
fn gesture_should_intercept(configured_fingers: Option<usize>, actual_fingers: usize) -> bool {
configured_fingers.is_some_and(|configured| {
configured >= GESTURE_MINIMAL_FINGERS && configured == actual_fingers
})
}
fn get_scroll_modifiers(eventflags: CGEventFlags) -> Modifiers {
const GROUP_FLAGS: [(Modifiers, u64); 4] = [
(Modifiers::ALT, CGEventFlags::MaskAlternate.0),
(Modifiers::SHIFT, CGEventFlags::MaskShift.0),
(Modifiers::CMD, CGEventFlags::MaskCommand.0),
(Modifiers::CTRL, CGEventFlags::MaskControl.0),
];
GROUP_FLAGS
.iter()
.fold(get_modifiers(eventflags), |modifiers, (group, flag)| {
if !modifiers.intersects(*group) && eventflags.0 & flag != 0 {
modifiers | *group
} else {
modifiers
}
})
}
fn get_modifiers(eventflags: CGEventFlags) -> Modifiers {
const MODIFIER_MASKS: [(Modifiers, u64); 8] = [
(Modifiers::LALT, 0x0000_0020),
(Modifiers::RALT, 0x0000_0040),
(Modifiers::LSHIFT, 0x0000_0002),
(Modifiers::RSHIFT, 0x0000_0004),
(Modifiers::LCMD, 0x0000_0008),
(Modifiers::RCMD, 0x0000_0010),
(Modifiers::LCTRL, 0x0000_0001),
(Modifiers::RCTRL, 0x0000_2000),
];
if eventflags.0 == NX_DEVICEFNKEYMASK {
tracing::debug!("event flags {:#x}", eventflags.0);
return Modifiers::FN;
}
MODIFIER_MASKS
.iter()
.fold(Modifiers::empty(), |modifiers, (modifier, mask)| {
if eventflags.0 & mask != 0 {
modifiers | *modifier
} else {
modifiers
}
})
}
fn scroll_step_direction(config: &Config, delta: f64) -> Direction {
let natural = matches!(
config.swipe_gesture_direction(),
SwipeGestureDirection::Natural
);
if (delta > 0.0) == natural {
Direction::East
} else {
Direction::West
}
}
fn accept_window_scroll(
last: &mut Option<(Instant, Direction)>,
direction: &Direction,
momentum: bool,
now: Instant,
) -> bool {
if momentum
|| last.as_ref().is_some_and(|(time, previous)| {
previous == direction && now.duration_since(*time) < WINDOW_SCROLL_INTERVAL
})
{
return false;
}
*last = Some((now, direction.clone()));
true
}
#[cfg(test)]
mod tests {
use super::*;
fn step_config(extra: &str) -> Config {
Config::try_from(format!(
"[options]\n[bindings]\n[swipe.scroll]\nmodifier = 'alt'\nwindow_step = true\n{extra}"
).as_str()).expect("valid test config")
}
#[test]
fn wheel_steps_use_delta_sign_and_configured_direction() {
for reversed in [false, true] {
let config = step_config(if reversed {
"[swipe.gesture]\ndirection = 'Reversed'\n"
} else {
""
});
for delta in [0.1, 1.0, 3.0, 120.0] {
for signed_delta in [delta, -delta] {
let direction = scroll_step_direction(&config, signed_delta);
let expected = if (signed_delta > 0.0) == reversed {
Direction::West
} else {
Direction::East
};
assert_eq!(direction, expected);
}
}
}
}
#[test]
fn wheel_scroll_stays_free_unless_step_mode_is_enabled() {
assert!(!Config::default().swipe_scroll_window_step());
let disabled = Config::try_from("[swipe.scroll]\nwindow_step = false\n").unwrap();
assert!(!disabled.swipe_scroll_window_step());
let enabled = Config::try_from("[swipe.scroll]\nwindow_step = true\n").unwrap();
assert!(enabled.swipe_scroll_window_step());
}
#[test]
fn wheel_steps_limit_repeats_but_allow_immediate_reversal() {
let now = Instant::now();
let mut last = None;
assert!(accept_window_scroll(
&mut last,
&Direction::East,
false,
now
));
assert!(!accept_window_scroll(
&mut last,
&Direction::East,
false,
now + Duration::from_millis(100)
));
assert!(accept_window_scroll(
&mut last,
&Direction::East,
false,
now + WINDOW_SCROLL_INTERVAL
));
assert!(accept_window_scroll(
&mut last,
&Direction::West,
false,
now + WINDOW_SCROLL_INTERVAL
));
}
#[test]
fn wheel_momentum_does_not_select_a_window_or_delay_the_next_input() {
let now = Instant::now();
let mut last = None;
assert!(!accept_window_scroll(
&mut last,
&Direction::East,
true,
now
));
assert!(last.is_none());
assert!(accept_window_scroll(
&mut last,
&Direction::East,
false,
now
));
assert!(!accept_window_scroll(
&mut last,
&Direction::West,
true,
now
));
assert!(accept_window_scroll(
&mut last,
&Direction::East,
false,
now + WINDOW_SCROLL_INTERVAL
));
}
#[test]
fn wheel_steps_focus_adjacent_windows_and_stop_at_strip_edges() {
use crate::tests::TestHarness;
let config = step_config("");
let mut events = vec![Event::MenuOpened { window_id: 0 }];
events.extend(
[120.0, 3.0, 3.0, -120.0, -3.0, -3.0].map(|delta| Event::Command {
command: Command::Window(Operation::Focus(scroll_step_direction(&config, delta))),
}),
);
let mut harness = TestHarness::new().with_config(config).with_windows(3);
for (iteration, expected) in [0, 1, 2, 2, 1, 0, 0].into_iter().enumerate() {
harness = harness.on_iteration(iteration, move |world, _| {
crate::assert_focused!(world, expected);
assert_eq!(
world.query::<&crate::ecs::Scrolling>().iter(world).count(),
0
);
});
}
harness.run(events);
}
const NX_DEVICELALTKEYMASK: u64 = 0x0000_0020;
const NX_DEVICERALTKEYMASK: u64 = 0x0000_0040;
const NX_DEVICELSHIFTKEYMASK: u64 = 0x0000_0002;
const NX_DEVICERSHIFTKEYMASK: u64 = 0x0000_0004;
const NX_DEVICELCMDKEYMASK: u64 = 0x0000_0008;
const NX_DEVICERCMDKEYMASK: u64 = 0x0000_0010;
const NX_DEVICELCTLKEYMASK: u64 = 0x0000_0001;
const NX_DEVICERCTLKEYMASK: u64 = 0x0000_2000;
#[test]
fn no_modifiers() {
assert_eq!(get_modifiers(CGEventFlags(0)), Modifiers::empty());
}
#[test]
fn fn_modifier() {
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICEFNKEYMASK)),
Modifiers::FN
);
}
#[test]
fn single_left_modifier() {
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICELALTKEYMASK)),
Modifiers::LALT
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICELSHIFTKEYMASK)),
Modifiers::LSHIFT
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICELCMDKEYMASK)),
Modifiers::LCMD
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICELCTLKEYMASK)),
Modifiers::LCTRL
);
}
#[test]
fn single_right_modifier() {
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICERALTKEYMASK)),
Modifiers::RALT
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICERSHIFTKEYMASK)),
Modifiers::RSHIFT
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICERCMDKEYMASK)),
Modifiers::RCMD
);
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICERCTLKEYMASK)),
Modifiers::RCTRL
);
}
#[test]
fn both_sides_of_same_modifier() {
let flags = NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK;
assert_eq!(get_modifiers(CGEventFlags(flags)), Modifiers::ALT);
}
#[test]
fn multiple_different_modifiers() {
let flags = NX_DEVICELCTLKEYMASK | NX_DEVICELALTKEYMASK;
assert_eq!(
get_modifiers(CGEventFlags(flags)),
Modifiers::LCTRL | Modifiers::LALT
);
}
#[test]
fn mixed_sides_across_groups() {
let flags = NX_DEVICELCMDKEYMASK | NX_DEVICERALTKEYMASK | NX_DEVICERSHIFTKEYMASK;
assert_eq!(
get_modifiers(CGEventFlags(flags)),
Modifiers::LCMD | Modifiers::RALT | Modifiers::RSHIFT
);
}
#[test]
fn device_independent_flags_ignored() {
let generic_alt: u64 = 0x0008_0000;
assert_eq!(get_modifiers(CGEventFlags(generic_alt)), Modifiers::empty());
}
#[test]
fn shared_mouse_alt_scroll_matches_generic_binding() {
let modifiers = get_scroll_modifiers(CGEventFlags(0x2008_0000));
assert!(Modifiers::ALT.matches(modifiers));
assert!(!Modifiers::LALT.matches(modifiers));
assert!(!Modifiers::RALT.matches(modifiers));
}
#[test]
fn shared_mouse_extra_modifier_does_not_trigger_alt_scroll() {
let modifiers = get_scroll_modifiers(CGEventFlags(0x200c_0000));
assert!(!Modifiers::ALT.matches(modifiers));
assert!((Modifiers::ALT | Modifiers::CTRL).matches(modifiers));
}
#[test]
fn physical_scroll_keeps_the_reported_modifier_side() {
let modifiers = get_scroll_modifiers(CGEventFlags(0x0008_0000 | NX_DEVICERALTKEYMASK));
assert!(Modifiers::ALT.matches(modifiers));
assert!(Modifiers::RALT.matches(modifiers));
assert!(!Modifiers::LALT.matches(modifiers));
}
#[test]
fn ordinary_scroll_does_not_trigger_alt_scroll() {
assert!(!Modifiers::ALT.matches(get_scroll_modifiers(CGEventFlags(0x2000_0000))));
}
#[test]
fn shared_mouse_scroll_supports_other_modifier_groups() {
for (group, flag) in [
(Modifiers::SHIFT, 0x0002_0000),
(Modifiers::CTRL, 0x0004_0000),
(Modifiers::CMD, 0x0010_0000),
] {
assert!(group.matches(get_scroll_modifiers(CGEventFlags(flag))));
}
}
#[test]
fn secondary_fn_flag_is_not_ignored() {
assert_eq!(
get_modifiers(CGEventFlags(NX_DEVICEFNKEYMASK)),
Modifiers::FN
);
}
#[test]
fn only_intercepts_the_explicitly_configured_gesture() {
assert!(!gesture_should_intercept(None, 3));
assert!(!gesture_should_intercept(Some(2), 2));
assert!(gesture_should_intercept(Some(3), 3));
assert!(!gesture_should_intercept(Some(4), 3));
assert!(gesture_should_intercept(Some(4), 4));
}
}