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 eb259dd41c..71c877c458 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 d4a0044f79..7b440b5f60 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 3c173b3393..29427db022 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 da0f6d2a58..88b0d0a70a 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