* [PATCH yew-widget-toolkit v2 1/4] touch: gesture detector: implement pinch zoom gesture
@ 2026-04-16 8:47 Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 2/4] touch: gesture detector: unify on_drag{_start,_update,_end} callbacks Dominik Csapak
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-04-16 8:47 UTC (permalink / raw)
To: yew-devel
exposes three new callbacks:
* on_pinch_zoom_start
* on_pinch_zoom
* on_pinch_zoom_end
As callback parameter a new GesturePinchZoomEvent is introduced. This
struct contains the two involved points, the angle relative to the
starting angle (as radians) and the relative scale of the distance
between the points (for convenience).
The rotation angle does consider multiple rotations, e.g. if the angle
is at 359 degrees and the next update reports 1 degree, it gets updated
to 361 degrees (and so on). While technically we can't know if the user
rotated the touche very fast, it's rather unlikely that this happens
in the real world so this heuristic seems fine. (Usually a user
won't rotate their fingers more than 180-270 degrees anyway).
This gesture is only registered for exactly two touches/pointers as
otherwise it would be rather complicated to calculate the
distance/angle. (As a further step we could expose
on_multi_touch{_start,_end} then this can also be handled by a user).
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes in v2:
* only expose one callback
* include a GesturePhase in the GesturePinchZoomEvent so the user can still detect it
* better comments
src/touch/gesture_detector.rs | 322 +++++++++++++++++++++++++++++++++-
src/touch/mod.rs | 5 +-
2 files changed, 319 insertions(+), 8 deletions(-)
diff --git a/src/touch/gesture_detector.rs b/src/touch/gesture_detector.rs
index ec05c2b..8cabf55 100644
--- a/src/touch/gesture_detector.rs
+++ b/src/touch/gesture_detector.rs
@@ -86,6 +86,53 @@ impl Deref for GestureSwipeEvent {
}
}
+/// Determines the phase of the Gesture
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum GesturePhase {
+ /// The gesture just started
+ Start,
+ /// The gesture is already in progress and is updated
+ Update,
+ /// The gesture ended and this is the last update
+ End,
+}
+
+/// An event that can happen when the user uses a Pinch/Zoom gesture
+#[derive(Clone, PartialEq)]
+pub struct GesturePinchZoomEvent {
+ /// The current phase of the event
+ pub phase: GesturePhase,
+
+ /// First touch/pointer [Point] of the Pinch/Zoom event
+ pub point0: PinchPoint,
+ /// Second touch/pointer [Point] of the Pinch/Zoom event
+ pub point1: PinchPoint,
+
+ /// Current angle of the gesture, relative to the starting position
+ pub angle: f64,
+
+ /// Current scale of the distance between touch points relative to the starting positions
+ pub scale: f64,
+}
+
+impl GesturePinchZoomEvent {
+ fn new(
+ phase: GesturePhase,
+ point0: PinchPoint,
+ point1: PinchPoint,
+ angle: f64,
+ scale: f64,
+ ) -> Self {
+ Self {
+ phase,
+ point0,
+ point1,
+ angle,
+ scale,
+ }
+ }
+}
+
/// Gesture detector.
///
/// You need to set the CSS attribute `touch-action: none;` on children to receive all events.
@@ -96,6 +143,7 @@ impl Deref for GestureSwipeEvent {
/// - long press: long tab without drag.
/// - drag: pointer move while touching the surface.
/// - swipe: fired at the end of a fast drag.
+/// - pinch/zoom: fired when two touches/pointers move.
///
/// # Note
///
@@ -103,7 +151,8 @@ impl Deref for GestureSwipeEvent {
///
/// Nested gesture detection is currently not implemented.
///
-/// Scale and rotate detection is also not implemented.
+/// It might be necessary to apply 'touch-action: none' to the content element.
+///
#[derive(Properties, Clone, PartialEq)]
pub struct GestureDetector {
/// The yew component key.
@@ -152,6 +201,10 @@ pub struct GestureDetector {
#[prop_or_default]
pub on_swipe: Option<Callback<GestureSwipeEvent>>,
+
+ /// Callback for Pinch/Zoom gesture event.
+ #[prop_or_default]
+ pub on_pinch_zoom: Option<Callback<GesturePinchZoomEvent>>,
}
impl GestureDetector {
@@ -203,6 +256,12 @@ impl GestureDetector {
self.on_swipe = cb.into_event_callback();
self
}
+
+ /// Builder style method to set the on_pinch_zoom callback
+ pub fn on_pinch_zoom(mut self, cb: impl IntoEventCallback<GesturePinchZoomEvent>) -> Self {
+ self.on_pinch_zoom = cb.into_event_callback();
+ self
+ }
}
pub enum Msg {
@@ -227,6 +286,7 @@ enum DetectionState {
Single,
Drag,
Double,
+ Multi,
// Error,
Done,
}
@@ -245,12 +305,90 @@ struct PointerState {
direction: f64,
}
+impl PointerState {
+ fn to_pinch_point(&self, id: i32) -> PinchPoint {
+ PinchPoint {
+ id,
+ x: self.x,
+ y: self.y,
+ }
+ }
+}
+
+/// Represents a single pointer or touch
+#[derive(Debug, Clone, PartialEq)]
+pub struct PinchPoint {
+ /// The numeric ID of the touch or pointer
+ pub id: i32,
+ /// The x coordinate in pixels
+ pub x: i32,
+ /// The y coordinate in pixels
+ pub y: i32,
+}
+
+impl PinchPoint {
+ /// calculates the distance in pixels to another [Point]
+ pub fn distance(&self, other: &PinchPoint) -> f64 {
+ compute_distance(self.x, self.y, other.x, other.y)
+ }
+
+ /// calculates the angle of the line to another [Point] in radians
+ pub fn angle(&self, other: &PinchPoint) -> f64 {
+ let x_diff = (other.x - self.x) as f64;
+ let y_diff = (-other.y + self.y) as f64;
+
+ y_diff.atan2(x_diff) + std::f64::consts::PI
+ }
+}
+
+#[derive(Debug, Default, Clone, PartialEq)]
+struct PinchZoomInfo {
+ start_angle: f64,
+ current_angle: f64,
+ start_distance: f64,
+ current_distance: f64,
+}
+
+impl PinchZoomInfo {
+ fn new(point0: PinchPoint, point1: PinchPoint) -> Self {
+ let angle = point0.angle(&point1);
+
+ // force a minimal distance of 1 pixel
+ let distance = point0.distance(&point1).max(1.0);
+
+ Self {
+ start_angle: angle,
+ current_angle: angle,
+ start_distance: distance,
+ current_distance: distance,
+ }
+ }
+
+ fn update(&mut self, point0: PinchPoint, point1: PinchPoint) {
+ let last_angle = self.current_angle;
+ let rotations = (last_angle / std::f64::consts::TAU).round();
+
+ let angle = point0.angle(&point1) + rotations * std::f64::consts::TAU;
+
+ if (last_angle - angle).abs() < std::f64::consts::PI {
+ self.current_angle = angle;
+ } else if last_angle > angle {
+ self.current_angle = angle + std::f64::consts::TAU;
+ } else if last_angle < angle {
+ self.current_angle = angle - std::f64::consts::TAU;
+ }
+
+ self.current_distance = point0.distance(&point1);
+ }
+}
+
#[doc(hidden)]
pub struct PwtGestureDetector {
touch_only: bool,
node_ref: NodeRef,
state: DetectionState,
pointers: HashMap<i32, PointerState>,
+ pinch_zoom_info: PinchZoomInfo,
}
fn now() -> f64 {
@@ -298,6 +436,10 @@ impl PwtGestureDetector {
let start_y = event.y();
self.register_pointer_state(ctx, id, start_x, start_y);
+
+ if self.pointers.len() == 2 {
+ self.start_pinch_zoom();
+ }
}
fn register_touches(&mut self, ctx: &Context<Self>, event: &TouchEvent) {
@@ -307,6 +449,20 @@ impl PwtGestureDetector {
let y = touch.client_y();
self.register_pointer_state(ctx, id, x, y);
});
+
+ if self.pointers.len() == 2 {
+ self.start_pinch_zoom();
+ }
+ }
+
+ fn start_pinch_zoom(&mut self) {
+ let (point0, point1) = self.get_pinch_points();
+ self.pinch_zoom_info = PinchZoomInfo::new(point0, point1)
+ }
+
+ fn update_pinch_zoom(&mut self) {
+ let (point0, point1) = self.get_pinch_points();
+ self.pinch_zoom_info.update(point0, point1);
}
fn unregister_touches<F: FnMut(i32, Touch, PointerState)>(
@@ -332,6 +488,28 @@ impl PwtGestureDetector {
}
}
+ fn get_pinch_points(&self) -> (PinchPoint, PinchPoint) {
+ let mut points: Vec<_> = self
+ .pointers
+ .iter()
+ .map(|(id, pointer)| pointer.to_pinch_point(*id))
+ .collect();
+ assert!(points.len() == 2);
+
+ // sort for stable stable order
+ points.sort_by_key(|p| p.id);
+
+ (points.remove(0), points.remove(0))
+ }
+
+ fn get_angle(&self) -> f64 {
+ self.pinch_zoom_info.current_angle - self.pinch_zoom_info.start_angle
+ }
+
+ fn get_scale(&self) -> f64 {
+ self.pinch_zoom_info.current_distance / self.pinch_zoom_info.start_distance
+ }
+
fn update_pointer_position(&mut self, id: i32, x: i32, y: i32) -> Option<&PointerState> {
if let Some(pointer_state) = self.pointers.get_mut(&id) {
let ctime = now();
@@ -382,8 +560,12 @@ impl PwtGestureDetector {
self.state = match self.pointers.len() {
0 => DetectionState::Initial,
1 => DetectionState::Single,
- // TODO implement more touches
- _ => DetectionState::Double,
+ 2 => {
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
+ DetectionState::Double
+ }
+ _ => DetectionState::Multi,
};
}
Msg::PointerUp(_event) => { /* ignore */ }
@@ -432,6 +614,8 @@ impl PwtGestureDetector {
assert!(pointer_count == 1);
self.register_pointer(ctx, &event);
self.state = DetectionState::Double;
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
}
Msg::TouchStart(event) => {
let pointer_count = self.pointers.len();
@@ -440,8 +624,12 @@ impl PwtGestureDetector {
self.state = match self.pointers.len() {
0 => DetectionState::Initial,
1 => DetectionState::Single,
- // TODO implement more touches
- _ => DetectionState::Double,
+ 2 => {
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
+ DetectionState::Double
+ }
+ _ => DetectionState::Multi,
};
}
Msg::PointerUp(event) => {
@@ -560,13 +748,27 @@ impl PwtGestureDetector {
if let Some(on_drag_end) = &props.on_drag_end {
on_drag_end.emit(event.into());
}
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
}
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;
+ let pointer_count = self.pointers.len();
+ match pointer_count {
+ 2 => {
+ self.state = DetectionState::Double;
+
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
+ }
+ count if count > 2 => {
+ self.state = DetectionState::Multi;
+ }
+ _ => {}
+ }
for_each_active_touch(&event, |touch| {
if self.pointers.contains_key(&touch.identifier()) {
if let Some(on_drag_end) = &props.on_drag_end {
@@ -717,6 +919,110 @@ impl PwtGestureDetector {
true
}
+ fn call_on_pinch_zoom(
+ &mut self,
+ ctx: &Context<Self>,
+ point0: PinchPoint,
+ point1: PinchPoint,
+ phase: GesturePhase,
+ ) {
+ if let Some(on_pinch_zoom) = &ctx.props().on_pinch_zoom {
+ on_pinch_zoom.emit(GesturePinchZoomEvent::new(
+ phase,
+ point0,
+ point1,
+ self.get_angle(),
+ self.get_scale(),
+ ))
+ }
+ }
+
+ fn update_double(&mut self, ctx: &Context<Self>, msg: Msg) -> bool {
+ match msg {
+ Msg::TapTimeout(_id) => { /* ignore */ }
+ Msg::LongPressTimeout(_id) => { /* ignore */ }
+ Msg::PointerDown(event) => {
+ let pointer_count = self.pointers.len();
+ assert!(pointer_count == 2);
+ self.register_pointer(ctx, &event);
+ self.state = DetectionState::Multi;
+ }
+ Msg::TouchStart(event) => {
+ let pointer_count = self.pointers.len();
+ assert!(pointer_count == 2);
+ let (point0, point1) = self.get_pinch_points();
+ self.register_touches(ctx, &event);
+ self.state = DetectionState::Multi;
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::End);
+ }
+ Msg::PointerUp(event) | Msg::PointerCancel(event) | Msg::PointerLeave(event) => {
+ event.prevent_default();
+ let pointer_count = self.pointers.len();
+ assert!(pointer_count == 2);
+ let (point0, point1) = self.get_pinch_points();
+ if self.unregister_pointer(event.pointer_id()).is_some() {
+ self.state = DetectionState::Drag;
+ }
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::End);
+ }
+ Msg::TouchEnd(event) | Msg::TouchCancel(event) => {
+ let pointer_count = self.pointers.len();
+ assert!(pointer_count == 2);
+ let (point0, point1) = self.get_pinch_points();
+ let mut unregistered = 0;
+ for_each_changed_touch(&event, |touch| {
+ if self.unregister_pointer(touch.identifier()).is_some() {
+ unregistered += 1;
+ }
+ });
+ let pointer_count = pointer_count.saturating_sub(unregistered);
+ if pointer_count < 2 {
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::End);
+ }
+ match pointer_count {
+ 0 => self.state = DetectionState::Initial,
+ 1 => self.state = DetectionState::Drag,
+ 2 => {}
+ _more => self.state = DetectionState::Multi, // more touchpoints on removal?
+ }
+ }
+ Msg::PointerMove(event) => {
+ event.prevent_default();
+ let updated = self
+ .update_pointer_position(event.pointer_id(), event.x(), event.y())
+ .is_some();
+
+ self.update_pinch_zoom();
+
+ if updated {
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Update);
+ }
+ }
+ Msg::TouchMove(event) => {
+ let mut had_valid = false;
+ for_each_changed_touch(&event, |touch| {
+ if self
+ .update_pointer_position(
+ touch.identifier(),
+ touch.client_x(),
+ touch.client_y(),
+ )
+ .is_some()
+ {
+ had_valid = true
+ }
+ });
+ self.update_pinch_zoom();
+ if had_valid {
+ let (point0, point1) = self.get_pinch_points();
+ self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Update);
+ }
+ }
+ }
+ true
+ }
+
// Wait until all pointers are released
fn update_error(&mut self, ctx: &Context<Self>, msg: Msg) -> bool {
match msg {
@@ -777,6 +1083,7 @@ impl Component for PwtGestureDetector {
state: DetectionState::Initial,
pointers: HashMap::new(),
node_ref: NodeRef::default(),
+ pinch_zoom_info: PinchZoomInfo::default(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -786,7 +1093,8 @@ impl Component for PwtGestureDetector {
DetectionState::Initial => self.update_initial(ctx, msg),
DetectionState::Single => self.update_single(ctx, msg),
DetectionState::Drag => self.update_drag(ctx, msg),
- DetectionState::Double => self.update_error(ctx, msg), // todo
+ DetectionState::Double => self.update_double(ctx, msg),
+ DetectionState::Multi => self.update_error(ctx, msg), // todo
//DetectionState::Error => self.update_error(ctx, msg),
DetectionState::Done => self.update_error(ctx, msg),
}
diff --git a/src/touch/mod.rs b/src/touch/mod.rs
index acb27ae..6d00ee7 100644
--- a/src/touch/mod.rs
+++ b/src/touch/mod.rs
@@ -5,7 +5,10 @@ mod application_bar;
pub use application_bar::{ApplicationBar, PwtApplicationBar};
mod gesture_detector;
-pub use gesture_detector::{GestureDetector, GestureSwipeEvent, InputEvent, PwtGestureDetector};
+pub use gesture_detector::{
+ GestureDetector, GesturePhase, GesturePinchZoomEvent, GestureSwipeEvent, InputEvent,
+ PinchPoint, PwtGestureDetector,
+};
mod fab;
pub use fab::{Fab, FabSize, PwtFab};
--
2.47.3
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH yew-widget-toolkit v2 2/4] touch: gesture detector: unify on_drag{_start,_update,_end} callbacks
2026-04-16 8:47 [PATCH yew-widget-toolkit v2 1/4] touch: gesture detector: implement pinch zoom gesture Dominik Csapak
@ 2026-04-16 8:47 ` Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 3/4] touch: gesture detector: derive Clone and PartialEq for Events Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 4/4] touch: gesture detector: add missing comment for swipe callback Dominik Csapak
2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-04-16 8:47 UTC (permalink / raw)
To: yew-devel
Makes for a simpler interface with less callbacks, and with the
introduction of the GestureDragEvent with an included GesturePhase, we
don't lose anything here.
Adapted the users (Slidable and SideDialog) to use this new interface.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v2
src/touch/gesture_detector.rs | 100 +++++++++++++++++++---------------
src/touch/mod.rs | 4 +-
src/touch/side_dialog.rs | 78 +++++++++++++-------------
src/touch/slidable/mod.rs | 32 +++++------
4 files changed, 111 insertions(+), 103 deletions(-)
diff --git a/src/touch/gesture_detector.rs b/src/touch/gesture_detector.rs
index 8cabf55..d3b4452 100644
--- a/src/touch/gesture_detector.rs
+++ b/src/touch/gesture_detector.rs
@@ -133,6 +133,28 @@ impl GesturePinchZoomEvent {
}
}
+/// An event that can happen when the user uses a drag gesture
+#[derive(Clone, PartialEq)]
+pub struct GestureDragEvent {
+ /// The current phase of the event
+ pub phase: GesturePhase,
+
+ event: InputEvent,
+}
+
+impl GestureDragEvent {
+ fn new(event: InputEvent, phase: GesturePhase) -> Self {
+ Self { event, phase }
+ }
+}
+
+impl Deref for GestureDragEvent {
+ type Target = InputEvent;
+ fn deref(&self) -> &Self::Target {
+ &self.event
+ }
+}
+
/// Gesture detector.
///
/// You need to set the CSS attribute `touch-action: none;` on children to receive all events.
@@ -189,15 +211,9 @@ pub struct GestureDetector {
#[prop_or_default]
pub on_long_press: Option<Callback<()>>,
- /// Callback for drag-start events.
- #[prop_or_default]
- pub on_drag_start: Option<Callback<InputEvent>>,
- /// Callback for drag-start events.
- #[prop_or_default]
- pub on_drag_update: Option<Callback<InputEvent>>,
- /// Callback for drag-start events.
+ /// Callback for drag events.
#[prop_or_default]
- pub on_drag_end: Option<Callback<InputEvent>>,
+ pub on_drag: Option<Callback<GestureDragEvent>>,
#[prop_or_default]
pub on_swipe: Option<Callback<GestureSwipeEvent>>,
@@ -233,21 +249,9 @@ impl GestureDetector {
self
}
- /// Builder style method to set the on_drag_start callback
- pub fn on_drag_start(mut self, cb: impl IntoEventCallback<InputEvent>) -> Self {
- self.on_drag_start = cb.into_event_callback();
- self
- }
-
- /// Builder style method to set the on_drag_update callback
- pub fn on_drag_update(mut self, cb: impl IntoEventCallback<InputEvent>) -> Self {
- self.on_drag_update = cb.into_event_callback();
- self
- }
-
- /// Builder style method to set the on_drag_end callback
- pub fn on_drag_end(mut self, cb: impl IntoEventCallback<InputEvent>) -> Self {
- self.on_drag_end = cb.into_event_callback();
+ /// Builder style method to set the on_drag callback
+ pub fn on_drag(mut self, cb: impl IntoEventCallback<GestureDragEvent>) -> Self {
+ self.on_drag = cb.into_event_callback();
self
}
@@ -687,8 +691,8 @@ impl PwtGestureDetector {
//log::info!("DRAG START {} {}", event.x(), event.y());
self.state = DetectionState::Drag;
self.capture_pointer(event.pointer_id());
- if let Some(on_drag_start) = &props.on_drag_start {
- on_drag_start.emit(event.into());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(event.into(), GesturePhase::Start));
}
}
}
@@ -709,8 +713,9 @@ impl PwtGestureDetector {
// 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());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag
+ .emit(GestureDragEvent::new(touch.into(), GesturePhase::Start));
}
}
}
@@ -745,8 +750,8 @@ impl PwtGestureDetector {
self.register_pointer(ctx, &event);
self.state = DetectionState::Double;
//log::info!("DRAG END");
- if let Some(on_drag_end) = &props.on_drag_end {
- on_drag_end.emit(event.into());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(event.into(), GesturePhase::End));
}
let (point0, point1) = self.get_pinch_points();
self.call_on_pinch_zoom(ctx, point0, point1, GesturePhase::Start);
@@ -771,8 +776,8 @@ impl PwtGestureDetector {
}
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());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(touch.into(), GesturePhase::End));
}
}
});
@@ -792,8 +797,11 @@ impl PwtGestureDetector {
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(event.clone().into());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(
+ event.clone().into(),
+ GesturePhase::End,
+ ));
}
if let Some(on_swipe) = &props.on_swipe {
@@ -828,8 +836,11 @@ impl PwtGestureDetector {
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_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(
+ touch.clone().into(),
+ GesturePhase::End,
+ ));
}
if let Some(on_swipe) = &props.on_swipe {
@@ -865,8 +876,8 @@ impl PwtGestureDetector {
);
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(event.into());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(event.into(), GesturePhase::Update));
}
}
}
@@ -886,8 +897,11 @@ impl PwtGestureDetector {
);
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());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(
+ touch.into(),
+ GesturePhase::Update,
+ ));
}
}
}
@@ -899,8 +913,8 @@ impl PwtGestureDetector {
if let Some(_pointer_state) = self.unregister_pointer(event.pointer_id()) {
self.state = DetectionState::Initial;
//log::info!("DRAG END");
- if let Some(on_drag_end) = &props.on_drag_end {
- on_drag_end.emit(event.into());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(event.into(), GesturePhase::End));
}
}
}
@@ -909,8 +923,8 @@ impl PwtGestureDetector {
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());
+ if let Some(on_drag) = &props.on_drag {
+ on_drag.emit(GestureDragEvent::new(touch.into(), GesturePhase::End));
}
});
self.state = DetectionState::Initial;
diff --git a/src/touch/mod.rs b/src/touch/mod.rs
index 6d00ee7..34d5402 100644
--- a/src/touch/mod.rs
+++ b/src/touch/mod.rs
@@ -6,8 +6,8 @@ pub use application_bar::{ApplicationBar, PwtApplicationBar};
mod gesture_detector;
pub use gesture_detector::{
- GestureDetector, GesturePhase, GesturePinchZoomEvent, GestureSwipeEvent, InputEvent,
- PinchPoint, PwtGestureDetector,
+ GestureDetector, GestureDragEvent, GesturePhase, GesturePinchZoomEvent, GestureSwipeEvent,
+ InputEvent, PinchPoint, PwtGestureDetector,
};
mod fab;
diff --git a/src/touch/side_dialog.rs b/src/touch/side_dialog.rs
index 8760160..9bc23c9 100644
--- a/src/touch/side_dialog.rs
+++ b/src/touch/side_dialog.rs
@@ -10,10 +10,11 @@ use yew::virtual_dom::{Key, VComp, VNode};
use crate::dom::IntoHtmlElement;
use crate::props::{AsCssStylesMut, CssStyles};
use crate::state::{SharedState, SharedStateObserver};
+use crate::touch::GestureDragEvent;
use crate::widget::Container;
use crate::{impl_yew_std_props_builder, prelude::*};
-use super::{GestureDetector, GestureSwipeEvent, InputEvent};
+use super::{GestureDetector, GesturePhase, GestureSwipeEvent, InputEvent};
// Messages sent from the [SideDialogController].
pub enum SideDialogControllerMsg {
@@ -125,9 +126,7 @@ pub enum Msg {
Close,
Dismiss, // Slide out, then close
SliderAnimationEnd,
- DragStart(InputEvent),
- DragEnd(InputEvent),
- Drag(InputEvent),
+ Drag(GestureDragEvent),
Swipe(GestureSwipeEvent),
Controller,
}
@@ -262,43 +261,44 @@ impl Component for PwtSideDialog {
};
true
}
- Msg::DragStart(event) => {
+ Msg::Drag(event) => {
let x = event.x() as f64;
let y = event.y() as f64;
- if x > 0.0 && y > 0.0 {
- // prevent divide by zero
- self.drag_start = Some((x, y));
- self.drag_delta = Some((0.0, 0.0));
- }
- false
- }
- Msg::DragEnd(_event) => {
- let mut dismiss = false;
- let threshold = 100.0;
- if let Some((delta_x, delta_y)) = self.drag_delta {
- dismiss = match props.location {
- SideDialogLocation::Left => delta_x < -threshold,
- SideDialogLocation::Right => delta_x > threshold,
- SideDialogLocation::Top => delta_y < -threshold,
- SideDialogLocation::Bottom => delta_y > threshold,
- };
- }
- self.drag_start = None;
- self.drag_delta = None;
-
- if dismiss {
- ctx.link().send_message(Msg::Dismiss);
- }
- true
- }
- Msg::Drag(event) => {
- if let Some(start) = &self.drag_start {
- let x = event.x() as f64;
- let y = event.y() as f64;
+ match event.phase {
+ GesturePhase::Start => {
+ if x > 0.0 && y > 0.0 {
+ // prevent divide by zero
+ self.drag_start = Some((x, y));
+ self.drag_delta = Some((0.0, 0.0));
+ }
+ false
+ }
+ GesturePhase::Update => {
+ if let Some(start) = &self.drag_start {
+ self.drag_delta = Some((x - start.0, y - start.1));
+ }
+ true
+ }
+ GesturePhase::End => {
+ let mut dismiss = false;
+ let threshold = 100.0;
+ if let Some((delta_x, delta_y)) = self.drag_delta {
+ dismiss = match props.location {
+ SideDialogLocation::Left => delta_x < -threshold,
+ SideDialogLocation::Right => delta_x > threshold,
+ SideDialogLocation::Top => delta_y < -threshold,
+ SideDialogLocation::Bottom => delta_y > threshold,
+ };
+ }
+ self.drag_start = None;
+ self.drag_delta = None;
- self.drag_delta = Some((x - start.0, y - start.1));
+ if dismiss {
+ ctx.link().send_message(Msg::Dismiss);
+ }
+ true
+ }
}
- true
}
Msg::Swipe(event) => {
let angle = event.direction; // -180 to + 180
@@ -421,9 +421,7 @@ impl Component for PwtSideDialog {
}
}
})
- .on_drag_start(ctx.link().callback(Msg::DragStart))
- .on_drag_end(ctx.link().callback(Msg::DragEnd))
- .on_drag_update(ctx.link().callback(Msg::Drag))
+ .on_drag(ctx.link().callback(Msg::Drag))
.on_swipe(ctx.link().callback(Msg::Swipe));
let controller_context = html! {
diff --git a/src/touch/slidable/mod.rs b/src/touch/slidable/mod.rs
index ff77bbd..ae45e29 100644
--- a/src/touch/slidable/mod.rs
+++ b/src/touch/slidable/mod.rs
@@ -15,7 +15,9 @@ use yew::virtual_dom::VNode;
use crate::dom::DomSizeObserver;
use crate::prelude::*;
use crate::props::CssLength;
-use crate::touch::{GestureDetector, GestureSwipeEvent, InputEvent};
+use crate::touch::{
+ GestureDetector, GestureDragEvent, GesturePhase, GestureSwipeEvent, InputEvent,
+};
use crate::widget::{Container, Row};
use pwt_macros::widget;
@@ -133,9 +135,7 @@ pub struct PwtSlidable {
pub enum Msg {
StartDismissTransition,
- Drag(InputEvent),
- DragStart(InputEvent),
- DragEnd(InputEvent),
+ Drag(GestureDragEvent),
Swipe(GestureSwipeEvent),
LeftResize(f64),
RightResize(f64),
@@ -245,17 +245,15 @@ impl Component for PwtSlidable {
Msg::StartDismissTransition => {
self.view_state = ViewState::DismissTransition;
}
- Msg::Drag(event) => {
- self.drag_pos = Some(self.drag_start - event.x());
- }
- Msg::DragStart(event) => {
- self.drag_start = event.x();
- }
- Msg::DragEnd(_event) => {
- self.drag_start = 0;
- self.start_pos -= self.drag_pos.take().unwrap_or(0) as f64;
- self.finalize_drag();
- }
+ Msg::Drag(event) => match event.phase {
+ GesturePhase::Start => self.drag_start = event.x(),
+ GesturePhase::Update => self.drag_pos = Some(self.drag_start - event.x()),
+ GesturePhase::End => {
+ self.drag_start = 0;
+ self.start_pos -= self.drag_pos.take().unwrap_or(0) as f64;
+ self.finalize_drag();
+ }
+ },
Msg::ContentResize(width, height) => {
if self.start_pos == 0f64 && self.drag_pos.is_none() && !self.switch_back {
self.content_width = width.max(0f64);
@@ -379,9 +377,7 @@ impl Component for PwtSlidable {
.with_child(props.content.clone())
.into_html_with_ref(self.content_ref.clone()),
)
- .on_drag_start(ctx.link().callback(Msg::DragStart))
- .on_drag_end(ctx.link().callback(Msg::DragEnd))
- .on_drag_update(ctx.link().callback(Msg::Drag))
+ .on_drag(ctx.link().callback(Msg::Drag))
.on_tap(ctx.link().callback(Msg::OnTap))
.on_swipe(ctx.link().callback(Msg::Swipe));
--
2.47.3
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH yew-widget-toolkit v2 3/4] touch: gesture detector: derive Clone and PartialEq for Events
2026-04-16 8:47 [PATCH yew-widget-toolkit v2 1/4] touch: gesture detector: implement pinch zoom gesture Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 2/4] touch: gesture detector: unify on_drag{_start,_update,_end} callbacks Dominik Csapak
@ 2026-04-16 8:47 ` Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 4/4] touch: gesture detector: add missing comment for swipe callback Dominik Csapak
2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-04-16 8:47 UTC (permalink / raw)
To: yew-devel
Since these are exposed via the callbacks, it's useful to derive at
least these traits.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v2
src/touch/gesture_detector.rs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/touch/gesture_detector.rs b/src/touch/gesture_detector.rs
index d3b4452..47a0850 100644
--- a/src/touch/gesture_detector.rs
+++ b/src/touch/gesture_detector.rs
@@ -19,6 +19,7 @@ use crate::widget::Container;
/// An event that can happen from a [`PointerEvent`] or a [`Touch`]
///
/// For convenience, expose the most important values from the underlying events
+#[derive(Clone, PartialEq)]
pub enum InputEvent {
PointerEvent(PointerEvent),
Touch(Touch),
@@ -67,6 +68,7 @@ impl From<Touch> for InputEvent {
}
/// Like [PointerEvent](web_sys::PointerEvent), but includes the swipe direction
+#[derive(Clone, PartialEq)]
pub struct GestureSwipeEvent {
event: InputEvent,
/// Direction angle (from -180 to +180 degree)
--
2.47.3
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH yew-widget-toolkit v2 4/4] touch: gesture detector: add missing comment for swipe callback
2026-04-16 8:47 [PATCH yew-widget-toolkit v2 1/4] touch: gesture detector: implement pinch zoom gesture Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 2/4] touch: gesture detector: unify on_drag{_start,_update,_end} callbacks Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 3/4] touch: gesture detector: derive Clone and PartialEq for Events Dominik Csapak
@ 2026-04-16 8:47 ` Dominik Csapak
2 siblings, 0 replies; 4+ messages in thread
From: Dominik Csapak @ 2026-04-16 8:47 UTC (permalink / raw)
To: yew-devel
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
new in v2
src/touch/gesture_detector.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/touch/gesture_detector.rs b/src/touch/gesture_detector.rs
index 47a0850..17bf072 100644
--- a/src/touch/gesture_detector.rs
+++ b/src/touch/gesture_detector.rs
@@ -217,6 +217,7 @@ pub struct GestureDetector {
#[prop_or_default]
pub on_drag: Option<Callback<GestureDragEvent>>,
+ /// Callback for swipe events.
#[prop_or_default]
pub on_swipe: Option<Callback<GestureSwipeEvent>>,
--
2.47.3
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-04-16 8:59 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-16 8:47 [PATCH yew-widget-toolkit v2 1/4] touch: gesture detector: implement pinch zoom gesture Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 2/4] touch: gesture detector: unify on_drag{_start,_update,_end} callbacks Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 3/4] touch: gesture detector: derive Clone and PartialEq for Events Dominik Csapak
2026-04-16 8:47 ` [PATCH yew-widget-toolkit v2 4/4] touch: gesture detector: add missing comment for swipe callback Dominik Csapak
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.