From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id C1FFF1FF16F for ; Tue, 24 Jun 2025 14:19:27 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 162A936912; Tue, 24 Jun 2025 14:20:02 +0200 (CEST) From: Dominik Csapak To: yew-devel@lists.proxmox.com Date: Tue, 24 Jun 2025 14:19:20 +0200 Message-Id: <20250624121925.57056-7-d.csapak@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250624121925.57056-1-d.csapak@proxmox.com> References: <20250624121925.57056-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [yew-devel] [PATCH yew-widget-toolkit 2/7] touch: gesture detector: implement a touch only mode X-BeenThere: yew-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Yew framework devel list at Proxmox List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Yew framework devel list at Proxmox Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: yew-devel-bounces@lists.proxmox.com Sender: "yew-devel" 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 --- 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, @@ -241,13 +249,9 @@ fn now() -> f64 { } impl PwtGestureDetector { - fn register_pointer(&mut self, ctx: &Context, event: &PointerEvent) { + fn register_pointer_state(&mut self, ctx: &Context, 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, 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, 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( + &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 { 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 { + 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) -> 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(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(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