all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: yew-devel@lists.proxmox.com
Subject: [yew-devel] [PATCH yew-widget-toolkit 2/7] touch: gesture detector: implement a touch only mode
Date: Tue, 24 Jun 2025 14:19:20 +0200	[thread overview]
Message-ID: <20250624121925.57056-7-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250624121925.57056-1-d.csapak@proxmox.com>

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@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



_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel


  parent reply	other threads:[~2025-06-24 12:19 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-06-24 12:19 [yew-devel] [PATCH yew-widget-toolkit/yew-widget-toolkit-assets 00/11] various touch fixes Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit-assets 1/4] slidable: don't add padding on top and bottom to actions Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit-assets 2/4] slidable: set background-color to pwt-color-background for actions Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit-assets 3/4] color scheme: add `pwt-default-colors` helper Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit-assets 4/4] material: add rounded corners to bottom sheet Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 1/7] touch: gesture detector: introduce `InputEvent` Dominik Csapak
2025-06-24 16:27   ` [yew-devel] applied: " Dietmar Maurer
2025-06-24 12:19 ` Dominik Csapak [this message]
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 3/7] touch: fab: rename on_click to on_activate Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 4/7] touch: slidable action: " Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 5/7] touch: slidable action: add css classes property Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 6/7] touch: slidable: add on_tap callback Dominik Csapak
2025-06-24 12:19 ` [yew-devel] [PATCH yew-widget-toolkit 7/7] touch: slidable: cleanup unnecessary macro use Dominik Csapak
2025-06-25  6:57 ` [yew-devel] applied: [PATCH yew-widget-toolkit/yew-widget-toolkit-assets 00/11] various touch fixes Dietmar Maurer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250624121925.57056-7-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=yew-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal