From 98db7fa61eeb9e5c109f2ff0b2ff5baf7cfe3c6d Mon Sep 17 00:00:00 2001 From: Owen Law <81528246+someone13574@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:44:21 -0400 Subject: [PATCH] Use XI2 for Scrolling on X11 (#10695) Changes the X11 platform code to use the xinput extension which allows for smooth scrolling and horizontal scrolling. Release Notes: - Added smooth scrolling to X11 on Linux - Added horizontal scrolling to X11 on Linux --- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 126 +++++++++++++++++-- crates/gpui/src/platform/linux/x11/window.rs | 24 +++- 3 files changed, 135 insertions(+), 17 deletions(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 471e0a7347f8b44e564a47a3d7369ba99fffd1bc..021b4bcb9a8dcf5ac2f007271dd273be5e6fafd9 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -115,7 +115,7 @@ wayland-protocols = { version = "0.31.2", features = [ ] } oo7 = "0.3.0" open = "5.1.2" -x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] } +x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr", "xinput"] } xkbcommon = { version = "0.7", features = ["wayland", "x11"] } [target.'cfg(windows)'.dependencies] diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 908d2ea750e09930509a20b588dce23ce4270bde..ae1723f30cc97b23446ade2aabac473a24c188f4 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -12,9 +12,10 @@ use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; use x11rb::errors::ConnectionError; use x11rb::protocol::randr::ConnectionExt as _; +use x11rb::protocol::xinput::ConnectionExt; use x11rb::protocol::xkb::ConnectionExt as _; use x11rb::protocol::xproto::ConnectionExt as _; -use x11rb::protocol::{randr, xkb, xproto, Event}; +use x11rb::protocol::{randr, xinput, xkb, xproto, Event}; use x11rb::xcb_ffi::XCBConnection; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; use xkbcommon::xkb as xkbc; @@ -63,6 +64,10 @@ pub struct X11ClientState { pub(crate) focused_window: Option, pub(crate) xkb: xkbc::State, + pub(crate) scroll_devices: Vec, + pub(crate) scroll_x: Option, + pub(crate) scroll_y: Option, + pub(crate) common: LinuxCommon, pub(crate) clipboard: X11ClipboardContext, pub(crate) primary: X11ClipboardContext, @@ -92,6 +97,19 @@ impl X11Client { xcb_connection .prefetch_extension_information(randr::X11_EXTENSION_NAME) .unwrap(); + xcb_connection + .prefetch_extension_information(xinput::X11_EXTENSION_NAME) + .unwrap(); + + let xinput_version = xcb_connection + .xinput_xi_query_version(2, 0) + .unwrap() + .reply() + .unwrap(); + assert!( + xinput_version.major_version >= 2, + "XInput Extension v2 not supported." + ); let atoms = XcbAtoms::new(&xcb_connection).unwrap(); let xkb = xcb_connection @@ -125,6 +143,46 @@ impl X11Client { xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id) }; + let device_list = xcb_connection + .xinput_list_input_devices() + .unwrap() + .reply() + .unwrap(); + let scroll_devices = device_list + .devices + .iter() + .scan(0, |class_info_idx, device_info| { + Some(*class_info_idx + device_info.num_class_info as usize) + }) + .zip(device_list.devices.iter()) + .map(|(class_info_idx, device_info)| { + ( + device_info, + device_list.infos + [(class_info_idx - device_info.num_class_info as usize)..(class_info_idx)] + .to_vec(), + ) + }) + .filter(|(device_info, class_info)| { + device_info.device_use == xinput::DeviceUse::IS_X_EXTENSION_POINTER + && class_info.iter().any(|class_info| match class_info.info { + xinput::InputInfoInfo::Valuator(xinput::InputInfoInfoValuator { + mode, + .. + }) => mode == xinput::ValuatorMode::RELATIVE, + _ => false, + }) + }) + .map(|(device_info, _)| *device_info) + .collect::>(); + for device in &scroll_devices { + xcb_connection + .xinput_open_device(device.device_id) + .unwrap() + .reply() + .unwrap(); + } + let clipboard = X11ClipboardContext::::new().unwrap(); let primary = X11ClipboardContext::::new().unwrap(); @@ -166,6 +224,11 @@ impl X11Client { windows: HashMap::default(), focused_window: None, xkb: xkb_state, + + scroll_devices, + scroll_x: None, + scroll_y: None, + clipboard, primary, }))) @@ -314,22 +377,58 @@ impl X11Client { click_count: current_count, first_mouse: false, })); - } else if event.detail >= 4 && event.detail <= 5 { - // https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11 - let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 }; - let scroll_y = SCROLL_LINES * scroll_direction; - - drop(state); - window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent { - position, - delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)), - modifiers, - touch_phase: TouchPhase::Moved, - })); } else { log::warn!("Unknown button press: {event:?}"); } } + Event::XinputMotion(event) => { + let window = self.get_window(event.event)?; + + let position = Point::new( + (event.event_x as f32 / u16::MAX as f32).into(), + (event.event_y as f32 / u16::MAX as f32).into(), + ); + + let axisvalues = event + .axisvalues + .iter() + .map(|axisvalue| { + axisvalue.integral as f32 + axisvalue.frac as f32 / u32::MAX as f32 + }) + .collect::>(); + + if event.valuator_mask[0] & 4 == 4 { + let new_scroll = axisvalues[0]; + let old_scroll = self.0.borrow().scroll_x; + self.0.borrow_mut().scroll_x = Some(new_scroll); + + if let Some(old_scroll) = old_scroll { + let delta_scroll = (old_scroll - new_scroll).into(); + window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(Point::new(delta_scroll, 0.0.into())), + modifiers: crate::Modifiers::none(), + touch_phase: TouchPhase::default(), + })); + } + } + + if event.valuator_mask[0] & 8 == 8 { + let new_scroll = axisvalues[0] / 2.0; + let old_scroll = self.0.borrow().scroll_y; + self.0.borrow_mut().scroll_y = Some(new_scroll); + + if let Some(old_scroll) = old_scroll { + let delta_scroll = (old_scroll - new_scroll).into(); + window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(Point::new(0.0.into(), delta_scroll)), + modifiers: crate::Modifiers::none(), + touch_phase: TouchPhase::default(), + })); + } + } + } Event::ButtonRelease(event) => { let window = self.get_window(event.event)?; let state = self.0.borrow(); @@ -429,6 +528,7 @@ impl LinuxClient for X11Client { state.x_root_index, x_window, &state.atoms, + &state.scroll_devices, ); let screen_resources = state diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 884011a376a2e04dfad3bf24e764f9e09a0ae057..12b635d372e99020cdf38772824fd763643ff74a 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -13,7 +13,10 @@ use raw_window_handle as rwh; use util::ResultExt; use x11rb::{ connection::Connection, - protocol::xproto::{self, ConnectionExt as _, CreateWindowAux}, + protocol::{ + xinput, + xproto::{self, ConnectionExt as _, CreateWindowAux}, + }, wrapper::ConnectionExt, xcb_ffi::XCBConnection, }; @@ -140,6 +143,7 @@ impl X11WindowState { x_main_screen_index: usize, x_window: xproto::Window, atoms: &XcbAtoms, + scroll_devices: &Vec, ) -> Self { let x_screen_index = params .display_id @@ -160,8 +164,6 @@ impl X11WindowState { | xproto::EventMask::BUTTON1_MOTION | xproto::EventMask::BUTTON2_MOTION | xproto::EventMask::BUTTON3_MOTION - | xproto::EventMask::BUTTON4_MOTION - | xproto::EventMask::BUTTON5_MOTION | xproto::EventMask::BUTTON_MOTION, ); @@ -181,6 +183,20 @@ impl X11WindowState { ) .unwrap(); + for device in scroll_devices { + xinput::ConnectionExt::xinput_xi_select_events( + &xcb_connection, + x_window, + &[xinput::EventMask { + deviceid: device.device_id as u16, + mask: vec![xinput::XIEventMask::MOTION], + }], + ) + .unwrap() + .check() + .unwrap(); + } + if let Some(titlebar) = params.titlebar { if let Some(title) = titlebar.title { xcb_connection @@ -262,6 +278,7 @@ impl X11Window { x_main_screen_index: usize, x_window: xproto::Window, atoms: &XcbAtoms, + scroll_devices: &Vec, ) -> Self { X11Window { state: Rc::new(RefCell::new(X11WindowState::new( @@ -270,6 +287,7 @@ impl X11Window { x_main_screen_index, x_window, atoms, + scroll_devices, ))), callbacks: Rc::new(RefCell::new(Callbacks::default())), xcb_connection: xcb_connection.clone(),