[yew-devel] [PATCH yew-widget-toolkit 2/7] touch: gesture detector: implement a touch only mode

Dominik Csapak d.csapak at proxmox.com
Tue Jun 24 14:19:20 CEST 2025


when dealing with touch devices, using pointer events is not practical,
since those will be canceled by touch events. While doing

```
event.prevent_default();
```

inside the 'touchstart' event would allow us to use pointer events,
click events on touch enabled devices would not anymore (since those are
generated from the touch events when touching).

As a slightly less broken workaround, detect if the browser is touch
capable, and set the gesture detector to a touch only mode, so that it
only uses the touchstart/end/move/cancel events.

This makes it both work on a touch enabled device with touch, and a
non-touch enabled device with the mouse. One downside is that it does
not work with a mouse on touch enabled devices, but this should not that
big of a problem, since the gesture detector is intended to be used with
touch interfaces in the first place.

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 Cargo.toml                    |   1 +
 src/touch/gesture_detector.rs | 263 ++++++++++++++++++++++++++++++++--
 2 files changed, 251 insertions(+), 13 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 2561436..0a8d147 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,7 @@ web-sys = { version = "0.3", features = [
   "IntersectionObserverEntry",
   "KeyboardEventInit",
   "Touch",
+  "TouchList",
 ] }
 js-sys = "0.3"
 log = "0.4.6"
diff --git a/src/touch/gesture_detector.rs b/src/touch/gesture_detector.rs
index 0929e01..6984722 100644
--- a/src/touch/gesture_detector.rs
+++ b/src/touch/gesture_detector.rs
@@ -3,6 +3,8 @@ use std::collections::HashMap;
 use std::rc::Rc;
 
 use gloo_timers::callback::Timeout;
+use gloo_utils::window;
+use wasm_bindgen::JsValue;
 use web_sys::Touch;
 use yew::html::IntoEventCallback;
 use yew::prelude::*;
@@ -203,6 +205,11 @@ pub enum Msg {
 
     LongPressTimeout(i32),
     TapTimeout(i32),
+
+    TouchStart(TouchEvent),
+    TouchMove(TouchEvent),
+    TouchCancel(TouchEvent),
+    TouchEnd(TouchEvent),
 }
 
 #[derive(Copy, Clone, PartialEq)]
@@ -231,6 +238,7 @@ struct PointerState {
 
 #[doc(hidden)]
 pub struct PwtGestureDetector {
+    touch_only: bool,
     node_ref: NodeRef,
     state: DetectionState,
     pointers: HashMap<i32, PointerState>,
@@ -241,13 +249,9 @@ fn now() -> f64 {
 }
 
 impl PwtGestureDetector {
-    fn register_pointer(&mut self, ctx: &Context<Self>, event: &PointerEvent) {
+    fn register_pointer_state(&mut self, ctx: &Context<Self>, id: i32, start_x: i32, start_y: i32) {
         let props = ctx.props();
 
-        let id = event.pointer_id();
-        let start_x = event.x();
-        let start_y = event.y();
-
         let link = ctx.link().clone();
         let _long_press_timeout = Timeout::new(props.long_press_delay, move || {
             link.send_message(Msg::LongPressTimeout(id))
@@ -279,6 +283,36 @@ impl PwtGestureDetector {
         );
     }
 
+    fn register_pointer(&mut self, ctx: &Context<Self>, event: &PointerEvent) {
+        let id = event.pointer_id();
+        let start_x = event.x();
+        let start_y = event.y();
+
+        self.register_pointer_state(ctx, id, start_x, start_y);
+    }
+
+    fn register_touches(&mut self, ctx: &Context<Self>, event: &TouchEvent) {
+        for_each_changed_touch(event, |touch: Touch| {
+            let id = touch.identifier();
+            let x = touch.client_x();
+            let y = touch.client_y();
+            self.register_pointer_state(ctx, id, x, y);
+        });
+    }
+
+    fn unregister_touches<F: FnMut(i32, Touch, PointerState)>(
+        &mut self,
+        event: &TouchEvent,
+        mut func: F,
+    ) {
+        for_each_changed_touch(event, |touch: Touch| {
+            let id = touch.identifier();
+            if let Some(state) = self.pointers.remove(&id) {
+                func(id, touch, state);
+            }
+        });
+    }
+
     fn unregister_pointer(&mut self, id: i32) -> Option<PointerState> {
         self.pointers.remove(&id)
     }
@@ -332,10 +366,24 @@ impl PwtGestureDetector {
                 self.register_pointer(ctx, &event);
                 self.state = DetectionState::Single;
             }
+            Msg::TouchStart(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 0);
+                self.register_touches(ctx, &event);
+                self.state = match self.pointers.len() {
+                    0 => DetectionState::Initial,
+                    1 => DetectionState::Single,
+                    // TODO implement more touches
+                    _ => DetectionState::Double,
+                };
+            }
             Msg::PointerUp(_event) => { /* ignore */ }
             Msg::PointerMove(_event) => { /* ignore */ }
             Msg::PointerCancel(_event) => { /* ignore */ }
             Msg::PointerLeave(_event) => { /* ignore */ }
+            Msg::TouchMove(_event) => { /* ignore */ }
+            Msg::TouchCancel(_event) => { /* ignore */ }
+            Msg::TouchEnd(_event) => { /* ignore */ }
         }
         true
     }
@@ -376,6 +424,17 @@ impl PwtGestureDetector {
                 self.register_pointer(ctx, &event);
                 self.state = DetectionState::Double;
             }
+            Msg::TouchStart(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                self.register_touches(ctx, &event);
+                self.state = match self.pointers.len() {
+                    0 => DetectionState::Initial,
+                    1 => DetectionState::Single,
+                    // TODO implement more touches
+                    _ => DetectionState::Double,
+                };
+            }
             Msg::PointerUp(event) => {
                 event.prevent_default();
                 let pointer_count = self.pointers.len();
@@ -396,6 +455,25 @@ impl PwtGestureDetector {
                     }
                 }
             }
+            Msg::TouchEnd(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                self.unregister_touches(&event, |_id, touch, pointer_state| {
+                    let distance = compute_distance(
+                        pointer_state.start_x,
+                        pointer_state.start_y,
+                        touch.client_x(),
+                        touch.client_y(),
+                    );
+                    if !pointer_state.got_tap_timeout && distance < props.tap_tolerance {
+                        if let Some(on_tap) = &props.on_tap {
+                            //log::info!("tap {} {}", event.x(), event.y());
+                            on_tap.emit(touch.into());
+                        }
+                    }
+                });
+                self.state = DetectionState::Initial;
+            }
             Msg::PointerMove(event) => {
                 event.prevent_default();
                 if let Some(pointer_state) =
@@ -418,6 +496,29 @@ impl PwtGestureDetector {
                     }
                 }
             }
+            Msg::TouchMove(event) => {
+                for_each_changed_touch(&event, |touch| {
+                    if let Some(pointer_state) = self.update_pointer_position(
+                        touch.identifier(),
+                        touch.client_x(),
+                        touch.client_y(),
+                    ) {
+                        let distance = compute_distance(
+                            pointer_state.start_x,
+                            pointer_state.start_y,
+                            touch.client_x(),
+                            touch.client_y(),
+                        );
+                        // Make sure it cannot be a TAP or LONG PRESS event
+                        if distance >= props.tap_tolerance {
+                            self.state = DetectionState::Drag;
+                            if let Some(on_drag_start) = &props.on_drag_start {
+                                on_drag_start.emit(touch.into());
+                            }
+                        }
+                    }
+                });
+            }
             Msg::PointerCancel(event) | Msg::PointerLeave(event) => {
                 let pointer_count = self.pointers.len();
                 assert!(pointer_count == 1);
@@ -425,6 +526,12 @@ impl PwtGestureDetector {
                     self.state = DetectionState::Initial;
                 }
             }
+            Msg::TouchCancel(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                self.unregister_touches(&event, |_, _, _| {});
+                self.state = DetectionState::Initial;
+            }
         }
         true
     }
@@ -445,6 +552,20 @@ impl PwtGestureDetector {
                     on_drag_end.emit(event.into());
                 }
             }
+            Msg::TouchStart(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                // Abort current drags
+                self.register_touches(ctx, &event);
+                self.state = DetectionState::Double;
+                for_each_active_touch(&event, |touch| {
+                    if self.pointers.contains_key(&touch.identifier()) {
+                        if let Some(on_drag_end) = &props.on_drag_end {
+                            on_drag_end.emit(touch.into());
+                        }
+                    }
+                });
+            }
             Msg::PointerUp(event) => {
                 event.prevent_default();
                 let pointer_count = self.pointers.len();
@@ -482,6 +603,44 @@ impl PwtGestureDetector {
                     }
                 }
             }
+            Msg::TouchEnd(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                for_each_changed_touch(&event, |touch| {
+                    if let Some(pointer_state) = self.unregister_pointer(touch.identifier()) {
+                        let distance = compute_distance(
+                            pointer_state.start_x,
+                            pointer_state.start_y,
+                            touch.client_x(),
+                            touch.client_y(),
+                        );
+                        let time_diff = now() - pointer_state.start_ctime;
+                        let speed = distance / time_diff;
+                        //log::info!("DRAG END {time_diff} {speed}");
+                        if let Some(on_drag_end) = &props.on_drag_end {
+                            on_drag_end.emit(touch.clone().into());
+                        }
+
+                        if let Some(on_swipe) = &props.on_swipe {
+                            if distance > props.swipe_min_distance
+                                && time_diff < props.swipe_max_duration
+                                && speed > props.swipe_min_velocity
+                            {
+                                let direction = compute_direction(
+                                    pointer_state.start_x,
+                                    pointer_state.start_y,
+                                    touch.client_x(),
+                                    touch.client_y(),
+                                );
+
+                                let event = GestureSwipeEvent::new(touch.into(), direction);
+                                on_swipe.emit(event)
+                            }
+                        }
+                    }
+                });
+                self.state = DetectionState::Initial;
+            }
             Msg::PointerMove(event) => {
                 event.prevent_default();
                 if let Some(pointer_state) =
@@ -501,6 +660,28 @@ impl PwtGestureDetector {
                     }
                 }
             }
+            Msg::TouchMove(event) => {
+                for_each_changed_touch(&event, |touch| {
+                    if let Some(pointer_state) = self.update_pointer_position(
+                        touch.identifier(),
+                        touch.client_x(),
+                        touch.client_y(),
+                    ) {
+                        let distance = compute_distance(
+                            pointer_state.start_x,
+                            pointer_state.start_y,
+                            touch.client_x(),
+                            touch.client_y(),
+                        );
+                        if distance >= props.tap_tolerance || pointer_state.got_tap_timeout {
+                            //log::info!("DRAG TO {} {}", event.x(), event.y());
+                            if let Some(on_drag_update) = &props.on_drag_update {
+                                on_drag_update.emit(touch.into());
+                            }
+                        }
+                    }
+                });
+            }
             Msg::PointerCancel(event) | Msg::PointerLeave(event) => {
                 let pointer_count = self.pointers.len();
                 assert!(pointer_count == 1);
@@ -512,6 +693,17 @@ impl PwtGestureDetector {
                     }
                 }
             }
+            Msg::TouchCancel(event) => {
+                let pointer_count = self.pointers.len();
+                assert!(pointer_count == 1);
+                self.unregister_touches(&event, |_id, touch, _pointer_state| {
+                    //log::info!("DRAG END");
+                    if let Some(on_drag_end) = &props.on_drag_end {
+                        on_drag_end.emit(touch.into());
+                    }
+                });
+                self.state = DetectionState::Initial;
+            }
         }
         true
     }
@@ -524,19 +716,35 @@ impl PwtGestureDetector {
             Msg::PointerDown(event) => {
                 self.register_pointer(ctx, &event);
             }
+            Msg::TouchStart(event) => {
+                self.register_touches(ctx, &event);
+            }
             Msg::PointerUp(event) => {
                 self.unregister_pointer(event.pointer_id());
                 if self.pointers.is_empty() {
                     self.state = DetectionState::Initial;
                 }
             }
+            Msg::TouchEnd(event) => {
+                self.unregister_touches(&event, |_, _, _| {});
+                if self.pointers.is_empty() {
+                    self.state = DetectionState::Initial;
+                }
+            }
             Msg::PointerMove(_event) => { /* ignore */ }
+            Msg::TouchMove(_event) => { /* ignore */ }
             Msg::PointerCancel(event) => {
                 self.unregister_pointer(event.pointer_id());
                 if self.pointers.is_empty() {
                     self.state = DetectionState::Initial;
                 }
             }
+            Msg::TouchCancel(event) => {
+                self.unregister_touches(&event, |_, _, _| {});
+                if self.pointers.is_empty() {
+                    self.state = DetectionState::Initial;
+                }
+            }
             Msg::PointerLeave(event) => {
                 self.unregister_pointer(event.pointer_id());
                 if self.pointers.is_empty() {
@@ -553,7 +761,10 @@ impl Component for PwtGestureDetector {
     type Properties = GestureDetector;
 
     fn create(_ctx: &Context<Self>) -> Self {
+        let touch_only = window().has_own_property(&JsValue::from_str("ontouchstart"));
+
         Self {
+            touch_only,
             state: DetectionState::Initial,
             pointers: HashMap::new(),
             node_ref: NodeRef::default(),
@@ -575,17 +786,25 @@ impl Component for PwtGestureDetector {
     fn view(&self, ctx: &Context<Self>) -> Html {
         let props = ctx.props();
 
-        Container::new()
+        let mut container = Container::new()
             .node_ref(self.node_ref.clone())
             .class("pwt-d-contents")
             .style("touch-action", "none")
-            .onpointerdown(ctx.link().callback(Msg::PointerDown))
-            .onpointerup(ctx.link().callback(Msg::PointerUp))
-            .onpointermove(ctx.link().callback(Msg::PointerMove))
-            .onpointercancel(ctx.link().callback(Msg::PointerCancel))
-            .onpointerleave(ctx.link().callback(Msg::PointerLeave))
-            .with_child(props.content.clone())
-            .into()
+            .with_child(props.content.clone());
+
+        if self.touch_only {
+            container.add_ontouchstart(ctx.link().callback(Msg::TouchStart));
+            container.add_ontouchmove(ctx.link().callback(Msg::TouchMove));
+            container.add_ontouchcancel(ctx.link().callback(Msg::TouchCancel));
+            container.add_ontouchend(ctx.link().callback(Msg::TouchEnd));
+        } else {
+            container.add_onpointerdown(ctx.link().callback(Msg::PointerDown));
+            container.add_onpointerup(ctx.link().callback(Msg::PointerUp));
+            container.add_onpointermove(ctx.link().callback(Msg::PointerMove));
+            container.add_onpointercancel(ctx.link().callback(Msg::PointerCancel));
+            container.add_onpointerleave(ctx.link().callback(Msg::PointerLeave));
+        }
+        container.into()
     }
 }
 
@@ -612,3 +831,21 @@ fn compute_distance(x1: i32, y1: i32, x2: i32, y2: i32) -> f64 {
 
     (dx * dx + dy * dy).sqrt()
 }
+
+fn for_each_changed_touch<F: FnMut(Touch)>(event: &TouchEvent, mut func: F) {
+    let touch_list = event.changed_touches();
+    for i in 0..touch_list.length() {
+        if let Some(touch) = touch_list.get(i) {
+            func(touch);
+        }
+    }
+}
+
+fn for_each_active_touch<F: FnMut(Touch)>(event: &TouchEvent, mut func: F) {
+    let touch_list = event.touches();
+    for i in 0..touch_list.length() {
+        if let Some(touch) = touch_list.get(i) {
+            func(touch);
+        }
+    }
+}
-- 
2.39.5





More information about the yew-devel mailing list