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
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub(crate) enum PanAxis {
87 Horizontal,
88 Vertical,
89}
90
91struct HitTestResultCache {
95 value: PaintHitTestResult,
96 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
97}
98
99pub struct TouchSequenceInfo {
100 pub(crate) state: TouchSequenceState,
102 active_touch_points: Vec<TouchPoint>,
104 touch_ids_in_move: FxHashSet<TouchId>,
109 pub prevent_click: bool,
116 pub prevent_move: TouchMoveAllowed,
120 pending_touch_move_actions: Vec<ScrollZoomEvent>,
128 hit_test_result_cache: Option<HitTestResultCache>,
130}
131
132impl TouchSequenceInfo {
133 fn touch_count(&self) -> usize {
134 self.active_touch_points.len()
135 }
136
137 fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
138 debug_assert_eq!(self.touch_count(), 2);
139 let p0 = self.active_touch_points[0].point;
140 let p1 = self.active_touch_points[1].point;
141 let center = p0.lerp(p1, 0.5);
142 let distance = (p0 - p1).length();
143
144 (distance, center)
145 }
146
147 fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
148 debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
149 self.pending_touch_move_actions.push(action);
150 }
151
152 fn is_finished(&self) -> bool {
155 matches!(
156 self.state,
157 Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
158 )
159 }
160
161 fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
162 if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
163 let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
164 hit_test_result_cache.value.point_in_viewport += scaled_delta;
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq)]
173
174pub struct TouchPoint {
175 pub touch_id: TouchId,
176 pub point: Point2D<f32, DevicePixel>,
177}
178
179impl TouchPoint {
180 fn new(touch_id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
181 TouchPoint { touch_id, point }
182 }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq)]
187pub(crate) enum TouchSequenceState {
188 Touching,
190 Panning {
192 axis: PanAxis,
194 velocity: Vector2D<f32, DevicePixel>,
195 },
196 Pinching,
198 MultiTouch,
200 PendingFling {
205 velocity: Vector2D<f32, DevicePixel>,
206 point: DevicePoint,
207 },
208 Flinging {
210 velocity: Vector2D<f32, DevicePixel>,
211 point: DevicePoint,
212 },
213 PendingClick(DevicePoint),
215 Finished,
217}
218
219pub(crate) struct FlingAction {
220 pub delta: DeviceVector2D,
221 pub cursor: DevicePoint,
222}
223
224impl TouchHandler {
225 pub(crate) fn new(webview_id: WebViewId) -> Self {
226 let finished_info = TouchSequenceInfo {
227 state: TouchSequenceState::Finished,
228 active_touch_points: vec![],
229 touch_ids_in_move: FxHashSet::default(),
230 prevent_click: false,
231 prevent_move: TouchMoveAllowed::Pending,
232 pending_touch_move_actions: vec![],
233 hit_test_result_cache: None,
234 };
235 let mut touch_sequence_map = FxHashMap::default();
239 touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
240 TouchHandler {
241 webview_id,
242 current_sequence_id: TouchSequenceId::new(),
243 touch_sequence_map,
244 pending_touch_input_events: Default::default(),
245 observing_frames_for_fling: Default::default(),
246 }
247 }
248
249 pub(crate) fn set_handling_touch_move_for_touch_id(
250 &mut self,
251 sequence_id: TouchSequenceId,
252 touch_id: TouchId,
253 flag: TouchIdMoveTracking,
254 ) {
255 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
256 match flag {
257 TouchIdMoveTracking::Track => {
258 sequence.touch_ids_in_move.insert(touch_id);
259 },
260 TouchIdMoveTracking::Remove => {
261 sequence.touch_ids_in_move.remove(&touch_id);
262 },
263 }
264 }
265 }
266
267 pub(crate) fn is_handling_touch_move_for_touch_id(
268 &self,
269 sequence_id: TouchSequenceId,
270 touch_id: TouchId,
271 ) -> bool {
272 self.touch_sequence_map
273 .get(&sequence_id)
274 .is_some_and(|seq| seq.touch_ids_in_move.contains(&touch_id))
275 }
276
277 pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
278 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
279 sequence.prevent_click = true;
280 } else {
281 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
282 }
283 }
284
285 pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
286 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
287 sequence.prevent_move = TouchMoveAllowed::Prevented;
288 } else {
289 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
290 }
291 }
292
293 pub(crate) fn move_allowed(&self, sequence_id: TouchSequenceId) -> bool {
296 self.touch_sequence_map
297 .get(&sequence_id)
298 .is_none_or(|sequence| sequence.prevent_move == TouchMoveAllowed::Allowed)
299 }
300
301 pub(crate) fn take_pending_touch_move_actions(
302 &mut self,
303 sequence_id: TouchSequenceId,
304 ) -> Vec<ScrollZoomEvent> {
305 self.touch_sequence_map
306 .get_mut(&sequence_id)
307 .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
308 .unwrap_or_default()
309 }
310
311 pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
312 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
313 sequence.pending_touch_move_actions.clear();
314 }
315 }
316
317 pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
319 if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) &&
320 sequence.pending_touch_move_actions.is_empty() &&
321 sequence.state == Finished
322 {
323 self.touch_sequence_map.remove(&sequence_id);
324 }
325 }
326
327 pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
328 let old = self.touch_sequence_map.remove(&sequence_id);
329 debug_assert!(old.is_some(), "Sequence already removed?");
330 }
331
332 fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
333 self.touch_sequence_map
334 .get_mut(&self.current_sequence_id)
335 .expect("Current Touch sequence does not exist")
336 }
337
338 fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
339 self.touch_sequence_map.get(&self.current_sequence_id)
340 }
341
342 fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
343 self.touch_sequence_map.get_mut(&self.current_sequence_id)
344 }
345
346 fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
347 self.touch_sequence_map
348 .get(&sequence_id)
349 .expect("Touch sequence not found.")
350 }
351
352 pub(crate) fn get_touch_sequence_mut(
353 &mut self,
354 sequence_id: TouchSequenceId,
355 ) -> Option<&mut TouchSequenceInfo> {
356 self.touch_sequence_map.get_mut(&sequence_id)
357 }
358
359 pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
360 if !self
362 .touch_sequence_map
363 .contains_key(&self.current_sequence_id) ||
364 self.get_touch_sequence(self.current_sequence_id)
365 .is_finished()
366 {
367 self.current_sequence_id.next();
368 debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
369 let active_touch_points = vec![TouchPoint::new(touch_id, point)];
370 self.touch_sequence_map.insert(
371 self.current_sequence_id,
372 TouchSequenceInfo {
373 state: Touching,
374 active_touch_points,
375 touch_ids_in_move: FxHashSet::default(),
376 prevent_click: false,
377 prevent_move: TouchMoveAllowed::Pending,
378 pending_touch_move_actions: vec![],
379 hit_test_result_cache: None,
380 },
381 );
382 } else {
383 debug!("Touch down in sequence {:?}.", self.current_sequence_id);
384 let touch_sequence = self.get_current_touch_sequence_mut();
385 touch_sequence
386 .active_touch_points
387 .push(TouchPoint::new(touch_id, point));
388 match touch_sequence.active_touch_points.len() {
389 2.. => {
390 touch_sequence.state = MultiTouch;
391 },
392 0..2 => {
393 unreachable!("Secondary touch_down event with less than 2 fingers active?");
394 },
395 }
396 touch_sequence.prevent_click = true;
398 }
399 }
400
401 pub(crate) fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
402 let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
403
404 let Flinging {
405 velocity,
406 point: cursor,
407 } = &mut touch_sequence.state
408 else {
409 self.observing_frames_for_fling.set(false);
410 return None;
411 };
412
413 if velocity.length().abs() < FLING_MIN_SCREEN_PX {
414 self.stop_fling_if_needed();
415 None
416 } else {
417 *velocity *= FLING_SCALING_FACTOR;
420 let _span = profile_traits::info_span!(
421 "TouchHandler::Flinging",
422 velocity = ?velocity,
423 )
424 .entered();
425 debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
426 Some(FlingAction {
427 delta: DeviceVector2D::new(velocity.x, velocity.y),
428 cursor: *cursor,
429 })
430 }
431 }
432
433 pub(crate) fn stop_fling_if_needed(&mut self) {
434 let current_sequence_id = self.current_sequence_id;
435 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
436 debug!(
437 "Touch sequence already removed before stoping potential flinging during Paint update"
438 );
439 return;
440 };
441 let Flinging { .. } = touch_sequence.state else {
442 return;
443 };
444 let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
445 debug!("Stopping flinging in touch sequence {current_sequence_id:?}");
446 touch_sequence.state = Finished;
447 self.try_remove_touch_sequence(current_sequence_id);
450 self.observing_frames_for_fling.set(false);
451 }
452
453 pub(crate) fn on_touch_move(
454 &mut self,
455 touch_id: TouchId,
456 point: Point2D<f32, DevicePixel>,
457 scale: f32,
458 ) -> Option<ScrollZoomEvent> {
459 let touch_sequence = self.try_get_current_touch_sequence_mut()?;
463 let idx = match touch_sequence
464 .active_touch_points
465 .iter_mut()
466 .position(|t| t.touch_id == touch_id)
467 {
468 Some(i) => i,
469 None => {
470 error!("Got a touchmove event for a non-active touch point");
471 return None;
472 },
473 };
474 let old_point = touch_sequence.active_touch_points[idx].point;
475 let delta = point - old_point;
476 touch_sequence.update_hit_test_result_cache_pointer(delta);
477
478 let action = match touch_sequence.touch_count() {
479 1 => {
480 if let Panning {
481 axis,
482 ref mut velocity,
483 } = touch_sequence.state
484 {
485 let pan_delta = match axis {
488 PanAxis::Horizontal => Vector2D::new(delta.x, 0.0),
489 PanAxis::Vertical => Vector2D::new(0.0, delta.y),
490 };
491 *velocity += pan_delta;
493 *velocity /= 2.0;
494 touch_sequence.active_touch_points[idx].point = point;
496
497 Some(ScrollZoomEvent::Scroll(ScrollEvent {
499 scroll: Scroll::Delta((-pan_delta).into()),
500 point,
501 }))
502 } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
503 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
504 {
505 let _span = profile_traits::info_span!(
506 "TouchHandler::ScrollBegin",
507 delta = ?delta,
508 )
509 .entered();
510 let axis = if delta.y.abs() > delta.x.abs() {
514 PanAxis::Vertical
515 } else {
516 PanAxis::Horizontal
517 };
518 let pan_delta = match axis {
519 PanAxis::Horizontal => Vector2D::new(delta.x, 0.0),
520 PanAxis::Vertical => Vector2D::new(0.0, delta.y),
521 };
522 touch_sequence.state = Panning {
523 axis,
524 velocity: pan_delta,
525 };
526 touch_sequence.prevent_click = true;
528 touch_sequence.active_touch_points[idx].point = point;
530
531 Some(ScrollZoomEvent::Scroll(ScrollEvent {
533 scroll: Scroll::Delta((-pan_delta).into()),
534 point,
535 }))
536 } else {
537 None
540 }
541 },
542 2 => {
543 if touch_sequence.state == Pinching ||
544 delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
545 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
546 {
547 touch_sequence.state = Pinching;
548 let (d0, _) = touch_sequence.pinch_distance_and_center();
549
550 touch_sequence.active_touch_points[idx].point = point;
552 let (d1, c1) = touch_sequence.pinch_distance_and_center();
553
554 Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
555 } else {
556 None
559 }
560 },
561 _ => {
562 touch_sequence.active_touch_points[idx].point = point;
563 touch_sequence.state = MultiTouch;
564 None
565 },
566 };
567 if let Some(action) = action &&
570 touch_sequence.prevent_move == TouchMoveAllowed::Pending
571 {
572 touch_sequence.add_pending_touch_move_action(action);
573 }
574
575 action
576 }
577
578 pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
579 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
580 warn!("Current touch sequence not found");
581 return;
582 };
583 let old = match touch_sequence
584 .active_touch_points
585 .iter()
586 .position(|t| t.touch_id == touch_id)
587 {
588 Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
589 None => {
590 warn!("Got a touchup event for a non-active touch point");
591 None
592 },
593 };
594 match touch_sequence.state {
595 Touching => {
596 if touch_sequence.prevent_click {
597 touch_sequence.state = Finished;
598 } else {
599 touch_sequence.state = PendingClick(point);
600 }
601 },
602 Panning { velocity, .. } => {
603 if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
604 let _span = profile_traits::info_span!(
605 "TouchHandler::FlingStart",
606 velocity = ?velocity,
607 )
608 .entered();
609 debug!(
611 "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
612 Raw velocity is {velocity:?}."
613 );
614
615 let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
618 match touch_sequence.prevent_move {
619 TouchMoveAllowed::Allowed => {
620 touch_sequence.state = Flinging { velocity, point }
621 },
624 TouchMoveAllowed::Pending => {
625 touch_sequence.state = PendingFling { velocity, point }
626 },
627 TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
628 }
629 } else {
630 let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
631 touch_sequence.state = Finished;
632 }
633 },
634 Pinching => {
635 touch_sequence.state = Touching;
636 },
637 MultiTouch => {
638 if touch_sequence.active_touch_points.is_empty() {
640 touch_sequence.state = Finished;
641 }
642 },
643 PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
644 error!("Touch-up received, but touch handler already in post-touchup state.")
645 },
646 }
647 #[cfg(debug_assertions)]
648 if touch_sequence.active_touch_points.is_empty() {
649 debug_assert!(
650 touch_sequence.is_finished(),
651 "Did not transition to a finished state: {:?}",
652 touch_sequence.state
653 );
654 }
655 debug!(
656 "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
657 touch_sequence.active_touch_points.len(),
658 self.current_sequence_id
659 );
660 }
661
662 pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
663 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
665 return;
666 };
667 match touch_sequence
668 .active_touch_points
669 .iter()
670 .position(|t| t.touch_id == touch_id)
671 {
672 Some(i) => {
673 touch_sequence.active_touch_points.swap_remove(i);
674 },
675 None => {
676 warn!("Got a touchcancel event for a non-active touch point");
677 return;
678 },
679 }
680 if touch_sequence.active_touch_points.is_empty() {
681 touch_sequence.state = Finished;
682 }
683 }
684
685 pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
686 let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
687 if sequence.state == Finished {
688 return None;
689 }
690 sequence
691 .hit_test_result_cache
692 .as_ref()
693 .map(|cache| Some(cache.value.clone()))?
694 }
695
696 pub(crate) fn set_hit_test_result_cache_value(
697 &mut self,
698 value: PaintHitTestResult,
699 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
700 ) {
701 if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) &&
702 sequence.hit_test_result_cache.is_none()
703 {
704 sequence.hit_test_result_cache = Some(HitTestResultCache {
705 value,
706 device_pixels_per_page,
707 });
708 }
709 }
710
711 pub(crate) fn add_pending_touch_input_event(
712 &self,
713 id: InputEventId,
714 touch_id: TouchId,
715 event_type: TouchEventType,
716 ) {
717 self.pending_touch_input_events.borrow_mut().insert(
718 id,
719 PendingTouchInputEvent {
720 event_type,
721 sequence_id: self.current_sequence_id,
722 touch_id,
723 },
724 );
725 }
726
727 pub(crate) fn take_pending_touch_input_event(
728 &self,
729 id: InputEventId,
730 ) -> Option<PendingTouchInputEvent> {
731 self.pending_touch_input_events.borrow_mut().remove(&id)
732 }
733
734 pub(crate) fn add_touch_move_refresh_observer_if_necessary(
735 &self,
736 refresh_driver: Rc<BaseRefreshDriver>,
737 repaint_reason: &Cell<RepaintReason>,
738 ) {
739 if self.observing_frames_for_fling.get() {
740 return;
741 }
742
743 let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
744 return;
745 };
746
747 if !matches!(
748 current_touch_sequence.state,
749 TouchSequenceState::Flinging { .. },
750 ) {
751 return;
752 }
753
754 refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
755 webview_id: self.webview_id,
756 }));
757 self.observing_frames_for_fling.set(true);
758 repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
759 }
760}
761
762pub(crate) struct PendingTouchInputEvent {
766 pub event_type: TouchEventType,
767 pub sequence_id: TouchSequenceId,
768 pub touch_id: TouchId,
769}
770
771pub(crate) struct FlingRefreshDriverObserver {
772 pub webview_id: WebViewId,
773}
774
775impl RefreshDriverObserver for FlingRefreshDriverObserver {
776 fn frame_started(&self, painter: &mut Painter) -> bool {
777 painter
778 .webview_renderer_mut(self.webview_id)
779 .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
780 }
781}