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
This commit is contained in:
Owen Law 2024-04-29 12:40:42 -04:00 committed by GitHub
parent ff8e7f91c1
commit ec95605fec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 157 additions and 20 deletions

View file

@ -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]

View file

@ -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
}

View file

@ -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

View file

@ -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