1use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8use embedder_traits::{InputEventId, PaintHitTestResult, Scroll, TouchEventType, TouchId};
9use euclid::{Point2D, Scale, Vector2D};
10use log::{debug, error, warn};
11use rustc_hash::{FxHashMap, FxHashSet};
12use servo_base::id::WebViewId;
13use style_traits::CSSPixel;
14use webrender_api::units::{DevicePixel, DevicePoint, DeviceVector2D};
15
16use self::TouchSequenceState::*;
17use crate::paint::RepaintReason;
18use crate::painter::Painter;
19use crate::refresh_driver::{BaseRefreshDriver, RefreshDriverObserver};
20use crate::webview_renderer::{ScrollEvent, ScrollZoomEvent, WebViewRenderer};
21
22#[repr(transparent)]
25#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
26pub(crate) struct TouchSequenceId(u32);
27
28impl TouchSequenceId {
29 const fn new() -> Self {
30 Self(0)
31 }
32
33 fn next(&mut self) {
39 self.0 = self.0.wrapping_add(1);
40 }
41}
42
43const TOUCH_PAN_MIN_SCREEN_PX: f32 = 10.0;
45const FLING_SCALING_FACTOR: f32 = 0.95;
47const FLING_MIN_SCREEN_PX: f32 = 3.0;
49const FLING_MAX_SCREEN_PX: f32 = 4000.0;
51
52pub struct TouchHandler {
53 webview_id: WebViewId,
55 pub current_sequence_id: TouchSequenceId,
56 touch_sequence_map: FxHashMap<TouchSequenceId, TouchSequenceInfo>,
58 pub(crate) pending_touch_input_events: RefCell<FxHashMap<InputEventId, PendingTouchInputEvent>>,
61 observing_frames_for_fling: Cell<bool>,
63}
64
65#[derive(Debug, Eq, PartialEq)]
67pub enum TouchMoveAllowed {
68 Prevented,
70 Allowed,
72 Pending,
74}
75
76pub(crate) enum TouchIdMoveTracking {
77 Track,
78 Remove,
79}
80
81struct HitTestResultCache {
85 value: PaintHitTestResult,
86 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
87}
88
89pub struct TouchSequenceInfo {
90 pub(crate) state: TouchSequenceState,
92 active_touch_points: Vec<TouchPoint>,
94 touch_ids_in_move: FxHashSet<TouchId>,
99 pub prevent_click: bool,
106 pub prevent_move: TouchMoveAllowed,
110 pending_touch_move_actions: Vec<ScrollZoomEvent>,
118 hit_test_result_cache: Option<HitTestResultCache>,
120}
121
122impl TouchSequenceInfo {
123 fn touch_count(&self) -> usize {
124 self.active_touch_points.len()
125 }
126
127 fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
128 debug_assert_eq!(self.touch_count(), 2);
129 let p0 = self.active_touch_points[0].point;
130 let p1 = self.active_touch_points[1].point;
131 let center = p0.lerp(p1, 0.5);
132 let distance = (p0 - p1).length();
133
134 (distance, center)
135 }
136
137 fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
138 debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
139 self.pending_touch_move_actions.push(action);
140 }
141
142 fn is_finished(&self) -> bool {
145 matches!(
146 self.state,
147 Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
148 )
149 }
150
151 fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
152 if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
153 let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
154 hit_test_result_cache.value.point_in_viewport += scaled_delta;
156 }
157 }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq)]
163
164pub struct TouchPoint {
165 pub touch_id: TouchId,
166 pub point: Point2D<f32, DevicePixel>,
167}
168
169impl TouchPoint {
170 fn new(touch_id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
171 TouchPoint { touch_id, point }
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq)]
177pub(crate) enum TouchSequenceState {
178 Touching,
180 Panning {
182 velocity: Vector2D<f32, DevicePixel>,
183 },
184 Pinching,
186 MultiTouch,
188 PendingFling {
193 velocity: Vector2D<f32, DevicePixel>,
194 point: DevicePoint,
195 },
196 Flinging {
198 velocity: Vector2D<f32, DevicePixel>,
199 point: DevicePoint,
200 },
201 PendingClick(DevicePoint),
203 Finished,
205}
206
207pub(crate) struct FlingAction {
208 pub delta: DeviceVector2D,
209 pub cursor: DevicePoint,
210}
211
212impl TouchHandler {
213 pub(crate) fn new(webview_id: WebViewId) -> Self {
214 let finished_info = TouchSequenceInfo {
215 state: TouchSequenceState::Finished,
216 active_touch_points: vec![],
217 touch_ids_in_move: FxHashSet::default(),
218 prevent_click: false,
219 prevent_move: TouchMoveAllowed::Pending,
220 pending_touch_move_actions: vec![],
221 hit_test_result_cache: None,
222 };
223 let mut touch_sequence_map = FxHashMap::default();
227 touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
228 TouchHandler {
229 webview_id,
230 current_sequence_id: TouchSequenceId::new(),
231 touch_sequence_map,
232 pending_touch_input_events: Default::default(),
233 observing_frames_for_fling: Default::default(),
234 }
235 }
236
237 pub(crate) fn set_handling_touch_move_for_touch_id(
238 &mut self,
239 sequence_id: TouchSequenceId,
240 touch_id: TouchId,
241 flag: TouchIdMoveTracking,
242 ) {
243 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
244 match flag {
245 TouchIdMoveTracking::Track => {
246 sequence.touch_ids_in_move.insert(touch_id);
247 },
248 TouchIdMoveTracking::Remove => {
249 sequence.touch_ids_in_move.remove(&touch_id);
250 },
251 }
252 }
253 }
254
255 pub(crate) fn is_handling_touch_move_for_touch_id(
256 &self,
257 sequence_id: TouchSequenceId,
258 touch_id: TouchId,
259 ) -> bool {
260 self.touch_sequence_map
261 .get(&sequence_id)
262 .is_some_and(|seq| seq.touch_ids_in_move.contains(&touch_id))
263 }
264
265 pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
266 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
267 sequence.prevent_click = true;
268 } else {
269 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
270 }
271 }
272
273 pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
274 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
275 sequence.prevent_move = TouchMoveAllowed::Prevented;
276 } else {
277 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
278 }
279 }
280
281 pub(crate) fn move_allowed(&self, sequence_id: TouchSequenceId) -> bool {
284 self.touch_sequence_map
285 .get(&sequence_id)
286 .is_none_or(|sequence| sequence.prevent_move == TouchMoveAllowed::Allowed)
287 }
288
289 pub(crate) fn take_pending_touch_move_actions(
290 &mut self,
291 sequence_id: TouchSequenceId,
292 ) -> Vec<ScrollZoomEvent> {
293 self.touch_sequence_map
294 .get_mut(&sequence_id)
295 .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
296 .unwrap_or_default()
297 }
298
299 pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
300 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
301 sequence.pending_touch_move_actions.clear();
302 }
303 }
304
305 pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
307 if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
308 if sequence.pending_touch_move_actions.is_empty() && sequence.state == Finished {
309 self.touch_sequence_map.remove(&sequence_id);
310 }
311 }
312 }
313
314 pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
315 let old = self.touch_sequence_map.remove(&sequence_id);
316 debug_assert!(old.is_some(), "Sequence already removed?");
317 }
318
319 fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
320 self.touch_sequence_map
321 .get_mut(&self.current_sequence_id)
322 .expect("Current Touch sequence does not exist")
323 }
324
325 fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
326 self.touch_sequence_map.get(&self.current_sequence_id)
327 }
328
329 fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
330 self.touch_sequence_map.get_mut(&self.current_sequence_id)
331 }
332
333 fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
334 self.touch_sequence_map
335 .get(&sequence_id)
336 .expect("Touch sequence not found.")
337 }
338
339 pub(crate) fn get_touch_sequence_mut(
340 &mut self,
341 sequence_id: TouchSequenceId,
342 ) -> Option<&mut TouchSequenceInfo> {
343 self.touch_sequence_map.get_mut(&sequence_id)
344 }
345
346 pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
347 if !self
349 .touch_sequence_map
350 .contains_key(&self.current_sequence_id) ||
351 self.get_touch_sequence(self.current_sequence_id)
352 .is_finished()
353 {
354 self.current_sequence_id.next();
355 debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
356 let active_touch_points = vec![TouchPoint::new(touch_id, point)];
357 self.touch_sequence_map.insert(
358 self.current_sequence_id,
359 TouchSequenceInfo {
360 state: Touching,
361 active_touch_points,
362 touch_ids_in_move: FxHashSet::default(),
363 prevent_click: false,
364 prevent_move: TouchMoveAllowed::Pending,
365 pending_touch_move_actions: vec![],
366 hit_test_result_cache: None,
367 },
368 );
369 } else {
370 debug!("Touch down in sequence {:?}.", self.current_sequence_id);
371 let touch_sequence = self.get_current_touch_sequence_mut();
372 touch_sequence
373 .active_touch_points
374 .push(TouchPoint::new(touch_id, point));
375 match touch_sequence.active_touch_points.len() {
376 2.. => {
377 touch_sequence.state = MultiTouch;
378 },
379 0..2 => {
380 unreachable!("Secondary touch_down event with less than 2 fingers active?");
381 },
382 }
383 touch_sequence.prevent_click = true;
385 }
386 }
387
388 pub(crate) fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
389 let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
390
391 let Flinging {
392 velocity,
393 point: cursor,
394 } = &mut touch_sequence.state
395 else {
396 self.observing_frames_for_fling.set(false);
397 return None;
398 };
399
400 if velocity.length().abs() < FLING_MIN_SCREEN_PX {
401 self.stop_fling_if_needed();
402 None
403 } else {
404 *velocity *= FLING_SCALING_FACTOR;
407 let _span = profile_traits::info_span!(
408 "TouchHandler::Flinging",
409 velocity = ?velocity,
410 )
411 .entered();
412 debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
413 Some(FlingAction {
414 delta: DeviceVector2D::new(velocity.x, velocity.y),
415 cursor: *cursor,
416 })
417 }
418 }
419
420 pub(crate) fn stop_fling_if_needed(&mut self) {
421 let current_sequence_id = self.current_sequence_id;
422 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
423 debug!(
424 "Touch sequence already removed before stoping potential flinging during Paint update"
425 );
426 return;
427 };
428 let Flinging { .. } = touch_sequence.state else {
429 return;
430 };
431 let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
432 debug!("Stopping flinging in touch sequence {current_sequence_id:?}");
433 touch_sequence.state = Finished;
434 self.try_remove_touch_sequence(current_sequence_id);
437 self.observing_frames_for_fling.set(false);
438 }
439
440 pub(crate) fn on_touch_move(
441 &mut self,
442 touch_id: TouchId,
443 point: Point2D<f32, DevicePixel>,
444 scale: f32,
445 ) -> Option<ScrollZoomEvent> {
446 let touch_sequence = self.try_get_current_touch_sequence_mut()?;
450 let idx = match touch_sequence
451 .active_touch_points
452 .iter_mut()
453 .position(|t| t.touch_id == touch_id)
454 {
455 Some(i) => i,
456 None => {
457 error!("Got a touchmove event for a non-active touch point");
458 return None;
459 },
460 };
461 let old_point = touch_sequence.active_touch_points[idx].point;
462 let delta = point - old_point;
463 touch_sequence.update_hit_test_result_cache_pointer(delta);
464
465 let action = match touch_sequence.touch_count() {
466 1 => {
467 if let Panning { ref mut velocity } = touch_sequence.state {
468 *velocity += delta;
470 *velocity /= 2.0;
471 touch_sequence.active_touch_points[idx].point = point;
473
474 Some(ScrollZoomEvent::Scroll(ScrollEvent {
476 scroll: Scroll::Delta((-delta).into()),
477 point,
478 }))
479 } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
480 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
481 {
482 let _span = profile_traits::info_span!(
483 "TouchHandler::ScrollBegin",
484 delta = ?delta,
485 )
486 .entered();
487 touch_sequence.state = Panning {
488 velocity: Vector2D::new(delta.x, delta.y),
489 };
490 touch_sequence.prevent_click = true;
492 touch_sequence.active_touch_points[idx].point = point;
494
495 Some(ScrollZoomEvent::Scroll(ScrollEvent {
497 scroll: Scroll::Delta((-delta).into()),
498 point,
499 }))
500 } else {
501 None
504 }
505 },
506 2 => {
507 if touch_sequence.state == Pinching ||
508 delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
509 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
510 {
511 touch_sequence.state = Pinching;
512 let (d0, _) = touch_sequence.pinch_distance_and_center();
513
514 touch_sequence.active_touch_points[idx].point = point;
516 let (d1, c1) = touch_sequence.pinch_distance_and_center();
517
518 Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
519 } else {
520 None
523 }
524 },
525 _ => {
526 touch_sequence.active_touch_points[idx].point = point;
527 touch_sequence.state = MultiTouch;
528 None
529 },
530 };
531 if let Some(action) = action {
534 if touch_sequence.prevent_move == TouchMoveAllowed::Pending {
535 touch_sequence.add_pending_touch_move_action(action);
536 }
537 }
538
539 action
540 }
541
542 pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
543 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
544 warn!("Current touch sequence not found");
545 return;
546 };
547 let old = match touch_sequence
548 .active_touch_points
549 .iter()
550 .position(|t| t.touch_id == touch_id)
551 {
552 Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
553 None => {
554 warn!("Got a touchup event for a non-active touch point");
555 None
556 },
557 };
558 match touch_sequence.state {
559 Touching => {
560 if touch_sequence.prevent_click {
561 touch_sequence.state = Finished;
562 } else {
563 touch_sequence.state = PendingClick(point);
564 }
565 },
566 Panning { velocity } => {
567 if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
568 let _span = profile_traits::info_span!(
569 "TouchHandler::FlingStart",
570 velocity = ?velocity,
571 )
572 .entered();
573 debug!(
575 "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
576 Raw velocity is {velocity:?}."
577 );
578
579 let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
582 match touch_sequence.prevent_move {
583 TouchMoveAllowed::Allowed => {
584 touch_sequence.state = Flinging { velocity, point }
585 },
588 TouchMoveAllowed::Pending => {
589 touch_sequence.state = PendingFling { velocity, point }
590 },
591 TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
592 }
593 } else {
594 let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
595 touch_sequence.state = Finished;
596 }
597 },
598 Pinching => {
599 touch_sequence.state = Touching;
600 },
601 MultiTouch => {
602 if touch_sequence.active_touch_points.is_empty() {
604 touch_sequence.state = Finished;
605 }
606 },
607 PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
608 error!("Touch-up received, but touch handler already in post-touchup state.")
609 },
610 }
611 #[cfg(debug_assertions)]
612 if touch_sequence.active_touch_points.is_empty() {
613 debug_assert!(
614 touch_sequence.is_finished(),
615 "Did not transition to a finished state: {:?}",
616 touch_sequence.state
617 );
618 }
619 debug!(
620 "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
621 touch_sequence.active_touch_points.len(),
622 self.current_sequence_id
623 );
624 }
625
626 pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
627 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
629 return;
630 };
631 match touch_sequence
632 .active_touch_points
633 .iter()
634 .position(|t| t.touch_id == touch_id)
635 {
636 Some(i) => {
637 touch_sequence.active_touch_points.swap_remove(i);
638 },
639 None => {
640 warn!("Got a touchcancel event for a non-active touch point");
641 return;
642 },
643 }
644 if touch_sequence.active_touch_points.is_empty() {
645 touch_sequence.state = Finished;
646 }
647 }
648
649 pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
650 let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
651 if sequence.state == Finished {
652 return None;
653 }
654 sequence
655 .hit_test_result_cache
656 .as_ref()
657 .map(|cache| Some(cache.value.clone()))?
658 }
659
660 pub(crate) fn set_hit_test_result_cache_value(
661 &mut self,
662 value: PaintHitTestResult,
663 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
664 ) {
665 if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) {
666 if sequence.hit_test_result_cache.is_none() {
667 sequence.hit_test_result_cache = Some(HitTestResultCache {
668 value,
669 device_pixels_per_page,
670 });
671 }
672 }
673 }
674
675 pub(crate) fn add_pending_touch_input_event(
676 &self,
677 id: InputEventId,
678 touch_id: TouchId,
679 event_type: TouchEventType,
680 ) {
681 self.pending_touch_input_events.borrow_mut().insert(
682 id,
683 PendingTouchInputEvent {
684 event_type,
685 sequence_id: self.current_sequence_id,
686 touch_id,
687 },
688 );
689 }
690
691 pub(crate) fn take_pending_touch_input_event(
692 &self,
693 id: InputEventId,
694 ) -> Option<PendingTouchInputEvent> {
695 self.pending_touch_input_events.borrow_mut().remove(&id)
696 }
697
698 pub(crate) fn add_touch_move_refresh_observer_if_necessary(
699 &self,
700 refresh_driver: Rc<BaseRefreshDriver>,
701 repaint_reason: &Cell<RepaintReason>,
702 ) {
703 if self.observing_frames_for_fling.get() {
704 return;
705 }
706
707 let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
708 return;
709 };
710
711 if !matches!(
712 current_touch_sequence.state,
713 TouchSequenceState::Flinging { .. },
714 ) {
715 return;
716 }
717
718 refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
719 webview_id: self.webview_id,
720 }));
721 self.observing_frames_for_fling.set(true);
722 repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
723 }
724}
725
726pub(crate) struct PendingTouchInputEvent {
730 pub event_type: TouchEventType,
731 pub sequence_id: TouchSequenceId,
732 pub touch_id: TouchId,
733}
734
735pub(crate) struct FlingRefreshDriverObserver {
736 pub webview_id: WebViewId,
737}
738
739impl RefreshDriverObserver for FlingRefreshDriverObserver {
740 fn frame_started(&self, painter: &mut Painter) -> bool {
741 painter
742 .webview_renderer_mut(self.webview_id)
743 .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
744 }
745}