1use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8use base::id::WebViewId;
9use embedder_traits::{InputEventId, PaintHitTestResult, Scroll, TouchEventType, TouchId};
10use euclid::{Point2D, Scale, Vector2D};
11use log::{debug, error, warn};
12use rustc_hash::{FxHashMap, FxHashSet};
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 event_count: 1,
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 event_count: 1,
501 }))
502 } else {
503 None
506 }
507 },
508 2 => {
509 if touch_sequence.state == Pinching ||
510 delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
511 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
512 {
513 touch_sequence.state = Pinching;
514 let (d0, _) = touch_sequence.pinch_distance_and_center();
515
516 touch_sequence.active_touch_points[idx].point = point;
518 let (d1, c1) = touch_sequence.pinch_distance_and_center();
519
520 Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
521 } else {
522 None
525 }
526 },
527 _ => {
528 touch_sequence.active_touch_points[idx].point = point;
529 touch_sequence.state = MultiTouch;
530 None
531 },
532 };
533 if let Some(action) = action {
536 if touch_sequence.prevent_move == TouchMoveAllowed::Pending {
537 touch_sequence.add_pending_touch_move_action(action);
538 }
539 }
540
541 action
542 }
543
544 pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
545 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
546 warn!("Current touch sequence not found");
547 return;
548 };
549 let old = match touch_sequence
550 .active_touch_points
551 .iter()
552 .position(|t| t.touch_id == touch_id)
553 {
554 Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
555 None => {
556 warn!("Got a touchup event for a non-active touch point");
557 None
558 },
559 };
560 match touch_sequence.state {
561 Touching => {
562 if touch_sequence.prevent_click {
563 touch_sequence.state = Finished;
564 } else {
565 touch_sequence.state = PendingClick(point);
566 }
567 },
568 Panning { velocity } => {
569 if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
570 let _span = profile_traits::info_span!(
571 "TouchHandler::FlingStart",
572 velocity = ?velocity,
573 )
574 .entered();
575 debug!(
577 "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
578 Raw velocity is {velocity:?}."
579 );
580
581 let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
584 match touch_sequence.prevent_move {
585 TouchMoveAllowed::Allowed => {
586 touch_sequence.state = Flinging { velocity, point }
587 },
590 TouchMoveAllowed::Pending => {
591 touch_sequence.state = PendingFling { velocity, point }
592 },
593 TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
594 }
595 } else {
596 let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
597 touch_sequence.state = Finished;
598 }
599 },
600 Pinching => {
601 touch_sequence.state = Touching;
602 },
603 MultiTouch => {
604 if touch_sequence.active_touch_points.is_empty() {
606 touch_sequence.state = Finished;
607 }
608 },
609 PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
610 error!("Touch-up received, but touch handler already in post-touchup state.")
611 },
612 }
613 #[cfg(debug_assertions)]
614 if touch_sequence.active_touch_points.is_empty() {
615 debug_assert!(
616 touch_sequence.is_finished(),
617 "Did not transition to a finished state: {:?}",
618 touch_sequence.state
619 );
620 }
621 debug!(
622 "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
623 touch_sequence.active_touch_points.len(),
624 self.current_sequence_id
625 );
626 }
627
628 pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
629 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
631 return;
632 };
633 match touch_sequence
634 .active_touch_points
635 .iter()
636 .position(|t| t.touch_id == touch_id)
637 {
638 Some(i) => {
639 touch_sequence.active_touch_points.swap_remove(i);
640 },
641 None => {
642 warn!("Got a touchcancel event for a non-active touch point");
643 return;
644 },
645 }
646 if touch_sequence.active_touch_points.is_empty() {
647 touch_sequence.state = Finished;
648 }
649 }
650
651 pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
652 let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
653 if sequence.state == Finished {
654 return None;
655 }
656 sequence
657 .hit_test_result_cache
658 .as_ref()
659 .map(|cache| Some(cache.value.clone()))?
660 }
661
662 pub(crate) fn set_hit_test_result_cache_value(
663 &mut self,
664 value: PaintHitTestResult,
665 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
666 ) {
667 if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) {
668 if sequence.hit_test_result_cache.is_none() {
669 sequence.hit_test_result_cache = Some(HitTestResultCache {
670 value,
671 device_pixels_per_page,
672 });
673 }
674 }
675 }
676
677 pub(crate) fn add_pending_touch_input_event(
678 &self,
679 id: InputEventId,
680 touch_id: TouchId,
681 event_type: TouchEventType,
682 ) {
683 self.pending_touch_input_events.borrow_mut().insert(
684 id,
685 PendingTouchInputEvent {
686 event_type,
687 sequence_id: self.current_sequence_id,
688 touch_id,
689 },
690 );
691 }
692
693 pub(crate) fn take_pending_touch_input_event(
694 &self,
695 id: InputEventId,
696 ) -> Option<PendingTouchInputEvent> {
697 self.pending_touch_input_events.borrow_mut().remove(&id)
698 }
699
700 pub(crate) fn add_touch_move_refresh_observer_if_necessary(
701 &self,
702 refresh_driver: Rc<BaseRefreshDriver>,
703 repaint_reason: &Cell<RepaintReason>,
704 ) {
705 if self.observing_frames_for_fling.get() {
706 return;
707 }
708
709 let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
710 return;
711 };
712
713 if !matches!(
714 current_touch_sequence.state,
715 TouchSequenceState::Flinging { .. },
716 ) {
717 return;
718 }
719
720 refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
721 webview_id: self.webview_id,
722 }));
723 self.observing_frames_for_fling.set(true);
724 repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
725 }
726}
727
728pub(crate) struct PendingTouchInputEvent {
732 pub event_type: TouchEventType,
733 pub sequence_id: TouchSequenceId,
734 pub touch_id: TouchId,
735}
736
737pub(crate) struct FlingRefreshDriverObserver {
738 pub webview_id: WebViewId,
739}
740
741impl RefreshDriverObserver for FlingRefreshDriverObserver {
742 fn frame_started(&self, painter: &mut Painter) -> bool {
743 painter
744 .webview_renderer_mut(self.webview_id)
745 .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
746 }
747}