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;
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
76struct HitTestResultCache {
80 value: PaintHitTestResult,
81 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
82}
83
84pub struct TouchSequenceInfo {
85 pub(crate) state: TouchSequenceState,
87 active_touch_points: Vec<TouchPoint>,
89 handling_touch_move: bool,
94 pub prevent_click: bool,
101 pub prevent_move: TouchMoveAllowed,
105 pending_touch_move_actions: Vec<ScrollZoomEvent>,
113 hit_test_result_cache: Option<HitTestResultCache>,
115}
116
117impl TouchSequenceInfo {
118 fn touch_count(&self) -> usize {
119 self.active_touch_points.len()
120 }
121
122 fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
123 debug_assert_eq!(self.touch_count(), 2);
124 let p0 = self.active_touch_points[0].point;
125 let p1 = self.active_touch_points[1].point;
126 let center = p0.lerp(p1, 0.5);
127 let distance = (p0 - p1).length();
128
129 (distance, center)
130 }
131
132 fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
133 debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
134 self.pending_touch_move_actions.push(action);
135 }
136
137 fn is_finished(&self) -> bool {
140 matches!(
141 self.state,
142 Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
143 )
144 }
145
146 fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
147 if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
148 let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
149 hit_test_result_cache.value.point_in_viewport += scaled_delta;
151 }
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
158
159pub struct TouchPoint {
160 pub touch_id: TouchId,
161 pub point: Point2D<f32, DevicePixel>,
162}
163
164impl TouchPoint {
165 fn new(touch_id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
166 TouchPoint { touch_id, point }
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq)]
172pub(crate) enum TouchSequenceState {
173 Touching,
175 Panning {
177 velocity: Vector2D<f32, DevicePixel>,
178 },
179 Pinching,
181 MultiTouch,
183 PendingFling {
188 velocity: Vector2D<f32, DevicePixel>,
189 point: DevicePoint,
190 },
191 Flinging {
193 velocity: Vector2D<f32, DevicePixel>,
194 point: DevicePoint,
195 },
196 PendingClick(DevicePoint),
198 Finished,
200}
201
202pub(crate) struct FlingAction {
203 pub delta: DeviceVector2D,
204 pub cursor: DevicePoint,
205}
206
207impl TouchHandler {
208 pub(crate) fn new(webview_id: WebViewId) -> Self {
209 let finished_info = TouchSequenceInfo {
210 state: TouchSequenceState::Finished,
211 active_touch_points: vec![],
212 handling_touch_move: false,
213 prevent_click: false,
214 prevent_move: TouchMoveAllowed::Pending,
215 pending_touch_move_actions: vec![],
216 hit_test_result_cache: None,
217 };
218 let mut touch_sequence_map = FxHashMap::default();
222 touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
223 TouchHandler {
224 webview_id,
225 current_sequence_id: TouchSequenceId::new(),
226 touch_sequence_map,
227 pending_touch_input_events: Default::default(),
228 observing_frames_for_fling: Default::default(),
229 }
230 }
231
232 pub(crate) fn set_handling_touch_move(&mut self, sequence_id: TouchSequenceId, flag: bool) {
233 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
234 sequence.handling_touch_move = flag;
235 }
236 }
237
238 pub(crate) fn is_handling_touch_move(&self, sequence_id: TouchSequenceId) -> bool {
239 self.touch_sequence_map
240 .get(&sequence_id)
241 .is_some_and(|seq| seq.handling_touch_move)
242 }
243
244 pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
245 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
246 sequence.prevent_click = true;
247 } else {
248 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
249 }
250 }
251
252 pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
253 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
254 sequence.prevent_move = TouchMoveAllowed::Prevented;
255 } else {
256 warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
257 }
258 }
259
260 pub(crate) fn move_allowed(&self, sequence_id: TouchSequenceId) -> bool {
263 self.touch_sequence_map
264 .get(&sequence_id)
265 .is_none_or(|sequence| sequence.prevent_move == TouchMoveAllowed::Allowed)
266 }
267
268 pub(crate) fn take_pending_touch_move_actions(
269 &mut self,
270 sequence_id: TouchSequenceId,
271 ) -> Vec<ScrollZoomEvent> {
272 self.touch_sequence_map
273 .get_mut(&sequence_id)
274 .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
275 .unwrap_or_default()
276 }
277
278 pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
279 if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
280 sequence.pending_touch_move_actions.clear();
281 }
282 }
283
284 pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
286 if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
287 if sequence.pending_touch_move_actions.is_empty() && sequence.state == Finished {
288 self.touch_sequence_map.remove(&sequence_id);
289 }
290 }
291 }
292
293 pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
294 let old = self.touch_sequence_map.remove(&sequence_id);
295 debug_assert!(old.is_some(), "Sequence already removed?");
296 }
297
298 fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
299 self.touch_sequence_map
300 .get_mut(&self.current_sequence_id)
301 .expect("Current Touch sequence does not exist")
302 }
303
304 fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
305 self.touch_sequence_map.get(&self.current_sequence_id)
306 }
307
308 fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
309 self.touch_sequence_map.get_mut(&self.current_sequence_id)
310 }
311
312 fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
313 self.touch_sequence_map
314 .get(&sequence_id)
315 .expect("Touch sequence not found.")
316 }
317
318 pub(crate) fn get_touch_sequence_mut(
319 &mut self,
320 sequence_id: TouchSequenceId,
321 ) -> Option<&mut TouchSequenceInfo> {
322 self.touch_sequence_map.get_mut(&sequence_id)
323 }
324
325 pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
326 if !self
328 .touch_sequence_map
329 .contains_key(&self.current_sequence_id) ||
330 self.get_touch_sequence(self.current_sequence_id)
331 .is_finished()
332 {
333 self.current_sequence_id.next();
334 debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
335 let active_touch_points = vec![TouchPoint::new(touch_id, point)];
336 self.touch_sequence_map.insert(
337 self.current_sequence_id,
338 TouchSequenceInfo {
339 state: Touching,
340 active_touch_points,
341 handling_touch_move: false,
342 prevent_click: false,
343 prevent_move: TouchMoveAllowed::Pending,
344 pending_touch_move_actions: vec![],
345 hit_test_result_cache: None,
346 },
347 );
348 } else {
349 debug!("Touch down in sequence {:?}.", self.current_sequence_id);
350 let touch_sequence = self.get_current_touch_sequence_mut();
351 touch_sequence
352 .active_touch_points
353 .push(TouchPoint::new(touch_id, point));
354 match touch_sequence.active_touch_points.len() {
355 2.. => {
356 touch_sequence.state = MultiTouch;
357 },
358 0..2 => {
359 unreachable!("Secondary touch_down event with less than 2 fingers active?");
360 },
361 }
362 touch_sequence.prevent_click = true;
364 }
365 }
366
367 pub(crate) fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
368 let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
369
370 let Flinging {
371 velocity,
372 point: cursor,
373 } = &mut touch_sequence.state
374 else {
375 self.observing_frames_for_fling.set(false);
376 return None;
377 };
378
379 if velocity.length().abs() < FLING_MIN_SCREEN_PX {
380 self.stop_fling_if_needed();
381 None
382 } else {
383 *velocity *= FLING_SCALING_FACTOR;
386 let _span = profile_traits::info_span!(
387 "TouchHandler::Flinging",
388 velocity = ?velocity,
389 )
390 .entered();
391 debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
392 Some(FlingAction {
393 delta: DeviceVector2D::new(velocity.x, velocity.y),
394 cursor: *cursor,
395 })
396 }
397 }
398
399 pub(crate) fn stop_fling_if_needed(&mut self) {
400 let current_sequence_id = self.current_sequence_id;
401 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
402 debug!(
403 "Touch sequence already removed before stoping potential flinging during Paint update"
404 );
405 return;
406 };
407 let Flinging { .. } = touch_sequence.state else {
408 return;
409 };
410 let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
411 debug!("Stopping flinging in touch sequence {current_sequence_id:?}");
412 touch_sequence.state = Finished;
413 self.try_remove_touch_sequence(current_sequence_id);
416 self.observing_frames_for_fling.set(false);
417 }
418
419 pub(crate) fn on_touch_move(
420 &mut self,
421 touch_id: TouchId,
422 point: Point2D<f32, DevicePixel>,
423 scale: f32,
424 ) -> Option<ScrollZoomEvent> {
425 let touch_sequence = self.try_get_current_touch_sequence_mut()?;
429 let idx = match touch_sequence
430 .active_touch_points
431 .iter_mut()
432 .position(|t| t.touch_id == touch_id)
433 {
434 Some(i) => i,
435 None => {
436 error!("Got a touchmove event for a non-active touch point");
437 return None;
438 },
439 };
440 let old_point = touch_sequence.active_touch_points[idx].point;
441 let delta = point - old_point;
442 touch_sequence.update_hit_test_result_cache_pointer(delta);
443
444 let action = match touch_sequence.touch_count() {
445 1 => {
446 if let Panning { ref mut velocity } = touch_sequence.state {
447 *velocity += delta;
449 *velocity /= 2.0;
450 touch_sequence.active_touch_points[idx].point = point;
452
453 Some(ScrollZoomEvent::Scroll(ScrollEvent {
455 scroll: Scroll::Delta((-delta).into()),
456 point,
457 event_count: 1,
458 }))
459 } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
460 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
461 {
462 let _span = profile_traits::info_span!(
463 "TouchHandler::ScrollBegin",
464 delta = ?delta,
465 )
466 .entered();
467 touch_sequence.state = Panning {
468 velocity: Vector2D::new(delta.x, delta.y),
469 };
470 touch_sequence.prevent_click = true;
472 touch_sequence.active_touch_points[idx].point = point;
474
475 Some(ScrollZoomEvent::Scroll(ScrollEvent {
477 scroll: Scroll::Delta((-delta).into()),
478 point,
479 event_count: 1,
480 }))
481 } else {
482 None
485 }
486 },
487 2 => {
488 if touch_sequence.state == Pinching ||
489 delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
490 delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
491 {
492 touch_sequence.state = Pinching;
493 let (d0, _) = touch_sequence.pinch_distance_and_center();
494
495 touch_sequence.active_touch_points[idx].point = point;
497 let (d1, c1) = touch_sequence.pinch_distance_and_center();
498
499 Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
500 } else {
501 None
504 }
505 },
506 _ => {
507 touch_sequence.active_touch_points[idx].point = point;
508 touch_sequence.state = MultiTouch;
509 None
510 },
511 };
512 if let Some(action) = action {
515 if touch_sequence.prevent_move == TouchMoveAllowed::Pending {
516 touch_sequence.add_pending_touch_move_action(action);
517 }
518 }
519
520 action
521 }
522
523 pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
524 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
525 warn!("Current touch sequence not found");
526 return;
527 };
528 let old = match touch_sequence
529 .active_touch_points
530 .iter()
531 .position(|t| t.touch_id == touch_id)
532 {
533 Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
534 None => {
535 warn!("Got a touch up event for a non-active touch point");
536 None
537 },
538 };
539 match touch_sequence.state {
540 Touching => {
541 if touch_sequence.prevent_click {
542 touch_sequence.state = Finished;
543 } else {
544 touch_sequence.state = PendingClick(point);
545 }
546 },
547 Panning { velocity } => {
548 if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
549 let _span = profile_traits::info_span!(
550 "TouchHandler::FlingStart",
551 velocity = ?velocity,
552 )
553 .entered();
554 debug!(
556 "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
557 Raw velocity is {velocity:?}."
558 );
559
560 let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
563 match touch_sequence.prevent_move {
564 TouchMoveAllowed::Allowed => {
565 touch_sequence.state = Flinging { velocity, point }
566 },
569 TouchMoveAllowed::Pending => {
570 touch_sequence.state = PendingFling { velocity, point }
571 },
572 TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
573 }
574 } else {
575 let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
576 touch_sequence.state = Finished;
577 }
578 },
579 Pinching => {
580 touch_sequence.state = Touching;
581 },
582 MultiTouch => {
583 if touch_sequence.active_touch_points.is_empty() {
585 touch_sequence.state = Finished;
586 }
587 },
588 PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
589 error!("Touch-up received, but touch handler already in post-touchup state.")
590 },
591 }
592 #[cfg(debug_assertions)]
593 if touch_sequence.active_touch_points.is_empty() {
594 debug_assert!(
595 touch_sequence.is_finished(),
596 "Did not transition to a finished state: {:?}",
597 touch_sequence.state
598 );
599 }
600 debug!(
601 "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
602 touch_sequence.active_touch_points.len(),
603 self.current_sequence_id
604 );
605 }
606
607 pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
608 let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
610 return;
611 };
612 match touch_sequence
613 .active_touch_points
614 .iter()
615 .position(|t| t.touch_id == touch_id)
616 {
617 Some(i) => {
618 touch_sequence.active_touch_points.swap_remove(i);
619 },
620 None => {
621 warn!("Got a touchcancel event for a non-active touch point");
622 return;
623 },
624 }
625 if touch_sequence.active_touch_points.is_empty() {
626 touch_sequence.state = Finished;
627 }
628 }
629
630 pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
631 let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
632 if sequence.state == Finished {
633 return None;
634 }
635 sequence
636 .hit_test_result_cache
637 .as_ref()
638 .map(|cache| Some(cache.value.clone()))?
639 }
640
641 pub(crate) fn set_hit_test_result_cache_value(
642 &mut self,
643 value: PaintHitTestResult,
644 device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
645 ) {
646 if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) {
647 if sequence.hit_test_result_cache.is_none() {
648 sequence.hit_test_result_cache = Some(HitTestResultCache {
649 value,
650 device_pixels_per_page,
651 });
652 }
653 }
654 }
655
656 pub(crate) fn add_pending_touch_input_event(
657 &self,
658 id: InputEventId,
659 touch_id: TouchId,
660 event_type: TouchEventType,
661 ) {
662 self.pending_touch_input_events.borrow_mut().insert(
663 id,
664 PendingTouchInputEvent {
665 event_type,
666 sequence_id: self.current_sequence_id,
667 touch_id,
668 },
669 );
670 }
671
672 pub(crate) fn take_pending_touch_input_event(
673 &self,
674 id: InputEventId,
675 ) -> Option<PendingTouchInputEvent> {
676 self.pending_touch_input_events.borrow_mut().remove(&id)
677 }
678
679 pub(crate) fn add_touch_move_refresh_observer_if_necessary(
680 &self,
681 refresh_driver: Rc<BaseRefreshDriver>,
682 repaint_reason: &Cell<RepaintReason>,
683 ) {
684 if self.observing_frames_for_fling.get() {
685 return;
686 }
687
688 let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
689 return;
690 };
691
692 if !matches!(
693 current_touch_sequence.state,
694 TouchSequenceState::Flinging { .. },
695 ) {
696 return;
697 }
698
699 refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
700 webview_id: self.webview_id,
701 }));
702 self.observing_frames_for_fling.set(true);
703 repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
704 }
705}
706
707pub(crate) struct PendingTouchInputEvent {
711 pub event_type: TouchEventType,
712 pub sequence_id: TouchSequenceId,
713 #[expect(unused)]
714 pub touch_id: TouchId,
715}
716
717pub(crate) struct FlingRefreshDriverObserver {
718 pub webview_id: WebViewId,
719}
720
721impl RefreshDriverObserver for FlingRefreshDriverObserver {
722 fn frame_started(&self, painter: &mut Painter) -> bool {
723 painter
724 .webview_renderer_mut(self.webview_id)
725 .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
726 }
727}