diff --git a/crates/benilla-app/src/bindings.rs b/crates/benilla-app/src/bindings.rs index e5630d90f..c76a862fa 100644 --- a/crates/benilla-app/src/bindings.rs +++ b/crates/benilla-app/src/bindings.rs @@ -574,12 +574,17 @@ pub(crate) fn sync_dispatch( }) } -/// A wheel delta in lines (notches), whatever unit the OS reported: a trackpad's `Pixel` deltas -/// go through Bevy's own conversion factor. Every wheel reader shares it. -pub(crate) fn wheel_lines(unit: MouseScrollUnit, dy: f32) -> f32 { - match unit { - MouseScrollUnit::Line => dy, - MouseScrollUnit::Pixel => dy / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR, +/// Whether `Pixel` scroll is Wayland's axis units, Linux's one pixel path: winit reports lines on +/// X11 and on Windows. +pub(crate) const WAYLAND_PIXEL_SCROLL: bool = cfg!(target_os = "linux"); + +/// The physical pixels of a trackpad's travel that make one notch. On Wayland it is 10 axis units, +/// 10 logical pixels, as SDL, GLFW and Xwayland take one step; elsewhere Bevy's conversion factor. +pub(crate) fn pixels_per_notch(scale_factor: f32, wayland: bool) -> f32 { + if wayland { + 10.0 * scale_factor + } else { + MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR } } @@ -634,10 +639,11 @@ pub(crate) fn world_takes_wheel(dispatch: Option<&BindingDispatch>, mods: Mods, /// Whole notches out of a stream of line deltas, carrying the fraction between frames. /// -/// Deviation: a trackpad's `Pixel` deltas are carried until they make a line, and step at most -/// once per event ([`crate::ui_script::Record::Wheel`]). The reference steps on every -/// `WM_MOUSEWHEEL`, whatever its delta (`0x76c1a9`); the carry is kept because a carried trackpad -/// does not zoom or scroll at its event rate. +/// Deviation: a trackpad's `Pixel` deltas are carried until they make a line, and an event steps +/// once per whole line on Wayland and at most once elsewhere ([`Self::feed_pixels`]). The reference +/// steps on every `WM_MOUSEWHEEL`, whatever its delta (`0x76c1a9`); the carry is kept because a +/// carried trackpad does not zoom or scroll at its event rate, and Wayland takes every whole line +/// because a 10-pixel notch capped per event would lose a fast swipe's travel. /// /// For consumers that act per notch off the sign alone: the UI's `OnMouseWheel` (stock /// `ScrollFrameTemplate_OnMouseWheel` moves half a pane per call, `UIPanelTemplates.lua:150`) and @@ -662,6 +668,17 @@ impl WheelNotches { self.carry -= whole; whole as i32 } + + /// The steps a `Pixel` event of `travel` physical pixels makes. `scale_factor` is the window's + /// base scale, the one winit made the pixels physical with; `WOW_DPI`'s override is not. + pub(crate) fn feed_pixels(&mut self, travel: f32, scale_factor: f32, wayland: bool) -> i32 { + let whole = self.feed(travel / pixels_per_notch(scale_factor, wayland)); + if wayland { + whole + } else { + whole.signum() + } + } } /// The dispatch pass's head (see the module doc), ahead of the frame's records: the frame's @@ -2513,6 +2530,33 @@ mod tests { ); } + /// Wayland's notch is 10 logical pixels and an event steps for each whole one; elsewhere it is + /// Bevy's factor, whatever the scale, and an event steps at most once. + #[test] + fn pixel_travel_steps_by_the_platforms_notch() { + let mut wayland = WheelNotches::default(); + assert_eq!(wayland.feed_pixels(13.0, 1.33, true), 0, "under 10 logical"); + assert_eq!(wayland.feed_pixels(0.5, 1.33, true), 1, "the carry adds up"); + assert_eq!( + wayland.feed_pixels(-70.0, 1.33, true), + -5, + "each whole notch" + ); + + let mut elsewhere = WheelNotches::default(); + assert_eq!(elsewhere.feed_pixels(250.0, 2.0, false), 1, "at most one"); + assert_eq!( + elsewhere.feed_pixels(50.0, 2.0, false), + 1, + "the fraction carries" + ); + assert_eq!( + elsewhere.feed_pixels(99.0, 2.0, false), + 0, + "under the factor" + ); + } + /// The window deactivate (`0x514490`) clears the direction bits a Lua Start set and keeps the /// mouse pair. #[test] diff --git a/crates/benilla-app/src/ui_script/records.rs b/crates/benilla-app/src/ui_script/records.rs index fadfa51d1..34dd9542c 100644 --- a/crates/benilla-app/src/ui_script/records.rs +++ b/crates/benilla-app/src/ui_script/records.rs @@ -32,9 +32,9 @@ pub(crate) enum Record { /// A wheel turn, one step up (`true`) or down. The reference queues each `WM_MOUSEWHEEL`'s raw /// delta (`0x42d58c`, `0x424159`) and steps once per message on its sign alone (`setge` at /// `0x76c1a9`), so a line event is one step whatever its travel; a trackpad's `Pixel` events - /// step as their carry completes a line ([`crate::bindings::WheelNotches`]), at most once - /// each. A turn with no vertical travel is no record: `WM_MOUSEHWHEEL` takes the window - /// procedure's default arm and never reaches the client. + /// step as their carry completes lines ([`crate::bindings::WheelNotches::feed_pixels`]). A + /// turn with no vertical travel is no record: `WM_MOUSEHWHEEL` takes the window procedure's + /// default arm and never reaches the client. Wheel { up: bool, }, @@ -104,6 +104,9 @@ pub(crate) fn collect_input_records( ) { let events: Vec<&WindowEvent> = events.read().collect(); let now = window.single().ok().and_then(Window::cursor_position); + let base_scale = window + .single() + .map_or(1.0, |w| w.resolution.base_scale_factor()); let moved = events .iter() .any(|e| matches!(e, WindowEvent::CursorMoved(_) | WindowEvent::CursorLeft(_))); @@ -144,12 +147,13 @@ pub(crate) fn collect_input_records( let travel = crate::bindings::notch_travel(w.x, w.y, mods.shift); let step = match w.unit { MouseScrollUnit::Line => i32::from(travel > 0.0) - i32::from(travel < 0.0), - MouseScrollUnit::Pixel => records - .notches - .feed(crate::bindings::wheel_lines(w.unit, travel)) - .signum(), + MouseScrollUnit::Pixel => records.notches.feed_pixels( + travel, + base_scale, + crate::bindings::WAYLAND_PIXEL_SCROLL, + ), }; - if step != 0 { + for _ in 0..step.unsigned_abs() { out.push(InputRecord { record: Record::Wheel { up: step > 0 }, mods, @@ -427,7 +431,9 @@ mod tests { ], ); assert_eq!(shape(&got), ["WheelUp@Some(10.0)", "WheelUp@Some(10.0)"]); - let quarter = MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR / 4.0; + let wayland = crate::bindings::WAYLAND_PIXEL_SCROLL; + let notch = crate::bindings::pixels_per_notch(1.0, wayland); + let quarter = notch / 4.0; let got = collect(&mut w, vec![wheel(MouseScrollUnit::Pixel, quarter); 3]); assert!( got.is_empty(), @@ -440,10 +446,10 @@ mod tests { ["WheelUp@Some(10.0)"], "the fourth completes it" ); - // A pixel event carrying more than a line is still one step. - let lines = MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR * 2.5; - let got = collect(&mut w, vec![wheel(MouseScrollUnit::Pixel, -lines)]); - assert_eq!(shape(&got), ["WheelDown@Some(10.0)"]); + // A pixel event carrying more than a line: a record per step, two on Wayland, one elsewhere. + let got = collect(&mut w, vec![wheel(MouseScrollUnit::Pixel, -notch * 2.5)]); + let steps = if wayland { 2 } else { 1 }; + assert_eq!(shape(&got), vec!["WheelDown@Some(10.0)"; steps]); // The horizontal wheel is no notch: `WM_MOUSEHWHEEL` never reaches the client's wheel // walk. let sideways = WindowEvent::MouseWheel(MouseWheel { @@ -454,4 +460,17 @@ mod tests { }); assert!(collect(&mut w, vec![sideways]).is_empty()); } + + /// `Pixel` travel is physical at the OS's scale, which `WOW_DPI`'s override leaves alone. + #[test] + fn a_pixel_notch_reads_the_base_scale() { + let mut w = world(); + let mut windows = w.0.query_filtered::<&mut Window, With>(); + let mut window = windows.single_mut(&mut w.0).unwrap(); + window.resolution.set_scale_factor(2.0); + window.resolution.set_scale_factor_override(Some(1.0)); + let notch = crate::bindings::pixels_per_notch(2.0, crate::bindings::WAYLAND_PIXEL_SCROLL); + let got = collect(&mut w, vec![wheel(MouseScrollUnit::Pixel, notch)]); + assert_eq!(got.len(), 1, "one notch at the base scale"); + } }