From ec95605fec5f501e8b21784b6ac7b1978d43659d Mon Sep 17 00:00:00 2001 From: Owen Law <81528246+someone13574@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:40:42 -0400 Subject: [PATCH] XI2 Smooth Scrolling for X11 - Attempt 2 (#11110) 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 --- 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(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index eb259dd41cac0ad7ca8b6ce5e5269e89f8609712..71c877c458ebc40acc6154a4f015af92f75ce86e 100644 --- a/crates/gpui/Cargo.toml +++ b/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] diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index d4a0044f799c6766f96c077b54cf2abc81244d6e..7b440b5f60c4b3fddd7c25bfd67d71ccab5c7e4e 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, 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, pub(crate) xkb: xkbc::State, + pub(crate) scroll_class_data: Vec, + pub(crate) scroll_x: Option, + pub(crate) scroll_y: Option, + pub(crate) common: LinuxCommon, pub(crate) clipboard: X11ClipboardContext, pub(crate) primary: X11ClipboardContext, @@ -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::>(); 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::>(); + + 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 +} diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index 3c173b3393d27d68d2091b45a4f7d14493603712..29427db022d7992cdae8fbc4753a484abdd94c57 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/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 { Some(if state.contains(xproto::KeyButMask::BUTTON1) { MouseButton::Left diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index da0f6d2a58916bfa5d97a3b832d11f975231ef5d..88b0d0a70acee4b0467255f65fc5ed311aef92b0 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, }; @@ -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