Skip to main content

yew_hooks/hooks/
use_long_press.rs

1use std::rc::Rc;
2
3use gloo::timers::callback::Timeout;
4use yew::prelude::*;
5
6use super::{use_event, use_event_with_window, use_mut_latest};
7
8/// Options for long press.
9#[derive(Default)]
10pub struct UseLongPressOptions {
11    /// Callback for when long press starts.
12    pub onstart: Option<Box<dyn FnMut(MouseEvent)>>,
13    /// Callback for when long press ends.
14    pub onend: Option<Box<dyn FnMut(MouseEvent)>>,
15    /// Callback for when long press is completed (after threshold).
16    pub onlongpress: Option<Box<dyn FnMut(MouseEvent)>>,
17    /// Duration in milliseconds before long press is triggered.
18    pub threshold: Option<u32>,
19    /// Maximum movement in pixels allowed during press. If exceeded, long press won't trigger.
20    pub move_threshold: Option<f64>,
21    /// Whether to prevent default behavior on mouse/touch events.
22    pub prevent_default: Option<bool>,
23}
24
25/// State handle for the [`use_long_press`] hook.
26pub struct UseLongPressHandle {
27    /// Whether the element is currently being pressed.
28    pub pressing: UseStateHandle<bool>,
29    /// Whether the long press threshold has been reached.
30    pub long_pressed: UseStateHandle<bool>,
31    /// Cancel the current long press timeout.
32    cancel: Rc<dyn Fn()>,
33}
34
35impl UseLongPressHandle {
36    /// Cancel the current long press timeout.
37    pub fn cancel(&self) {
38        (self.cancel)();
39    }
40}
41
42impl Clone for UseLongPressHandle {
43    fn clone(&self) -> Self {
44        Self {
45            pressing: self.pressing.clone(),
46            long_pressed: self.long_pressed.clone(),
47            cancel: self.cancel.clone(),
48        }
49    }
50}
51
52/// A hook that detects when a user presses and holds an element for a specified duration.
53///
54/// # Example
55///
56/// ```rust
57/// # use yew::prelude::*;
58/// # use log::debug;
59/// #
60/// use yew_hooks::prelude::*;
61///
62/// #[function_component(UseLongPress)]
63/// fn long_press() -> Html {
64///     let button = use_node_ref();
65///
66///     let long_press = use_long_press(button.clone(), 1000, move |e: MouseEvent| {
67///         debug!("Long pressed for 1 second!");
68///     });
69///
70///     let onmouseup = {
71///         let long_press = long_press.clone();
72///         Callback::from(move |_| {
73///             long_press.cancel();
74///         })
75///     };
76///
77///     html! {
78///         <>
79///             <button
80///                 ref={button}
81///                 onmouseup={onmouseup}
82///             >
83///                 { "Press and hold me for 1 second" }
84///             </button>
85///             <p>
86///                 { if *long_press.pressing { "Pressing..." } else { "Not pressing" } }
87///             </p>
88///             <p>
89///                 { if *long_press.long_pressed { "Long pressed!" } else { "Not long pressed yet" } }
90///             </p>
91///         </>
92///     }
93/// }
94/// ```
95#[hook]
96pub fn use_long_press<F>(node: NodeRef, threshold: u32, onlongpress: F) -> UseLongPressHandle
97where
98    F: FnMut(MouseEvent) + 'static,
99{
100    let options = UseLongPressOptions {
101        threshold: Some(threshold),
102        onlongpress: Some(Box::new(onlongpress)),
103        ..Default::default()
104    };
105
106    use_long_press_with_options(node, options)
107}
108
109/// A hook that detects when a user presses and holds an element for a specified duration with options.
110///
111/// # Example
112///
113/// ```rust
114/// # use yew::prelude::*;
115/// # use log::debug;
116/// #
117/// use yew_hooks::prelude::*;
118///
119/// #[function_component(UseLongPressWithOptions)]
120/// fn long_press_with_options() -> Html {
121///     let button = use_node_ref();
122///
123///     let long_press = use_long_press_with_options(
124///         button.clone(),
125///         UseLongPressOptions {
126///             threshold: Some(1000),
127///             move_threshold: None,
128///             onstart: Some(Box::new(|e: MouseEvent| {
129///                 debug!("Press started");
130///             })),
131///             onend: Some(Box::new(|e: MouseEvent| {
132///                 debug!("Press ended");
133///             })),
134///             onlongpress: Some(Box::new(|e: MouseEvent| {
135///                 debug!("Long pressed!");
136///             })),
137///             prevent_default: Some(true),
138///         }
139///     );
140///
141///     html! {
142///         <>
143///             <button ref={button}>
144///                 { "Press and hold me" }
145///             </button>
146///             <p>
147///                 { if *long_press.pressing { "Pressing..." } else { "Not pressing" } }
148///             </p>
149///         </>
150///     }
151/// }
152/// ```
153#[hook]
154pub fn use_long_press_with_options(
155    node: NodeRef,
156    options: UseLongPressOptions,
157) -> UseLongPressHandle {
158    let pressing = use_state(|| false);
159    let long_pressed = use_state(|| false);
160    let timeout_ref = use_mut_ref(|| None::<Timeout>);
161
162    let onstart_ref = use_mut_latest(options.onstart);
163    let onend_ref = use_mut_latest(options.onend);
164    let onlongpress_ref = use_mut_latest(options.onlongpress);
165    let threshold = options.threshold.unwrap_or(500);
166    let move_threshold = options.move_threshold;
167    let prevent_default = options.prevent_default.unwrap_or(true);
168
169    let start_coords = use_mut_ref(|| (0.0, 0.0));
170
171    let cancel = {
172        let timeout_ref = timeout_ref.clone();
173        let pressing = pressing.clone();
174        let long_pressed = long_pressed.clone();
175        // onend_ref is used in the closure below
176
177        Rc::new(move || {
178            // Clear any existing timeout
179            *timeout_ref.borrow_mut() = None;
180
181            // Reset states
182            if *pressing {
183                pressing.set(false);
184            }
185            if *long_pressed {
186                long_pressed.set(false);
187            }
188        })
189    };
190
191    // Handle mousedown/touchstart
192    {
193        let pressing = pressing.clone();
194        let long_pressed = long_pressed.clone();
195        let timeout_ref = timeout_ref.clone();
196        let onstart_ref = onstart_ref.clone();
197        let onlongpress_ref = onlongpress_ref.clone();
198        // cancel is used in the closure below
199
200        let start_coords_clone = start_coords.clone();
201        use_event(node.clone(), "mousedown", move |e: MouseEvent| {
202            if prevent_default {
203                e.prevent_default();
204            }
205
206            // Reset states
207            pressing.set(true);
208            long_pressed.set(false);
209
210            // Store starting coordinates for move threshold check
211            *start_coords_clone.borrow_mut() = (e.client_x() as f64, e.client_y() as f64);
212
213            // Call onstart callback
214            let onstart_ref = onstart_ref.current();
215            let onstart = &mut *onstart_ref.borrow_mut();
216            if let Some(onstart) = onstart {
217                onstart(e.clone());
218            }
219
220            // Clear any existing timeout
221            *timeout_ref.borrow_mut() = None;
222
223            // Set new timeout for long press
224            let e_clone = e.clone();
225            let long_pressed_clone = long_pressed.clone();
226            let onlongpress_ref_clone = onlongpress_ref.clone();
227            let start_coords_clone2 = start_coords_clone.clone();
228            let move_threshold = move_threshold;
229            let timeout = Timeout::new(threshold, move || {
230                // Check if movement exceeded threshold
231                if let Some(move_threshold) = move_threshold {
232                    let (start_x, start_y) = *start_coords_clone2.borrow();
233                    let current_x = e_clone.client_x() as f64;
234                    let current_y = e_clone.client_y() as f64;
235                    let distance =
236                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
237
238                    if distance > move_threshold {
239                        return; // Don't trigger long press if moved too far
240                    }
241                }
242
243                long_pressed_clone.set(true);
244
245                // Call onlongpress callback
246                let onlongpress_ref = onlongpress_ref_clone.current();
247                let onlongpress = &mut *onlongpress_ref.borrow_mut();
248                if let Some(onlongpress) = onlongpress {
249                    onlongpress(e_clone);
250                }
251            });
252
253            *timeout_ref.borrow_mut() = Some(timeout);
254        });
255    }
256
257    // Handle touchstart for mobile
258    {
259        let pressing = pressing.clone();
260        let long_pressed = long_pressed.clone();
261        let timeout_ref = timeout_ref.clone();
262        let onstart_ref = onstart_ref.clone();
263        let onlongpress_ref = onlongpress_ref.clone();
264        // cancel is used in the closure below
265
266        let start_coords_clone3 = start_coords.clone();
267        use_event(node.clone(), "touchstart", move |e: TouchEvent| {
268            if prevent_default {
269                e.prevent_default();
270            }
271
272            // Convert TouchEvent to MouseEvent for consistency
273            let mouse_event = MouseEvent::new("mousedown").unwrap();
274
275            // Reset states
276            pressing.set(true);
277            long_pressed.set(false);
278
279            // Store starting coordinates for move threshold check
280            if let Some(touch) = e.touches().get(0) {
281                *start_coords_clone3.borrow_mut() =
282                    (touch.client_x() as f64, touch.client_y() as f64);
283            }
284
285            // Call onstart callback
286            let onstart_ref = onstart_ref.current();
287            let onstart = &mut *onstart_ref.borrow_mut();
288            if let Some(onstart) = onstart {
289                onstart(mouse_event.clone());
290            }
291
292            // Clear any existing timeout
293            *timeout_ref.borrow_mut() = None;
294
295            // Set new timeout for long press
296            let mouse_event_clone = mouse_event.clone();
297            let long_pressed_clone = long_pressed.clone();
298            let onlongpress_ref_clone = onlongpress_ref.clone();
299            let start_coords_clone4 = start_coords_clone3.clone();
300            let move_threshold = move_threshold;
301            let timeout = Timeout::new(threshold, move || {
302                // Check if movement exceeded threshold
303                if let Some(move_threshold) = move_threshold {
304                    let (start_x, start_y) = *start_coords_clone4.borrow();
305                    // Get current touch position
306                    let current_x = e
307                        .changed_touches()
308                        .get(0)
309                        .map_or(start_x, |t| t.client_x() as f64);
310                    let current_y = e
311                        .changed_touches()
312                        .get(0)
313                        .map_or(start_y, |t| t.client_y() as f64);
314                    let distance =
315                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
316
317                    if distance > move_threshold {
318                        return; // Don't trigger long press if moved too far
319                    }
320                }
321
322                long_pressed_clone.set(true);
323
324                // Call onlongpress callback
325                let onlongpress_ref = onlongpress_ref_clone.current();
326                let onlongpress = &mut *onlongpress_ref.borrow_mut();
327                if let Some(onlongpress) = onlongpress {
328                    onlongpress(mouse_event_clone);
329                }
330            });
331
332            *timeout_ref.borrow_mut() = Some(timeout);
333        });
334    }
335
336    // Handle mouseup/touchend
337    {
338        let pressing = pressing.clone();
339        let long_pressed = long_pressed.clone();
340        let timeout_ref = timeout_ref.clone();
341        let onend_ref = onend_ref.clone();
342        // cancel is used in the closure below
343
344        let pressing_clone = pressing.clone();
345        let timeout_ref_clone = timeout_ref.clone();
346        let long_pressed_clone = long_pressed.clone();
347        let start_coords_clone5 = start_coords.clone();
348        use_event_with_window("mousemove", move |e: MouseEvent| {
349            if *pressing_clone && move_threshold.is_some() {
350                let (start_x, start_y) = *start_coords_clone5.borrow();
351                let current_x = e.client_x() as f64;
352                let current_y = e.client_y() as f64;
353                let distance =
354                    ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
355
356                if let Some(move_threshold) = move_threshold {
357                    if distance > move_threshold {
358                        // Cancel the long press if movement exceeds threshold
359                        *timeout_ref_clone.borrow_mut() = None;
360                        pressing_clone.set(false);
361                        long_pressed_clone.set(false);
362                    }
363                }
364            }
365        });
366
367        let pressing_clone2 = pressing.clone();
368        let long_pressed_clone2 = long_pressed.clone();
369        let timeout_ref_clone2 = timeout_ref.clone();
370        let onend_ref_clone = onend_ref.clone();
371        use_event_with_window("mouseup", move |e: MouseEvent| {
372            if *pressing_clone2 {
373                // Clear timeout
374                *timeout_ref_clone2.borrow_mut() = None;
375
376                // Update states
377                pressing_clone2.set(false);
378
379                // Call onend callback if not long pressed
380                if !*long_pressed_clone2 {
381                    let onend_ref = onend_ref_clone.current();
382                    let onend = &mut *onend_ref.borrow_mut();
383                    if let Some(onend) = onend {
384                        onend(e);
385                    }
386                }
387
388                // Reset long pressed state
389                long_pressed_clone2.set(false);
390            }
391        });
392    }
393
394    // Handle touchend for mobile
395    {
396        let pressing = pressing.clone();
397        let long_pressed = long_pressed.clone();
398        let timeout_ref = timeout_ref.clone();
399        let onend_ref = onend_ref.clone();
400        // cancel is used in the closure below
401
402        let pressing_clone3 = pressing.clone();
403        let timeout_ref_clone3 = timeout_ref.clone();
404        let long_pressed_clone3 = long_pressed.clone();
405        let start_coords_clone6 = start_coords.clone();
406        use_event_with_window("touchmove", move |e: TouchEvent| {
407            if *pressing_clone3 && move_threshold.is_some() {
408                let (start_x, start_y) = *start_coords_clone6.borrow();
409                if let Some(touch) = e.touches().get(0) {
410                    let current_x = touch.client_x() as f64;
411                    let current_y = touch.client_y() as f64;
412                    let distance =
413                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
414
415                    if let Some(move_threshold) = move_threshold {
416                        if distance > move_threshold {
417                            // Cancel the long press if movement exceeds threshold
418                            *timeout_ref_clone3.borrow_mut() = None;
419                            pressing_clone3.set(false);
420                            long_pressed_clone3.set(false);
421                        }
422                    }
423                }
424            }
425        });
426
427        let pressing_clone4 = pressing.clone();
428        let long_pressed_clone4 = long_pressed.clone();
429        let timeout_ref_clone4 = timeout_ref.clone();
430        let onend_ref_clone2 = onend_ref.clone();
431        use_event_with_window("touchend", move |_e: TouchEvent| {
432            if *pressing_clone4 {
433                // Convert TouchEvent to MouseEvent for consistency
434                let mouse_event = MouseEvent::new("mouseup").unwrap();
435
436                // Clear timeout
437                *timeout_ref_clone4.borrow_mut() = None;
438
439                // Update states
440                pressing_clone4.set(false);
441
442                // Call onend callback if not long pressed
443                if !*long_pressed_clone4 {
444                    let onend_ref = onend_ref_clone2.current();
445                    let onend = &mut *onend_ref.borrow_mut();
446                    if let Some(onend) = onend {
447                        onend(mouse_event);
448                    }
449                }
450
451                // Reset long pressed state
452                long_pressed_clone4.set(false);
453            }
454        });
455    }
456
457    // Handle mouseleave/touchcancel
458    {
459        let cancel = cancel.clone();
460
461        use_event(node.clone(), "mouseleave", move |_: MouseEvent| {
462            cancel();
463        });
464    }
465
466    {
467        let cancel = cancel.clone();
468
469        use_event(node.clone(), "touchcancel", move |_: TouchEvent| {
470            cancel();
471        });
472    }
473
474    // Cleanup on unmount
475    {
476        let cancel = cancel.clone();
477        use_effect_with((), move |_| {
478            move || {
479                cancel();
480            }
481        });
482    }
483
484    UseLongPressHandle {
485        pressing,
486        long_pressed,
487        cancel,
488    }
489}