linux: Various X11 scroll improvements (#18484)
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>
This commit is contained in:
parent
72be8c5d14
commit
527c9097f8
5 changed files with 407 additions and 142 deletions
|
@ -45,7 +45,7 @@ use crate::{
|
||||||
|
|
||||||
use super::x11::X11Client;
|
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.
|
// Values match the defaults on GTK.
|
||||||
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||||
|
|
|
@ -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));
|
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
|
||||||
match axis {
|
match axis {
|
||||||
wl_pointer::Axis::VerticalScroll => {
|
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 => {
|
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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
@ -1662,10 +1662,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||||
let wheel_percent = value120 as f32 / 120.0;
|
let wheel_percent = value120 as f32 / 120.0;
|
||||||
match axis {
|
match axis {
|
||||||
wl_pointer::Axis::VerticalScroll => {
|
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 => {
|
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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use core::str;
|
use core::str;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashSet;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::{Rc, Weak};
|
use std::rc::{Rc, Weak};
|
||||||
|
@ -42,7 +42,10 @@ use crate::{
|
||||||
WindowParams, X11Window,
|
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::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||||
use super::{XimCallbackEvent, XimHandler};
|
use super::{XimCallbackEvent, XimHandler};
|
||||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
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,
|
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 {
|
pub(crate) struct WindowRef {
|
||||||
window: X11WindowStatePtr,
|
window: X11WindowStatePtr,
|
||||||
|
@ -117,6 +128,26 @@ pub struct Xdnd {
|
||||||
position: Point<Pixels>,
|
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 struct X11ClientState {
|
||||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||||
pub(crate) event_loop: Option<calloop::EventLoop<'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_styles: HashMap<xproto::Window, CursorStyle>,
|
||||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||||
|
|
||||||
pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
|
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||||
pub(crate) scroll_x: Option<f32>,
|
|
||||||
pub(crate) scroll_y: Option<f32>,
|
|
||||||
|
|
||||||
pub(crate) common: LinuxCommon,
|
pub(crate) common: LinuxCommon,
|
||||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||||
|
@ -266,31 +295,21 @@ impl X11Client {
|
||||||
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
||||||
.unwrap();
|
.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
|
let xinput_version = xcb_connection
|
||||||
.xinput_xi_query_version(2, 0)
|
.xinput_xi_query_version(2, 1)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.reply()
|
.reply()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
// XInput 1.x is not supported.
|
||||||
assert!(
|
assert!(
|
||||||
xinput_version.major_version >= 2,
|
xinput_version.major_version >= 2,
|
||||||
"XInput Extension v2 not supported."
|
"XInput version >= 2 required."
|
||||||
);
|
);
|
||||||
|
|
||||||
let master_device_query = xcb_connection
|
let pointer_device_states =
|
||||||
.xinput_xi_query_device(XINPUT_MASTER_DEVICE)
|
get_new_pointer_device_states(&xcb_connection, &BTreeMap::new());
|
||||||
.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().reply().unwrap();
|
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
|
||||||
|
|
||||||
|
@ -434,9 +453,7 @@ impl X11Client {
|
||||||
cursor_styles: HashMap::default(),
|
cursor_styles: HashMap::default(),
|
||||||
cursor_cache: HashMap::default(),
|
cursor_cache: HashMap::default(),
|
||||||
|
|
||||||
scroll_class_data,
|
pointer_device_states,
|
||||||
scroll_x: None,
|
|
||||||
scroll_y: None,
|
|
||||||
|
|
||||||
clipboard,
|
clipboard,
|
||||||
clipboard_item: None,
|
clipboard_item: None,
|
||||||
|
@ -950,35 +967,56 @@ impl X11Client {
|
||||||
window.handle_ime_commit(text);
|
window.handle_ime_commit(text);
|
||||||
state = self.0.borrow_mut();
|
state = self.0.borrow_mut();
|
||||||
}
|
}
|
||||||
if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
|
match button_or_scroll_from_event_detail(event.detail) {
|
||||||
let click_elapsed = state.last_click.elapsed();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
state.last_click = Instant::now();
|
||||||
&& state
|
state.last_mouse_button = Some(button);
|
||||||
.last_mouse_button
|
state.last_location = position;
|
||||||
.is_some_and(|prev_button| prev_button == button)
|
let current_count = state.current_count;
|
||||||
&& is_within_click_distance(state.last_location, position)
|
|
||||||
{
|
drop(state);
|
||||||
state.current_count += 1;
|
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||||
} else {
|
button,
|
||||||
state.current_count = 1;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
log::warn!("Unknown button press: {event:?}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::XinputButtonRelease(event) => {
|
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_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||||
px(event.event_y 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()) {
|
match button_or_scroll_from_event_detail(event.detail) {
|
||||||
let click_count = state.current_count;
|
Some(ButtonOrScroll::Button(button)) => {
|
||||||
drop(state);
|
let click_count = state.current_count;
|
||||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
drop(state);
|
||||||
button,
|
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||||
position,
|
button,
|
||||||
modifiers,
|
position,
|
||||||
click_count,
|
modifiers,
|
||||||
}));
|
click_count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Some(ButtonOrScroll::Scroll(_)) => {}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::XinputMotion(event) => {
|
Event::XinputMotion(event) => {
|
||||||
|
@ -1014,12 +1056,6 @@ impl X11Client {
|
||||||
state.modifiers = modifiers;
|
state.modifiers = modifiers;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
let axisvalues = event
|
|
||||||
.axisvalues
|
|
||||||
.iter()
|
|
||||||
.map(|axisvalue| fp3232_to_f32(*axisvalue))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if event.valuator_mask[0] & 3 != 0 {
|
if event.valuator_mask[0] & 3 != 0 {
|
||||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||||
position,
|
position,
|
||||||
|
@ -1028,64 +1064,17 @@ impl X11Client {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut valuator_idx = 0;
|
state = self.0.borrow_mut();
|
||||||
let scroll_class_data = self.0.borrow().scroll_class_data.clone();
|
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
|
||||||
for shift in 0..32 {
|
let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
|
||||||
if (event.valuator_mask[0] >> shift) & 1 == 0 {
|
drop(state);
|
||||||
continue;
|
if let Some(scroll_delta) = scroll_delta {
|
||||||
|
window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
|
||||||
|
position,
|
||||||
|
scroll_delta,
|
||||||
|
modifiers,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
valuator_idx += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||||
|
@ -1095,10 +1084,10 @@ impl X11Client {
|
||||||
state.mouse_focused_window = Some(event.event);
|
state.mouse_focused_window = Some(event.event);
|
||||||
}
|
}
|
||||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
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();
|
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;
|
state.mouse_focused_window = None;
|
||||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||||
let position = point(
|
let position = point(
|
||||||
|
@ -1117,6 +1106,26 @@ impl X11Client {
|
||||||
}));
|
}));
|
||||||
window.set_hovered(false);
|
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)
|
.send_event(false, target, EventMask::default(), message)
|
||||||
.unwrap();
|
.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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,29 @@ use x11rb::protocol::{
|
||||||
|
|
||||||
use crate::{Modifiers, MouseButton, NavigationDirection};
|
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 {
|
Some(match detail {
|
||||||
1 => MouseButton::Left,
|
1 => ButtonOrScroll::Button(MouseButton::Left),
|
||||||
2 => MouseButton::Middle,
|
2 => ButtonOrScroll::Button(MouseButton::Middle),
|
||||||
3 => MouseButton::Right,
|
3 => ButtonOrScroll::Button(MouseButton::Right),
|
||||||
8 => MouseButton::Navigate(NavigationDirection::Back),
|
4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
|
||||||
9 => MouseButton::Navigate(NavigationDirection::Forward),
|
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,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -48,3 +64,91 @@ pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton>
|
||||||
return None;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
use super::{X11Display, XINPUT_ALL_DEVICES, XINPUT_ALL_DEVICE_GROUPS};
|
||||||
x11rb::atom_manager! {
|
x11rb::atom_manager! {
|
||||||
pub XcbAtoms: AtomsCookie {
|
pub XcbAtoms: AtomsCookie {
|
||||||
XA_ATOM,
|
XA_ATOM,
|
||||||
|
@ -475,7 +475,7 @@ impl X11WindowState {
|
||||||
.xinput_xi_select_events(
|
.xinput_xi_select_events(
|
||||||
x_window,
|
x_window,
|
||||||
&[xinput::EventMask {
|
&[xinput::EventMask {
|
||||||
deviceid: XINPUT_MASTER_DEVICE,
|
deviceid: XINPUT_ALL_DEVICE_GROUPS,
|
||||||
mask: vec![
|
mask: vec![
|
||||||
xinput::XIEventMask::MOTION
|
xinput::XIEventMask::MOTION
|
||||||
| xinput::XIEventMask::BUTTON_PRESS
|
| xinput::XIEventMask::BUTTON_PRESS
|
||||||
|
@ -487,6 +487,19 @@ impl X11WindowState {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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();
|
xcb_connection.flush().unwrap();
|
||||||
|
|
||||||
let raw = RawWindow {
|
let raw = RawWindow {
|
||||||
|
@ -1253,7 +1266,7 @@ impl PlatformWindow for X11Window {
|
||||||
self.0.x_window,
|
self.0.x_window,
|
||||||
state.atoms._GTK_SHOW_WINDOW_MENU,
|
state.atoms._GTK_SHOW_WINDOW_MENU,
|
||||||
[
|
[
|
||||||
XINPUT_MASTER_DEVICE as u32,
|
XINPUT_ALL_DEVICE_GROUPS as u32,
|
||||||
coords.dst_x as u32,
|
coords.dst_x as u32,
|
||||||
coords.dst_y as u32,
|
coords.dst_y as u32,
|
||||||
0,
|
0,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue