linux: Simplify scrolling implementation (#10497)

Mikayla Maki created

This PR adjusts our scrolling implementation to delay the generation of
ScrollWheel events until we receive a complete frame.

Note that our implementation is still a bit off-spec, as we don't delay
any other kind of events. But it's been working so far on a variety of
compositors and the other events contain complete data; so I'll hold off
on that refactor for now.

Release Notes:

- N/A

Change summary

crates/gpui/src/platform/linux/wayland/client.rs | 201 ++++++++++++-----
1 file changed, 135 insertions(+), 66 deletions(-)

Detailed changes

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

@@ -103,9 +103,13 @@ pub(crate) struct WaylandClientState {
     click: ClickState,
     repeat: KeyRepeat,
     modifiers: Modifiers,
-    scroll_direction: f64,
     axis_source: AxisSource,
     mouse_location: Option<Point<Pixels>>,
+    continuous_scroll_delta: Option<Point<Pixels>>,
+    discrete_scroll_delta: Option<Point<f32>>,
+    vertical_modifier: f32,
+    horizontal_modifier: f32,
+    scroll_event_received: bool,
     enter_token: Option<()>,
     button_pressed: Option<MouseButton>,
     mouse_focused_window: Option<WaylandWindowStatePtr>,
@@ -164,20 +168,21 @@ impl WaylandClientStatePtr {
 #[derive(Clone)]
 pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
 
-const WL_SEAT_MIN_VERSION: u32 = 4;
 const WL_OUTPUT_VERSION: u32 = 2;
 
 fn wl_seat_version(version: u32) -> u32 {
-    if version >= wl_pointer::EVT_AXIS_VALUE120_SINCE {
-        wl_pointer::EVT_AXIS_VALUE120_SINCE
-    } else if version >= WL_SEAT_MIN_VERSION {
-        WL_SEAT_MIN_VERSION
-    } else {
+    // We rely on the wl_pointer.frame event
+    const WL_SEAT_MIN_VERSION: u32 = 5;
+    const WL_SEAT_MAX_VERSION: u32 = 9;
+
+    if version < WL_SEAT_MIN_VERSION {
         panic!(
             "wl_seat below required version: {} < {}",
             version, WL_SEAT_MIN_VERSION
         );
     }
+
+    version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
 }
 
 impl WaylandClient {
@@ -257,9 +262,13 @@ impl WaylandClient {
                 function: false,
                 platform: false,
             },
-            scroll_direction: -1.0,
+            scroll_event_received: false,
             axis_source: AxisSource::Wheel,
             mouse_location: None,
+            continuous_scroll_delta: None,
+            discrete_scroll_delta: None,
+            vertical_modifier: -1.0,
+            horizontal_modifier: -1.0,
             button_pressed: None,
             mouse_focused_window: None,
             keyboard_focused_window: None,
@@ -887,77 +896,137 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                     _ => {}
                 }
             }
-            wl_pointer::Event::AxisRelativeDirection {
-                direction: WEnum::Value(direction),
-                ..
-            } => {
-                state.scroll_direction = match direction {
-                    AxisRelativeDirection::Identical => -1.0,
-                    AxisRelativeDirection::Inverted => 1.0,
-                    _ => -1.0,
-                }
-            }
+
+            // Axis Events
             wl_pointer::Event::AxisSource {
                 axis_source: WEnum::Value(axis_source),
             } => {
                 state.axis_source = axis_source;
             }
-            wl_pointer::Event::AxisValue120 {
-                axis: WEnum::Value(axis),
-                value120,
-            } => {
-                if let Some(focused_window) = state.mouse_focused_window.clone() {
-                    let value = value120 as f64 * state.scroll_direction;
-
-                    let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
-                        position: state.mouse_location.unwrap(),
-                        delta: match axis {
-                            wl_pointer::Axis::VerticalScroll => {
-                                ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
-                            }
-                            wl_pointer::Axis::HorizontalScroll => {
-                                ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
-                            }
-                            _ => unimplemented!(),
-                        },
-                        modifiers: state.modifiers,
-                        touch_phase: TouchPhase::Moved,
-                    });
-                    drop(state);
-                    focused_window.handle_input(input)
-                }
-            }
             wl_pointer::Event::Axis {
                 time,
                 axis: WEnum::Value(axis),
                 value,
                 ..
             } => {
-                // We handle discrete scroll events with `AxisValue120`.
-                if wl_pointer.version() >= wl_pointer::EVT_AXIS_VALUE120_SINCE
-                    && state.axis_source == AxisSource::Wheel
-                {
-                    return;
+                let axis_source = state.axis_source;
+                let axis_modifier = match axis {
+                    wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
+                    wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
+                    _ => 1.0,
+                };
+                let supports_relative_direction =
+                    wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
+                state.scroll_event_received = true;
+                let scroll_delta = state
+                    .continuous_scroll_delta
+                    .get_or_insert(point(px(0.0), px(0.0)));
+                // TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
+                let modifier = 3.0;
+                match axis {
+                    wl_pointer::Axis::VerticalScroll => {
+                        scroll_delta.y += px(value as f32 * modifier * axis_modifier);
+                    }
+                    wl_pointer::Axis::HorizontalScroll => {
+                        scroll_delta.x += px(value as f32 * modifier * axis_modifier);
+                    }
+                    _ => unreachable!(),
                 }
-                if let Some(focused_window) = state.mouse_focused_window.clone() {
-                    let value = value * state.scroll_direction;
+            }
+            wl_pointer::Event::AxisDiscrete {
+                axis: WEnum::Value(axis),
+                discrete,
+            } => {
+                state.scroll_event_received = true;
+                let axis_modifier = match axis {
+                    wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
+                    wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
+                    _ => 1.0,
+                };
 
-                    let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
-                        position: state.mouse_location.unwrap(),
-                        delta: match axis {
-                            wl_pointer::Axis::VerticalScroll => {
-                                ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
-                            }
-                            wl_pointer::Axis::HorizontalScroll => {
-                                ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
-                            }
-                            _ => unimplemented!(),
-                        },
-                        modifiers: state.modifiers,
-                        touch_phase: TouchPhase::Moved,
-                    });
-                    drop(state);
-                    focused_window.handle_input(input)
+                // TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
+                let modifier = 3.0;
+
+                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 * modifier;
+                    }
+                    wl_pointer::Axis::HorizontalScroll => {
+                        scroll_delta.x += discrete as f32 * axis_modifier * modifier;
+                    }
+                    _ => unreachable!(),
+                }
+            }
+            wl_pointer::Event::AxisRelativeDirection {
+                axis: WEnum::Value(axis),
+                direction: WEnum::Value(direction),
+            } => match (axis, direction) {
+                (wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Identical) => {
+                    state.vertical_modifier = -1.0
+                }
+                (wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Inverted) => {
+                    state.vertical_modifier = 1.0
+                }
+                (wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Identical) => {
+                    state.horizontal_modifier = -1.0
+                }
+                (wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Inverted) => {
+                    state.horizontal_modifier = 1.0
+                }
+                _ => unreachable!(),
+            },
+            wl_pointer::Event::AxisValue120 {
+                axis: WEnum::Value(axis),
+                value120,
+            } => {
+                state.scroll_event_received = true;
+                let axis_modifier = match axis {
+                    wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
+                    wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
+                    _ => unreachable!(),
+                };
+
+                let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
+                let wheel_percent = value120 as f32 / 120.0;
+                match axis {
+                    wl_pointer::Axis::VerticalScroll => {
+                        scroll_delta.y += wheel_percent * axis_modifier;
+                    }
+                    wl_pointer::Axis::HorizontalScroll => {
+                        scroll_delta.x += wheel_percent * axis_modifier;
+                    }
+                    _ => unreachable!(),
+                }
+            }
+            wl_pointer::Event::Frame => {
+                if state.scroll_event_received {
+                    state.scroll_event_received = false;
+                    let continuous = state.continuous_scroll_delta.take();
+                    let discrete = state.discrete_scroll_delta.take();
+                    if let Some(continuous) = continuous {
+                        if let Some(window) = state.mouse_focused_window.clone() {
+                            let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
+                                position: state.mouse_location.unwrap(),
+                                delta: ScrollDelta::Pixels(continuous),
+                                modifiers: state.modifiers,
+                                touch_phase: TouchPhase::Moved,
+                            });
+                            drop(state);
+                            window.handle_input(input);
+                        }
+                    } else if let Some(discrete) = discrete {
+                        if let Some(window) = state.mouse_focused_window.clone() {
+                            let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
+                                position: state.mouse_location.unwrap(),
+                                delta: ScrollDelta::Lines(discrete),
+                                modifiers: state.modifiers,
+                                touch_phase: TouchPhase::Moved,
+                            });
+                            drop(state);
+                            window.handle_input(input);
+                        }
+                    }
                 }
             }
             _ => {}