XI2 Smooth Scrolling for X11 - Attempt 2 (#11110)

Owen Law created

This should have fixed the problems that some users were reporting with
https://github.com/zed-industries/zed/pull/10695 .

The problem was with devices which send more than one valuator axis in a
single event whereas the original PR assumed there would only ever be
one axis per event. This version also does away with the complicated
device selection and instead just uses the master pointer device, which
automatically uses all sub-pointers.

Edit: Confirmed working for one of the user's which the first attempt
was broken for.

Release Notes:

- Added smooth scrolling for X11 on Linux
- Added horizontal scrolling for X11 on Linux

Change summary

crates/gpui/Cargo.toml                       |   2 
crates/gpui/src/platform/linux/x11/client.rs | 140 +++++++++++++++++++--
crates/gpui/src/platform/linux/x11/event.rs  |  16 ++
crates/gpui/src/platform/linux/x11/window.rs |  19 ++
4 files changed, 157 insertions(+), 20 deletions(-)

Detailed changes

crates/gpui/Cargo.toml 🔗

@@ -114,7 +114,7 @@ wayland-protocols = { version = "0.31.2", features = [
 oo7 = "0.3.0"
 open = "5.1.2"
 filedescriptor = "0.8.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, ScrollClass};
 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;
@@ -22,8 +23,9 @@ use xkbcommon::xkb as xkbc;
 use crate::platform::linux::LinuxClient;
 use crate::platform::{LinuxCommon, PlatformWindow};
 use crate::{
-    px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
-    PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
+    modifiers_from_xinput_info, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers,
+    ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta, Size,
+    TouchPhase, WindowParams, X11Window,
 };
 
 use super::{super::SCROLL_LINES, X11Display, X11WindowStatePtr, XcbAtoms};
@@ -63,6 +65,10 @@ pub struct X11ClientState {
     pub(crate) focused_window: Option<xproto::Window>,
     pub(crate) xkb: xkbc::State,
 
+    pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
+    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>,
@@ -110,6 +116,35 @@ 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 master_device_query = xcb_connection
+            .xinput_xi_query_device(1_u16)
+            .unwrap()
+            .reply()
+            .unwrap();
+        let scroll_class_data = master_device_query
+            .infos
+            .iter()
+            .find(|info| info.type_ == xinput::DeviceType::MASTER_POINTER)
+            .unwrap()
+            .classes
+            .iter()
+            .filter_map(|class| class.data.as_scroll())
+            .map(|class| *class)
+            .collect::<Vec<_>>();
 
         let atoms = XcbAtoms::new(&xcb_connection).unwrap();
         let xkb = xcb_connection
@@ -184,6 +219,11 @@ impl X11Client {
             windows: HashMap::default(),
             focused_window: None,
             xkb: xkb_state,
+
+            scroll_class_data,
+            scroll_x: None,
+            scroll_y: None,
+
             clipboard,
             primary,
         })))
@@ -330,18 +370,6 @@ 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:?}");
                 }
@@ -363,6 +391,84 @@ impl X11Client {
                     }));
                 }
             }
+            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 modifiers = modifiers_from_xinput_info(event.mods);
+
+                let axisvalues = event
+                    .axisvalues
+                    .iter()
+                    .map(|axisvalue| fp3232_to_f32(*axisvalue))
+                    .collect::<Vec<_>>();
+
+                if event.valuator_mask[0] & 3 != 0 {
+                    window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
+                        position,
+                        pressed_button: None,
+                        modifiers,
+                    }));
+                }
+
+                let mut valuator_idx = 0;
+                let scroll_class_data = self.0.borrow().scroll_class_data.clone();
+                for shift in 0..32 {
+                    if (event.valuator_mask[0] >> shift) & 1 == 0 {
+                        continue;
+                    }
+
+                    for scroll_class in &scroll_class_data {
+                        if scroll_class.scroll_type == xinput::ScrollType::HORIZONTAL
+                            && scroll_class.number == shift
+                        {
+                            let new_scroll = axisvalues[valuator_idx]
+                                / fp3232_to_f32(scroll_class.increment)
+                                * SCROLL_LINES as f32;
+                            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;
+                                window.handle_input(PlatformInput::ScrollWheel(
+                                    crate::ScrollWheelEvent {
+                                        position,
+                                        delta: ScrollDelta::Lines(Point::new(delta_scroll, 0.0)),
+                                        modifiers,
+                                        touch_phase: TouchPhase::default(),
+                                    },
+                                ));
+                            }
+                        } else if scroll_class.scroll_type == xinput::ScrollType::VERTICAL
+                            && scroll_class.number == shift
+                        {
+                            // the `increment` is the valuator delta equivalent to one positive unit of scrolling. Here that means SCROLL_LINES lines.
+                            let new_scroll = axisvalues[valuator_idx]
+                                / fp3232_to_f32(scroll_class.increment)
+                                * SCROLL_LINES as f32;
+                            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;
+                                window.handle_input(PlatformInput::ScrollWheel(
+                                    crate::ScrollWheelEvent {
+                                        position,
+                                        delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
+                                        modifiers,
+                                        touch_phase: TouchPhase::default(),
+                                    },
+                                ));
+                            }
+                        }
+                    }
+
+                    valuator_idx += 1;
+                }
+            }
             Event::MotionNotify(event) => {
                 let window = self.get_window(event.event)?;
                 let pressed_button = super::button_from_state(event.state);
@@ -573,3 +679,7 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
     log::info!("Refreshing at {} micros", micros);
     Duration::from_micros(micros)
 }
+
+fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
+    value.integral as f32 + value.frac as f32 / u32::MAX as f32
+}

crates/gpui/src/platform/linux/x11/event.rs 🔗

@@ -1,4 +1,7 @@
-use x11rb::protocol::xproto;
+use x11rb::protocol::{
+    xinput,
+    xproto::{self, ModMask},
+};
 
 use crate::{Modifiers, MouseButton, NavigationDirection};
 
@@ -23,6 +26,17 @@ pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
     }
 }
 
+pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers {
+    Modifiers {
+        control: modifier_info.effective as u16 & ModMask::CONTROL.bits()
+            == ModMask::CONTROL.bits(),
+        alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(),
+        shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(),
+        platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(),
+        function: false,
+    }
+}
+
 pub(crate) fn button_from_state(state: xproto::KeyButMask) -> Option<MouseButton> {
     Some(if state.contains(xproto::KeyButMask::BUTTON1) {
         MouseButton::Left

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,
 };
@@ -153,8 +156,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,
         );
 
@@ -174,6 +175,18 @@ impl X11WindowState {
             )
             .unwrap();
 
+        xinput::ConnectionExt::xinput_xi_select_events(
+            &xcb_connection,
+            x_window,
+            &[xinput::EventMask {
+                deviceid: 1,
+                mask: vec![xinput::XIEventMask::MOTION],
+            }],
+        )
+        .unwrap()
+        .check()
+        .unwrap();
+
         if let Some(titlebar) = params.titlebar {
             if let Some(title) = titlebar.title {
                 xcb_connection