Use XI2 for Scrolling on X11 (#10695)

Owen Law created

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

Change summary

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(-)

Detailed changes

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]

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<xproto::Window>,
     pub(crate) xkb: xkbc::State,
 
+    pub(crate) scroll_devices: Vec<xinput::DeviceInfo>,
+    pub(crate) scroll_x: Option<f32>,
+    pub(crate) scroll_y: Option<f32>,
+
     pub(crate) common: LinuxCommon,
     pub(crate) clipboard: X11ClipboardContext<Clipboard>,
     pub(crate) primary: X11ClipboardContext<Primary>,
@@ -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::<Vec<_>>();
+        for device in &scroll_devices {
+            xcb_connection
+                .xinput_open_device(device.device_id)
+                .unwrap()
+                .reply()
+                .unwrap();
+        }
+
         let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
         let primary = X11ClipboardContext::<Primary>::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::<Vec<_>>();
+
+                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

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<xinput::DeviceInfo>,
     ) -> 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<xinput::DeviceInfo>,
     ) -> 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(),