1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Desktop mouse-wheel to rotary-input translation.
//!
//! The final deployment target for rotary input is a Wear OS watch, but the
//! development platform is a desktop. Mapping winit's `MouseWheel` onto
//! [`RotaryScrollEvent`] lets the whole rotary stack be built and eyeballed
//! without a watch, and keeps a single code path under test on the host.
//!
//! # Sign convention
//!
//! winit reports a **positive** vertical wheel delta when the wheel is scrolled
//! *up / away from the user* — the same physical direction that produces a
//! **positive** Android `AXIS_SCROLL` detent for a crown. Compose negates the
//! Android value on its way into `RotaryScrollEvent` (see
//! [`cranpose_app_shell::RotaryScrollEvent`]), so this translation negates the
//! winit delta too. A wheel scrolled up and a crown turned up therefore produce
//! the same **negative** `vertical_scroll_pixels`.
use cranpose_app_shell::RotaryScrollEvent;
use cranpose_ui::Point;
/// Converts an already-logical wheel delta into a [`RotaryScrollEvent`].
///
/// `logical_delta` is the output of
/// `DesktopWinitPlatform::scroll_delta`, which normalizes both
/// `MouseScrollDelta::LineDelta` and `MouseScrollDelta::PixelDelta` into
/// logical pixels while preserving winit's sign. `uptime_millis` is a
/// monotonic timestamp; only differences between events are meaningful.
pub(crate) fn rotary_event_from_wheel_delta(
logical_delta: Point,
uptime_millis: u64,
) -> RotaryScrollEvent {
RotaryScrollEvent::new(-logical_delta.y, -logical_delta.x, uptime_millis)
}
#[cfg(test)]
mod tests {
use super::*;
use cranpose_platform_desktop_winit::DesktopWinitPlatform;
use winit::dpi::PhysicalPosition;
use winit::event::MouseScrollDelta;
/// One wheel notch as normalized by `DesktopWinitPlatform`.
const NOTCH_PIXELS: f32 = 40.0;
fn rotary_for(delta: MouseScrollDelta, scale_factor: f64) -> RotaryScrollEvent {
let platform = DesktopWinitPlatform::new(scale_factor);
rotary_event_from_wheel_delta(platform.scroll_delta(delta), 0)
}
#[test]
fn line_delta_scroll_up_produces_negative_vertical_pixels() {
// Wheel up == crown turned away == negative vertical_scroll_pixels,
// matching Compose's `val axisValue = -event.getAxisValue(AXIS_SCROLL)`.
let event = rotary_for(MouseScrollDelta::LineDelta(0.0, 1.0), 1.0);
assert_eq!(event.vertical_scroll_pixels, -NOTCH_PIXELS);
assert_eq!(event.horizontal_scroll_pixels, 0.0);
}
#[test]
fn line_delta_scroll_down_produces_positive_vertical_pixels() {
let event = rotary_for(MouseScrollDelta::LineDelta(0.0, -1.0), 1.0);
assert_eq!(event.vertical_scroll_pixels, NOTCH_PIXELS);
}
#[test]
fn line_delta_maps_horizontal_axis_too() {
let event = rotary_for(MouseScrollDelta::LineDelta(2.0, 0.0), 1.0);
assert_eq!(event.horizontal_scroll_pixels, -2.0 * NOTCH_PIXELS);
assert_eq!(event.vertical_scroll_pixels, 0.0);
}
#[test]
fn pixel_delta_is_converted_and_negated() {
let event = rotary_for(
MouseScrollDelta::PixelDelta(PhysicalPosition { x: 0.0, y: 12.0 }),
1.0,
);
assert_eq!(event.vertical_scroll_pixels, -12.0);
}
#[test]
fn pixel_delta_respects_hidpi_scale_factor() {
// 24 physical px at 2x == 12 logical px, then negated.
let event = rotary_for(
MouseScrollDelta::PixelDelta(PhysicalPosition { x: 8.0, y: 24.0 }),
2.0,
);
assert_eq!(event.vertical_scroll_pixels, -12.0);
assert_eq!(event.horizontal_scroll_pixels, -4.0);
}
#[test]
fn both_delta_variants_agree_on_direction() {
// A scroll "up" must have the same sign whichever variant the platform
// reports, or trackpads and wheels would scroll opposite ways.
let line = rotary_for(MouseScrollDelta::LineDelta(0.0, 1.0), 1.0);
let pixel = rotary_for(
MouseScrollDelta::PixelDelta(PhysicalPosition { x: 0.0, y: 40.0 }),
1.0,
);
assert_eq!(line.vertical_scroll_pixels, pixel.vertical_scroll_pixels);
assert!(line.vertical_scroll_pixels < 0.0);
}
#[test]
fn desktop_sign_matches_the_android_detent_path() {
// The two ingresses must agree: a crown turned "up" reports a positive
// detent, a wheel scrolled "up" reports a positive winit delta, and
// both must land on a negative vertical_scroll_pixels.
let desktop = rotary_for(MouseScrollDelta::LineDelta(0.0, 1.0), 1.0);
let android = RotaryScrollEvent::from_detents(1.0, NOTCH_PIXELS, NOTCH_PIXELS, 0);
assert_eq!(
desktop.vertical_scroll_pixels,
android.vertical_scroll_pixels
);
}
#[test]
fn zero_delta_produces_an_empty_event() {
let event = rotary_for(MouseScrollDelta::LineDelta(0.0, 0.0), 1.0);
assert!(event.is_empty());
}
#[test]
fn uptime_is_carried_through() {
let event = rotary_event_from_wheel_delta(Point { x: 0.0, y: 1.0 }, 9_876);
assert_eq!(event.uptime_millis, 9_876);
}
}