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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! 触控手势识别词表。
//!
//! RGPUI 在 rgpui 核心的单一可移植竞技场中识别来自原始
//! [`TouchEvent`](crate::TouchEvent) 的手势:识别器竞争进行中的触摸,
//! 获胜者认领,失败者被取消。识别出的手势尽可能通过*已有*的语义事件呈现,
//! 点击变为 [`ClickEvent::Touch`](crate::ClickEvent),平移变为携带
//! [`TouchPhase`](crate::TouchPhase) 的 [`ScrollWheelEvent`](crate::ScrollWheelEvent),
//! 捏合变为 [`PinchEvent`](crate::PinchEvent),
//! 因此针对 `on_click` 和滚动容器编写的组件在移动端无需修改即可工作。
use std::time::Duration;
use web_time::Instant;
use crate::{Axis, IsZero as _, Pixels, Point, TouchPhase, px};
/// 跟踪一次滚动手势中各事件的主导方向轴。
///
/// 手势由可用的触摸阶段分隔,对于只发出 [`TouchPhase::Moved`] 的平台,
/// 使用超时作为回退方案。
#[derive(Clone, Copy, Debug, Default)]
pub struct OngoingScroll {
last_event: Option<Instant>,
axis: Option<Axis>,
}
const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
impl OngoingScroll {
/// 将给定的增量过滤为当前滚动手势的主导方向轴。
pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
self.filter_at(delta, touch_phase, Instant::now())
}
fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
const UNLOCK_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: Pixels = px(6.);
if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) {
self.last_event = None;
self.axis = None;
return;
}
let x = delta.x.abs();
let y = delta.y.abs();
if x.is_zero() && y.is_zero() {
if touch_phase == TouchPhase::Started {
self.last_event = None;
self.axis = None;
}
return;
}
let starts_new_gesture = touch_phase == TouchPhase::Started
|| self
.last_event
.is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
let mut axis = self.axis;
if starts_new_gesture {
axis = if x <= y {
Some(Axis::Vertical)
} else {
Some(Axis::Horizontal)
};
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
match axis {
Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
axis = None;
}
Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
axis = None;
}
_ => {}
}
}
self.last_event = Some(now);
self.axis = axis;
match axis {
Some(Axis::Vertical) => delta.x = Pixels::ZERO,
Some(Axis::Horizontal) => delta.y = Pixels::ZERO,
None => {}
}
}
}
/// 手势识别器使用的体验常量。尽力提供,取决于各平台的支持,
/// 默认使用 RGPUI 自身(iOS 风格)的值
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GestureTuning {
/// 触摸在不再是潜在点击并变为平移/拖动之前可以移动的距离。
pub touch_slop: Pixels,
/// 点击之间累积点击计数的最大间隔。
pub multi_tap_interval: Duration,
/// 点击之间累积点击计数的最大距离。
pub multi_tap_slop: Pixels,
/// 触摸必须保持在 [`Self::touch_slop`] 范围内多久才会被识别为长按。
pub long_press_duration: Duration,
/// 在快速滑动后应用于滚动动量的每毫秒衰减因子。
/// (`UIScrollView` 使用每毫秒 `0.998` 作为其正常减速率。)
pub momentum_decay_per_ms: f32,
/// 启动滚动动量所需的最小释放速度(像素/秒)。
pub min_fling_velocity: f32,
}
impl Default for GestureTuning {
fn default() -> Self {
Self {
touch_slop: px(8.),
multi_tap_interval: Duration::from_millis(400),
multi_tap_slop: px(16.),
long_press_duration: Duration::from_millis(500),
momentum_decay_per_ms: 0.998,
min_fling_velocity: 50.,
}
}
}
/// 参与识别的手势类型集合。
///
/// 由 [`PlatformGestures::native_recognizers`] 使用,用于声明平台原生识别哪些手势,
/// 而不是留给 rgpui 核心的可移植识别器处理。
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GestureKinds {
/// 点击(和多次点击),通过 [`ClickEvent::Touch`](crate::ClickEvent) 呈现。
pub tap: bool,
/// 长按,通过 [`LongPressEvent`] 呈现。
pub long_press: bool,
/// 平移/滚动(包括快速滑动动量),通过
/// [`ScrollWheelEvent`](crate::ScrollWheelEvent) 呈现。
pub pan: bool,
/// 捏合缩放,通过 [`PinchEvent`](crate::PinchEvent) 呈现。
pub pinch: bool,
}
impl GestureKinds {
/// 无手势;rgpui 核心的可移植识别器处理所有手势。
pub const NONE: Self = Self {
tap: false,
long_press: false,
pan: false,
pinch: false,
};
/// 所有手势类型。
pub const ALL: Self = Self {
tap: true,
long_press: true,
pan: true,
pinch: true,
};
}
/// 长按手势,移动端的右键菜单触发器。
///
/// 裸长按通过 [`ClickEvent`](crate::ClickEvent) 以 `long_press: true` 呈现,
/// 与右键点击一起传递给辅助点击监听器。此事件是需要手势本身的元素的原始钩子
/// (例如长按开始拖动);注册 API 与手势竞技场一起提供。
#[derive(Clone, Debug, Default)]
pub struct LongPressEvent {
/// 被识别为长按的触摸位置。
pub position: Point<Pixels>,
}
/// 平台手势识别服务。
///
/// 如果你的移动平台支持原生手势识别,使用此 trait 将其与 RGPUI 共享。
pub trait PlatformGestures {
/// 此平台上可移植识别器的体验常量。
fn tuning(&self) -> GestureTuning {
GestureTuning::default()
}
/// 此平台原生识别的手势类型。
fn native_recognizers(&self) -> GestureKinds {
GestureKinds::NONE
}
}
/// 一个空操作的 [`PlatformGestures`] 实现:无原生识别器,使用默认调优。
/// 适用于桌面平台和测试。
pub struct NullPlatformGestures;
impl PlatformGestures for NullPlatformGestures {}