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 sequence.pending_touch_move_actions.is_empty() &&
309 sequence.state == Finished
310 {
311 self.touch_sequence_map.remove(&sequence_id);
312 }
313 }
314
315 pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
316 let old = self.touch_sequence_map.remove(&sequence_id);
317 debug_assert!(old.is_some(), "Sequence already removed?");
318 }
319
320 fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
321 self.touch_sequence_map
322 .get_mut(&self.current_sequence_id)
323 .expect("Current Touch sequence does not exist")
324 }
325
326 fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
327 self.touch_sequence_map.get(&self.current_sequence_id)
328 }
329
330 fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
331 self.touch_sequence_map.get_mut(&self.current_sequence_id)
332 }
333
334 fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
335 self.touch_sequence_map
336 .get(&sequence_id)
337 .expect("Touch sequence not found.")
338 }
339
340 pub(crate) fn get_touch_sequence_mut(
341 &mut self,
342 sequence_id: TouchSequenceId,
343 ) -> Option<&mut TouchSequenceInfo> {
344 self.touch_sequence_map.get_mut(&sequence_id)
345 }
346
347 pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
348 if !self
350 .touch_sequence_map
351 .contains_key(&self.current_sequence_id) ||
352 self.get_touch_sequence(self.current_sequence_id)
353 .is_finished()
354 {
355 self.current_sequence_id.next();
356 debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
357 let active_touch_points = vec![TouchPoint::new(touch_id, point)];
358 self.touch_sequence_map.insert(
359 self.current_sequence_id,
360 TouchSequenceInfo {
361 state: Touching,
362 active_touch_points,
363 touch_ids_in_move: FxHashSet::default(),
364 prevent_click: false,
365 prevent_move: TouchMoveAllowed::Pending,
366 pending_touch_move_actions: vec![],
367 hit_test_result_cache: None,
368 },
369 );
370 } else {
371 debug!("Touch down in sequence {:?}.", self.current_sequence_id);
372 let touch_sequence = self.get_current_touch_sequence_mut();
373 touch_sequence
374 .active_touch_points
375 .push(TouchPoint::new(touch_id, point));
376 match touch_sequence.active_touch_points.len() {
377 2.. => {
378 touch_sequence.state = MultiTouch;
379 },
380 0..2 => {
381 unreachable!("Secondary touch_down event with less than 2 fingers active?");
382 },
383 }
384 touch_sequence.prevent_click = true;
386 }
387 }
388
389 pub(crate) fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
390 let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
391
392 let Flinging {
393 velocity,
394 point: cursor,
395 } = &mut touch_sequence.state
396 else {
397 self.observing_frames_for_fling.set(false);
398 return None;
399 };
400
401 if velocity.length().abs() < FLING_MIN_SCREEN_PX {
402 self.stop_fling_if_needed();
403 None
404 } else {
405 *velocity *= FLING_SCALING_FACTOR;
408 let _span = profile_traits::info_span!(
409 "TouchHandler::Flinging",
410 velocity = ?velocity,
411 )
412 .entered();
413 debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
414 Some(FlingAction {
415 delta: DeviceVector2D::new(velocity.x, velocity.y),
416 cursor: *cursor,
417 })
418 }
419 }
420
421 pub(crate) fn stop_fling_if_needed(&mut self) {
422 let current_sequence_id = self.current_sequence_id;
423 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
424 debug!(
425 "Touch sequence already removed before stoping potential flinging during Paint update"
426 );
427 return;
428 };
429 let Flinging { .. } = touch_sequence.state else {
430 return;
431 };
432 let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
433 debug!("Stopping flinging in touch sequence {current_sequence_id:?}");
434 touch_sequence.state = Finished;
435 self.try_remove_touch_sequence(current_sequence_id);
438 self.observing_frames_for_fling.set(false);
439 }
440
441 pub(crate) fn on_touch_move(
442 &mut self,
443 touch_id: TouchId,
444 point: Point2D<f32, DevicePixel>,
445 scale: f32,
446 ) -> Option<ScrollZoomEvent> {
447 let touch_sequence = self.try_get_current_touch_sequence_mut()?;
451 let idx = match touch_sequence
452 .active_touch_points
453 .iter_mut()
454 .position(|t| t.touch_id == touch_id)
455 {
456 Some(i) => i,
457 None => {
458 error!("Got a touchmove event for a non-active touch point");
459 return None;
460 },
461 };
462 let old_point = touch_sequence.active_touch_points[idx].point;
463 let delta = point - old_point;
464 touch_sequence.update_hit_test_result_cache_pointer(delta);
465
466 let action = match touch_sequence.touch_count() {
467 1 => {
468 if let Panning { ref mut velocity } = touch_sequence.state {
469 *velocity += delta;
471 *velocity /= 2.0;
472 touch_sequence.active_touch_points[idx].point = point;
474
475 Some(ScrollZoomEvent::Scroll(ScrollEvent {
477 scroll: Scroll::Delta((-delta).into()),
478 point,
479 }))
480 } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
481 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
482 {
483 let _span = profile_traits::info_span!(
484 "TouchHandler::ScrollBegin",
485 delta = ?delta,
486 )
487 .entered();
488 touch_sequence.state = Panning {
489 velocity: Vector2D::new(delta.x, delta.y),
490 };
491 touch_sequence.prevent_click = true;
493 touch_sequence.active_touch_points[idx].point = point;
495
496 Some(ScrollZoomEvent::Scroll(ScrollEvent {
498 scroll: Scroll::Delta((-delta).into()),
499 point,
500 }))
501 } else {
502 None
505 }
506 },
507 2 => {
508 if touch_sequence.state == Pinching ||
509 delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
510 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
511 {
512 touch_sequence.state = Pinching;
513 let (d0, _) = touch_sequence.pinch_distance_and_center();
514
515 touch_sequence.active_touch_points[idx].point = point;
517 let (d1, c1) = touch_sequence.pinch_distance_and_center();
518
519 Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
520 } else {
521 None
524 }
525 },
526 _ => {
527 touch_sequence.active_touch_points[idx].point = point;
528 touch_sequence.state = MultiTouch;
529 None
530 },
531 };
532 if let Some(action) = action &&
535 touch_sequence.prevent_move == TouchMoveAllowed::Pending
536 {
537 touch_sequence.add_pending_touch_move_action(action);
538 }
539
540 action
541 }
542
543 pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
544 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
545 warn!("Current touch sequence not found");
546 return;
547 };
548 let old = match touch_sequence
549 .active_touch_points
550 .iter()
551 .position(|t| t.touch_id == touch_id)
552 {
553 Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
554 None => {
555 warn!("Got a touchup event for a non-active touch point");
556 None
557 },
558 };
559 match touch_sequence.state {
560 Touching => {
561 if touch_sequence.prevent_click {
562 touch_sequence.state = Finished;
563 } else {
564 touch_sequence.state = PendingClick(point);
565 }
566 },
567 Panning { velocity } => {
568 if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
569 let _span = profile_traits::info_span!(
570 "TouchHandler::FlingStart",
571 velocity = ?velocity,
572 )
573 .entered();
574 debug!(
576 "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
577 Raw velocity is {velocity:?}."
578 );
579
580 let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
583 match touch_sequence.prevent_move {
584 TouchMoveAllowed::Allowed => {
585 touch_sequence.state = Flinging { velocity, point }
586 },
589 TouchMoveAllowed::Pending => {
590 touch_sequence.state = PendingFling { velocity, point }
591 },
592 TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
593 }
594 } else {
595 let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
596 touch_sequence.state = Finished;
597 }
598 },
599 Pinching => {
600 touch_sequence.state = Touching;
601 },
602 MultiTouch => {
603 if touch_sequence.active_touch_points.is_empty() {
605 touch_sequence.state = Finished;
606 }
607 },
608 PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
609 error!("Touch-up received, but touch handler already in post-touchup state.")
610 },
611 }
612 #[cfg(debug_assertions)]
613 if touch_sequence.active_touch_points.is_empty() {
614 debug_assert!(
615 touch_sequence.is_finished(),
616 "Did not transition to a finished state: {:?}",
617 touch_sequence.state
618 );
619 }
620 debug!(
621 "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
622 touch_sequence.active_touch_points.len(),
623 self.current_sequence_id
624 );
625 }
626
627 pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
628 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
630 return;
631 };
632 match touch_sequence
633 .active_touch_points
634 .iter()
635 .position(|t| t.touch_id == touch_id)
636 {
637 Some(i) => {
638 touch_sequence.active_touch_points.swap_remove(i);
639 },
640 None => {
641 warn!("Got a touchcancel event for a non-active touch point");
642 return;
643 },
644 }
645 if touch_sequence.active_touch_points.is_empty() {
646 touch_sequence.state = Finished;
647 }
648 }
649
650 pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
651 let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
652 if sequence.state == Finished {
653 return None;
654 }
655 sequence
656 .hit_test_result_cache
657 .as_ref()
658 .map(|cache| Some(cache.value.clone()))?
659 }
660
661 pub(crate) fn set_hit_test_result_cache_value(
662 &mut self,
663 value: PaintHitTestResult,
664 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
665 ) {
666 if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) &&
667 sequence.hit_test_result_cache.is_none()
668 {
669 sequence.hit_test_result_cache = Some(HitTestResultCache {
670 value,
671 device_pixels_per_page,
672 });
673 }
674 }
675
676 pub(crate) fn add_pending_touch_input_event(
677 &self,
678 id: InputEventId,
679 touch_id: TouchId,
680 event_type: TouchEventType,
681 ) {
682 self.pending_touch_input_events.borrow_mut().insert(
683 id,
684 PendingTouchInputEvent {
685 event_type,
686 sequence_id: self.current_sequence_id,
687 touch_id,
688 },
689 );
690 }
691
692 pub(crate) fn take_pending_touch_input_event(
693 &self,
694 id: InputEventId,
695 ) -> Option<PendingTouchInputEvent> {
696 self.pending_touch_input_events.borrow_mut().remove(&id)
697 }
698
699 pub(crate) fn add_touch_move_refresh_observer_if_necessary(
700 &self,
701 refresh_driver: Rc<BaseRefreshDriver>,
702 repaint_reason: &Cell<RepaintReason>,
703 ) {
704 if self.observing_frames_for_fling.get() {
705 return;
706 }
707
708 let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
709 return;
710 };
711
712 if !matches!(
713 current_touch_sequence.state,
714 TouchSequenceState::Flinging { .. },
715 ) {
716 return;
717 }
718
719 refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
720 webview_id: self.webview_id,
721 }));
722 self.observing_frames_for_fling.set(true);
723 repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
724 }
725}
726
727pub(crate) struct PendingTouchInputEvent {
731 pub event_type: TouchEventType,
732 pub sequence_id: TouchSequenceId,
733 pub touch_id: TouchId,
734}
735
736pub(crate) struct FlingRefreshDriverObserver {
737 pub webview_id: WebViewId,
738}
739
740impl RefreshDriverObserver for FlingRefreshDriverObserver {
741 fn frame_started(&self, painter: &mut Painter) -> bool {
742 painter
743 .webview_renderer_mut(self.webview_id)
744 .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
745 }
746}