linux: Various X11 scroll improvements (#18484)

Michael Sloan and Thorsten Ball created

Closes  #14089, #14416, #15970, #17230, #18485

Release Notes:

- Fixed some cases where Linux X11 mouse scrolling doesn't work at all
(#14089, ##15970, #17230)
- Fixed handling of switching between Linux X11 devices used for
scrolling (#14416, #18485)

Change details:

Also includes the commit from PR #18317 so I don't have to deal with
merge conflicts.

* Now uses valuator info from slave pointers rather than master. This
hopefully fixes remaining cases where scrolling is fully
broken. https://github.com/zed-industries/zed/issues/14089,
https://github.com/zed-industries/zed/issues/15970,
https://github.com/zed-industries/zed/issues/17230

* Per-device recording of "last scroll position" used to calculate
deltas. This meant that swithing scroll devices would cause a sudden
jump of scroll position, often to the beginning or end of the
file (https://github.com/zed-industries/zed/issues/14416).

* Re-queries device metadata when devices change, so that newly
plugged in devices will work, and re-use of device-ids don't use old
metadata with a new device.

* xinput 2 documentation describes support for multiple master
devices. I believe this implementation will support that, since now it
just uses `DeviceInfo` from slave devices. The concept of master
devices is only used in registering for events.

* Uses popcount+bit masking to resolve axis indexes, instead of
iterating bit indices.

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

crates/gpui/src/platform/linux/platform.rs       |   2 
crates/gpui/src/platform/linux/wayland/client.rs |   8 
crates/gpui/src/platform/linux/x11/client.rs     | 406 ++++++++++++-----
crates/gpui/src/platform/linux/x11/event.rs      | 116 ++++
crates/gpui/src/platform/linux/x11/window.rs     |  19 
5 files changed, 408 insertions(+), 143 deletions(-)

Detailed changes

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

@@ -45,7 +45,7 @@ use crate::{
 
 use super::x11::X11Client;
 
-pub(crate) const SCROLL_LINES: f64 = 3.0;
+pub(crate) const SCROLL_LINES: f32 = 3.0;
 
 // Values match the defaults on GTK.
 // Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -1634,10 +1634,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
                 match axis {
                     wl_pointer::Axis::VerticalScroll => {
-                        scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
+                        scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES;
                     }
                     wl_pointer::Axis::HorizontalScroll => {
-                        scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
+                        scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES;
                     }
                     _ => unreachable!(),
                 }
@@ -1662,10 +1662,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 let wheel_percent = value120 as f32 / 120.0;
                 match axis {
                     wl_pointer::Axis::VerticalScroll => {
-                        scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES as f32;
+                        scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES;
                     }
                     wl_pointer::Axis::HorizontalScroll => {
-                        scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES as f32;
+                        scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES;
                     }
                     _ => unreachable!(),
                 }

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

@@ -1,6 +1,6 @@
 use core::str;
 use std::cell::RefCell;
-use std::collections::HashSet;
+use std::collections::{BTreeMap, HashSet};
 use std::ops::Deref;
 use std::path::PathBuf;
 use std::rc::{Rc, Weak};
@@ -42,7 +42,10 @@ use crate::{
     WindowParams, X11Window,
 };
 
-use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
+use super::{
+    button_or_scroll_from_event_detail, get_valuator_axis_index, modifiers_from_state,
+    pressed_button_from_mask, ButtonOrScroll, ScrollDirection,
+};
 use super::{X11Display, X11WindowStatePtr, XcbAtoms};
 use super::{XimCallbackEvent, XimHandler};
 use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
@@ -51,7 +54,15 @@ use crate::platform::linux::{
     get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
 };
 
-pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
+/// Value for DeviceId parameters which selects all devices.
+pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0;
+
+/// Value for DeviceId parameters which selects all device groups. Events that
+/// occur within the group are emitted by the group itself.
+///
+/// In XInput 2's interface, these are referred to as "master devices", but that
+/// terminology is both archaic and unclear.
+pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1;
 
 pub(crate) struct WindowRef {
     window: X11WindowStatePtr,
@@ -117,6 +128,26 @@ pub struct Xdnd {
     position: Point<Pixels>,
 }
 
+#[derive(Debug)]
+struct PointerDeviceState {
+    horizontal: ScrollAxisState,
+    vertical: ScrollAxisState,
+}
+
+#[derive(Debug, Default)]
+struct ScrollAxisState {
+    /// Valuator number for looking up this axis's scroll value.
+    valuator_number: Option<u16>,
+    /// Conversion factor from scroll units to lines.
+    multiplier: f32,
+    /// Last scroll value for calculating scroll delta.
+    ///
+    /// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes.
+    /// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event.
+    /// The consequence of not invalidating it can be large invalid deltas, which are much more user visible.
+    scroll_value: Option<f32>,
+}
+
 pub struct X11ClientState {
     pub(crate) loop_handle: LoopHandle<'static, X11Client>,
     pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -152,9 +183,7 @@ pub struct X11ClientState {
     pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
     pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
 
-    pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
-    pub(crate) scroll_x: Option<f32>,
-    pub(crate) scroll_y: Option<f32>,
+    pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
 
     pub(crate) common: LinuxCommon,
     pub(crate) clipboard: x11_clipboard::Clipboard,
@@ -266,31 +295,21 @@ impl X11Client {
             .prefetch_extension_information(xinput::X11_EXTENSION_NAME)
             .unwrap();
 
+        // Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and
+        // beyond, support for touch events would need to be added.
         let xinput_version = xcb_connection
-            .xinput_xi_query_version(2, 0)
+            .xinput_xi_query_version(2, 1)
             .unwrap()
             .reply()
             .unwrap();
+        // XInput 1.x is not supported.
         assert!(
             xinput_version.major_version >= 2,
-            "XInput Extension v2 not supported."
+            "XInput version >= 2 required."
         );
 
-        let master_device_query = xcb_connection
-            .xinput_xi_query_device(XINPUT_MASTER_DEVICE)
-            .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 pointer_device_states =
+            get_new_pointer_device_states(&xcb_connection, &BTreeMap::new());
 
         let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
 
@@ -434,9 +453,7 @@ impl X11Client {
             cursor_styles: HashMap::default(),
             cursor_cache: HashMap::default(),
 
-            scroll_class_data,
-            scroll_x: None,
-            scroll_y: None,
+            pointer_device_states,
 
             clipboard,
             clipboard_item: None,
@@ -950,35 +967,56 @@ impl X11Client {
                     window.handle_ime_commit(text);
                     state = self.0.borrow_mut();
                 }
-                if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
-                    let click_elapsed = state.last_click.elapsed();
-
-                    if click_elapsed < DOUBLE_CLICK_INTERVAL
-                        && state
-                            .last_mouse_button
-                            .is_some_and(|prev_button| prev_button == button)
-                        && is_within_click_distance(state.last_location, position)
-                    {
-                        state.current_count += 1;
-                    } else {
-                        state.current_count = 1;
-                    }
-
-                    state.last_click = Instant::now();
-                    state.last_mouse_button = Some(button);
-                    state.last_location = position;
-                    let current_count = state.current_count;
+                match button_or_scroll_from_event_detail(event.detail) {
+                    Some(ButtonOrScroll::Button(button)) => {
+                        let click_elapsed = state.last_click.elapsed();
+                        if click_elapsed < DOUBLE_CLICK_INTERVAL
+                            && state
+                                .last_mouse_button
+                                .is_some_and(|prev_button| prev_button == button)
+                            && is_within_click_distance(state.last_location, position)
+                        {
+                            state.current_count += 1;
+                        } else {
+                            state.current_count = 1;
+                        }
 
-                    drop(state);
-                    window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
-                        button,
-                        position,
-                        modifiers,
-                        click_count: current_count,
-                        first_mouse: false,
-                    }));
-                } else {
-                    log::warn!("Unknown button press: {event:?}");
+                        state.last_click = Instant::now();
+                        state.last_mouse_button = Some(button);
+                        state.last_location = position;
+                        let current_count = state.current_count;
+
+                        drop(state);
+                        window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
+                            button,
+                            position,
+                            modifiers,
+                            click_count: current_count,
+                            first_mouse: false,
+                        }));
+                    }
+                    Some(ButtonOrScroll::Scroll(direction)) => {
+                        drop(state);
+                        // Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events.
+                        // Since handling those events does the scrolling, they are skipped here.
+                        if !event
+                            .flags
+                            .contains(xinput::PointerEventFlags::POINTER_EMULATED)
+                        {
+                            let scroll_delta = match direction {
+                                ScrollDirection::Up => Point::new(0.0, SCROLL_LINES),
+                                ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES),
+                                ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0),
+                                ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0),
+                            };
+                            window.handle_input(PlatformInput::ScrollWheel(
+                                make_scroll_wheel_event(position, scroll_delta, modifiers),
+                            ));
+                        }
+                    }
+                    None => {
+                        log::error!("Unknown x11 button: {}", event.detail);
+                    }
                 }
             }
             Event::XinputButtonRelease(event) => {
@@ -991,15 +1029,19 @@ impl X11Client {
                     px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
                     px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
                 );
-                if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
-                    let click_count = state.current_count;
-                    drop(state);
-                    window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
-                        button,
-                        position,
-                        modifiers,
-                        click_count,
-                    }));
+                match button_or_scroll_from_event_detail(event.detail) {
+                    Some(ButtonOrScroll::Button(button)) => {
+                        let click_count = state.current_count;
+                        drop(state);
+                        window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
+                            button,
+                            position,
+                            modifiers,
+                            click_count,
+                        }));
+                    }
+                    Some(ButtonOrScroll::Scroll(_)) => {}
+                    None => {}
                 }
             }
             Event::XinputMotion(event) => {
@@ -1014,12 +1056,6 @@ impl X11Client {
                 state.modifiers = modifiers;
                 drop(state);
 
-                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,
@@ -1028,64 +1064,17 @@ impl X11Client {
                     }));
                 }
 
-                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;
-                                let (x, y) = if !modifiers.shift {
-                                    (0.0, delta_scroll)
-                                } else {
-                                    (delta_scroll, 0.0)
-                                };
-                                window.handle_input(PlatformInput::ScrollWheel(
-                                    crate::ScrollWheelEvent {
-                                        position,
-                                        delta: ScrollDelta::Lines(Point::new(x, y)),
-                                        modifiers,
-                                        touch_phase: TouchPhase::default(),
-                                    },
-                                ));
-                            }
-                        }
+                state = self.0.borrow_mut();
+                if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
+                    let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
+                    drop(state);
+                    if let Some(scroll_delta) = scroll_delta {
+                        window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
+                            position,
+                            scroll_delta,
+                            modifiers,
+                        )));
                     }
-
-                    valuator_idx += 1;
                 }
             }
             Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
@@ -1095,10 +1084,10 @@ impl X11Client {
                 state.mouse_focused_window = Some(event.event);
             }
             Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
-                self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
-                self.0.borrow_mut().scroll_y = None;
-
                 let mut state = self.0.borrow_mut();
+
+                // Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
+                reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states);
                 state.mouse_focused_window = None;
                 let pressed_button = pressed_button_from_mask(event.buttons[0]);
                 let position = point(
@@ -1117,6 +1106,26 @@ impl X11Client {
                 }));
                 window.set_hovered(false);
             }
+            Event::XinputHierarchy(event) => {
+                let mut state = self.0.borrow_mut();
+                // Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values.
+                // Any change to a device invalidates its scroll values.
+                for info in event.infos {
+                    if is_pointer_device(info.type_) {
+                        state.pointer_device_states.remove(&info.deviceid);
+                    }
+                }
+                state.pointer_device_states = get_new_pointer_device_states(
+                    &state.xcb_connection,
+                    &state.pointer_device_states,
+                );
+            }
+            Event::XinputDeviceChanged(event) => {
+                let mut state = self.0.borrow_mut();
+                if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
+                    reset_pointer_device_scroll_positions(&mut pointer);
+                }
+            }
             _ => {}
         };
 
@@ -1742,3 +1751,142 @@ fn xdnd_send_status(
         .send_event(false, target, EventMask::default(), message)
         .unwrap();
 }
+
+/// Recomputes `pointer_device_states` by querying all pointer devices.
+/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used.
+fn get_new_pointer_device_states(
+    xcb_connection: &XCBConnection,
+    scroll_values_to_preserve: &BTreeMap<xinput::DeviceId, PointerDeviceState>,
+) -> BTreeMap<xinput::DeviceId, PointerDeviceState> {
+    let devices_query_result = xcb_connection
+        .xinput_xi_query_device(XINPUT_ALL_DEVICES)
+        .unwrap()
+        .reply()
+        .unwrap();
+
+    let mut pointer_device_states = BTreeMap::new();
+    pointer_device_states.extend(
+        devices_query_result
+            .infos
+            .iter()
+            .filter(|info| is_pointer_device(info.type_))
+            .filter_map(|info| {
+                let scroll_data = info
+                    .classes
+                    .iter()
+                    .filter_map(|class| class.data.as_scroll())
+                    .map(|class| *class)
+                    .rev()
+                    .collect::<Vec<_>>();
+                let old_state = scroll_values_to_preserve.get(&info.deviceid);
+                let old_horizontal = old_state.map(|state| &state.horizontal);
+                let old_vertical = old_state.map(|state| &state.vertical);
+                let horizontal = scroll_data
+                    .iter()
+                    .find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL)
+                    .map(|data| scroll_data_to_axis_state(data, old_horizontal));
+                let vertical = scroll_data
+                    .iter()
+                    .find(|data| data.scroll_type == xinput::ScrollType::VERTICAL)
+                    .map(|data| scroll_data_to_axis_state(data, old_vertical));
+                if horizontal.is_none() && vertical.is_none() {
+                    None
+                } else {
+                    Some((
+                        info.deviceid,
+                        PointerDeviceState {
+                            horizontal: horizontal.unwrap_or_else(Default::default),
+                            vertical: vertical.unwrap_or_else(Default::default),
+                        },
+                    ))
+                }
+            }),
+    );
+    if pointer_device_states.is_empty() {
+        log::error!("Found no xinput mouse pointers.");
+    }
+    return pointer_device_states;
+}
+
+/// Returns true if the device is a pointer device. Does not include pointer device groups.
+fn is_pointer_device(type_: xinput::DeviceType) -> bool {
+    type_ == xinput::DeviceType::SLAVE_POINTER
+}
+
+fn scroll_data_to_axis_state(
+    data: &xinput::DeviceClassDataScroll,
+    old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>,
+) -> ScrollAxisState {
+    ScrollAxisState {
+        valuator_number: Some(data.number),
+        multiplier: SCROLL_LINES / fp3232_to_f32(data.increment),
+        scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value),
+    }
+}
+
+fn reset_all_pointer_device_scroll_positions(
+    pointer_device_states: &mut BTreeMap<xinput::DeviceId, PointerDeviceState>,
+) {
+    pointer_device_states
+        .iter_mut()
+        .for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state));
+}
+
+fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) {
+    pointer.horizontal.scroll_value = None;
+    pointer.vertical.scroll_value = None;
+}
+
+/// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present.
+fn get_scroll_delta_and_update_state(
+    pointer: &mut PointerDeviceState,
+    event: &xinput::MotionEvent,
+) -> Option<Point<f32>> {
+    let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal);
+    let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical);
+    if delta_x.is_some() || delta_y.is_some() {
+        Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0)))
+    } else {
+        None
+    }
+}
+
+fn get_axis_scroll_delta_and_update_state(
+    event: &xinput::MotionEvent,
+    axis: &mut ScrollAxisState,
+) -> Option<f32> {
+    let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?;
+    if let Some(axis_value) = event.axisvalues.get(axis_index) {
+        let new_scroll = fp3232_to_f32(*axis_value);
+        let delta_scroll = axis
+            .scroll_value
+            .map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier);
+        axis.scroll_value = Some(new_scroll);
+        delta_scroll
+    } else {
+        log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly.");
+        None
+    }
+}
+
+fn make_scroll_wheel_event(
+    position: Point<Pixels>,
+    scroll_delta: Point<f32>,
+    modifiers: Modifiers,
+) -> crate::ScrollWheelEvent {
+    // When shift is held down, vertical scrolling turns into horizontal scrolling.
+    let delta = if modifiers.shift {
+        Point {
+            x: scroll_delta.y,
+            y: 0.0,
+        }
+    } else {
+        scroll_delta
+    };
+    crate::ScrollWheelEvent {
+        position,
+        delta: ScrollDelta::Lines(delta),
+        modifiers,
+        touch_phase: TouchPhase::default(),
+    }
+}

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

@@ -5,13 +5,29 @@ use x11rb::protocol::{
 
 use crate::{Modifiers, MouseButton, NavigationDirection};
 
-pub(crate) fn button_of_key(detail: xproto::Button) -> Option<MouseButton> {
+pub(crate) enum ButtonOrScroll {
+    Button(MouseButton),
+    Scroll(ScrollDirection),
+}
+
+pub(crate) enum ScrollDirection {
+    Up,
+    Down,
+    Left,
+    Right,
+}
+
+pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option<ButtonOrScroll> {
     Some(match detail {
-        1 => MouseButton::Left,
-        2 => MouseButton::Middle,
-        3 => MouseButton::Right,
-        8 => MouseButton::Navigate(NavigationDirection::Back),
-        9 => MouseButton::Navigate(NavigationDirection::Forward),
+        1 => ButtonOrScroll::Button(MouseButton::Left),
+        2 => ButtonOrScroll::Button(MouseButton::Middle),
+        3 => ButtonOrScroll::Button(MouseButton::Right),
+        4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
+        5 => ButtonOrScroll::Scroll(ScrollDirection::Down),
+        6 => ButtonOrScroll::Scroll(ScrollDirection::Left),
+        7 => ButtonOrScroll::Scroll(ScrollDirection::Right),
+        8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)),
+        9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)),
         _ => return None,
     })
 }
@@ -48,3 +64,91 @@ pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton>
         return None;
     })
 }
+
+pub(crate) fn get_valuator_axis_index(
+    valuator_mask: &Vec<u32>,
+    valuator_number: u16,
+) -> Option<usize> {
+    // XInput valuator masks have a 1 at the bit indexes corresponding to each
+    // valuator present in this event's axisvalues. Axisvalues is ordered from
+    // lowest valuator number to highest, so counting bits before the 1 bit for
+    // this valuator yields the index in axisvalues.
+    if bit_is_set_in_vec(&valuator_mask, valuator_number) {
+        Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize)
+    } else {
+        None
+    }
+}
+
+/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`.
+fn popcount_upto_bit_index(bit_vec: &Vec<u32>, bit_index: u16) -> u32 {
+    let array_index = bit_index as usize / 32;
+    let popcount: u32 = bit_vec
+        .get(array_index)
+        .map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones());
+    if array_index == 0 {
+        popcount
+    } else {
+        // Valuator numbers over 32 probably never occur for scroll position, but may as well
+        // support it.
+        let leading_popcount: u32 = bit_vec
+            .iter()
+            .take(array_index)
+            .map(|bits| bits.count_ones())
+            .sum();
+        popcount + leading_popcount
+    }
+}
+
+fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
+    let array_index = bit_index as usize / 32;
+    bit_vec
+        .get(array_index)
+        .map_or(false, |bits| bit_is_set(*bits, bit_index % 32))
+}
+
+fn bit_is_set(bits: u32, bit_index: u16) -> bool {
+    bits & (1 << bit_index) != 0
+}
+
+/// Sets every bit with `i >= bit_index` to 0.
+fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 {
+    if bit_index == 0 {
+        0
+    } else if bit_index >= 32 {
+        u32::MAX
+    } else {
+        bits & ((1 << bit_index) - 1)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_get_valuator_axis_index() {
+        assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0));
+        assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1));
+        assert!(get_valuator_axis_index(&vec![0b11], 2) == None);
+
+        assert!(get_valuator_axis_index(&vec![0b100], 0) == None);
+        assert!(get_valuator_axis_index(&vec![0b100], 1) == None);
+        assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0));
+        assert!(get_valuator_axis_index(&vec![0b100], 3) == None);
+
+        assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None);
+        assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0));
+        assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None);
+        assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1));
+
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None);
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0));
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None);
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1));
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2));
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None);
+
+        assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3));
+    }
+}

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

@@ -29,7 +29,7 @@ use std::{
     sync::Arc,
 };
 
-use super::{X11Display, XINPUT_MASTER_DEVICE};
+use super::{X11Display, XINPUT_ALL_DEVICES, XINPUT_ALL_DEVICE_GROUPS};
 x11rb::atom_manager! {
     pub XcbAtoms: AtomsCookie {
         XA_ATOM,
@@ -475,7 +475,7 @@ impl X11WindowState {
             .xinput_xi_select_events(
                 x_window,
                 &[xinput::EventMask {
-                    deviceid: XINPUT_MASTER_DEVICE,
+                    deviceid: XINPUT_ALL_DEVICE_GROUPS,
                     mask: vec![
                         xinput::XIEventMask::MOTION
                             | xinput::XIEventMask::BUTTON_PRESS
@@ -487,6 +487,19 @@ impl X11WindowState {
             )
             .unwrap();
 
+        xcb_connection
+            .xinput_xi_select_events(
+                x_window,
+                &[xinput::EventMask {
+                    deviceid: XINPUT_ALL_DEVICES,
+                    mask: vec![
+                        xinput::XIEventMask::HIERARCHY,
+                        xinput::XIEventMask::DEVICE_CHANGED,
+                    ],
+                }],
+            )
+            .unwrap();
+
         xcb_connection.flush().unwrap();
 
         let raw = RawWindow {
@@ -1253,7 +1266,7 @@ impl PlatformWindow for X11Window {
             self.0.x_window,
             state.atoms._GTK_SHOW_WINDOW_MENU,
             [
-                XINPUT_MASTER_DEVICE as u32,
+                XINPUT_ALL_DEVICE_GROUPS as u32,
                 coords.dst_x as u32,
                 coords.dst_y as u32,
                 0,