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            #[allow(clippy::unnecessary_cast)]
212            {
213                *start_coords_clone.borrow_mut() = (e.client_x() as f64, e.client_y() as f64);
214            }
215
216            // Call onstart callback
217            let onstart_ref = onstart_ref.current();
218            let onstart = &mut *onstart_ref.borrow_mut();
219            if let Some(onstart) = onstart {
220                onstart(e.clone());
221            }
222
223            // Clear any existing timeout
224            *timeout_ref.borrow_mut() = None;
225
226            // Set new timeout for long press
227            let e_clone = e.clone();
228            let long_pressed_clone = long_pressed.clone();
229            let onlongpress_ref_clone = onlongpress_ref.clone();
230            let start_coords_clone2 = start_coords_clone.clone();
231            let move_threshold = move_threshold;
232            let timeout = Timeout::new(threshold, move || {
233                // Check if movement exceeded threshold
234                if let Some(move_threshold) = move_threshold {
235                    let (start_x, start_y) = *start_coords_clone2.borrow();
236                    #[allow(clippy::unnecessary_cast)]
237                    let current_x = e_clone.client_x() as f64;
238                    #[allow(clippy::unnecessary_cast)]
239                    let current_y = e_clone.client_y() as f64;
240                    let distance =
241                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
242
243                    if distance > move_threshold {
244                        return; // Don't trigger long press if moved too far
245                    }
246                }
247
248                long_pressed_clone.set(true);
249
250                // Call onlongpress callback
251                let onlongpress_ref = onlongpress_ref_clone.current();
252                let onlongpress = &mut *onlongpress_ref.borrow_mut();
253                if let Some(onlongpress) = onlongpress {
254                    onlongpress(e_clone);
255                }
256            });
257
258            *timeout_ref.borrow_mut() = Some(timeout);
259        });
260    }
261
262    // Handle touchstart for mobile
263    {
264        let pressing = pressing.clone();
265        let long_pressed = long_pressed.clone();
266        let timeout_ref = timeout_ref.clone();
267        let onstart_ref = onstart_ref.clone();
268        let onlongpress_ref = onlongpress_ref.clone();
269        // cancel is used in the closure below
270
271        let start_coords_clone3 = start_coords.clone();
272        use_event(node.clone(), "touchstart", move |e: TouchEvent| {
273            if prevent_default {
274                e.prevent_default();
275            }
276
277            // Convert TouchEvent to MouseEvent for consistency
278            let mouse_event = MouseEvent::new("mousedown").unwrap();
279
280            // Reset states
281            pressing.set(true);
282            long_pressed.set(false);
283
284            // Store starting coordinates for move threshold check
285            if let Some(touch) = e.touches().get(0) {
286                *start_coords_clone3.borrow_mut() =
287                    (touch.client_x() as f64, touch.client_y() as f64);
288            }
289
290            // Call onstart callback
291            let onstart_ref = onstart_ref.current();
292            let onstart = &mut *onstart_ref.borrow_mut();
293            if let Some(onstart) = onstart {
294                onstart(mouse_event.clone());
295            }
296
297            // Clear any existing timeout
298            *timeout_ref.borrow_mut() = None;
299
300            // Set new timeout for long press
301            let mouse_event_clone = mouse_event.clone();
302            let long_pressed_clone = long_pressed.clone();
303            let onlongpress_ref_clone = onlongpress_ref.clone();
304            let start_coords_clone4 = start_coords_clone3.clone();
305            let move_threshold = move_threshold;
306            let timeout = Timeout::new(threshold, move || {
307                // Check if movement exceeded threshold
308                if let Some(move_threshold) = move_threshold {
309                    let (start_x, start_y) = *start_coords_clone4.borrow();
310                    // Get current touch position
311                    let current_x = e
312                        .changed_touches()
313                        .get(0)
314                        .map_or(start_x, |t| t.client_x() as f64);
315                    let current_y = e
316                        .changed_touches()
317                        .get(0)
318                        .map_or(start_y, |t| t.client_y() as f64);
319                    let distance =
320                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
321
322                    if distance > move_threshold {
323                        return; // Don't trigger long press if moved too far
324                    }
325                }
326
327                long_pressed_clone.set(true);
328
329                // Call onlongpress callback
330                let onlongpress_ref = onlongpress_ref_clone.current();
331                let onlongpress = &mut *onlongpress_ref.borrow_mut();
332                if let Some(onlongpress) = onlongpress {
333                    onlongpress(mouse_event_clone);
334                }
335            });
336
337            *timeout_ref.borrow_mut() = Some(timeout);
338        });
339    }
340
341    // Handle mouseup/touchend
342    {
343        let pressing = pressing.clone();
344        let long_pressed = long_pressed.clone();
345        let timeout_ref = timeout_ref.clone();
346        let onend_ref = onend_ref.clone();
347        // cancel is used in the closure below
348
349        let pressing_clone = pressing.clone();
350        let timeout_ref_clone = timeout_ref.clone();
351        let long_pressed_clone = long_pressed.clone();
352        let start_coords_clone5 = start_coords.clone();
353        use_event_with_window("mousemove", move |e: MouseEvent| {
354            if *pressing_clone && move_threshold.is_some() {
355                let (start_x, start_y) = *start_coords_clone5.borrow();
356                #[allow(clippy::unnecessary_cast)]
357                let current_x = e.client_x() as f64;
358                #[allow(clippy::unnecessary_cast)]
359                let current_y = e.client_y() as f64;
360                let distance =
361                    ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
362
363                if let Some(move_threshold) = move_threshold {
364                    if distance > move_threshold {
365                        // Cancel the long press if movement exceeds threshold
366                        *timeout_ref_clone.borrow_mut() = None;
367                        pressing_clone.set(false);
368                        long_pressed_clone.set(false);
369                    }
370                }
371            }
372        });
373
374        let pressing_clone2 = pressing.clone();
375        let long_pressed_clone2 = long_pressed.clone();
376        let timeout_ref_clone2 = timeout_ref.clone();
377        let onend_ref_clone = onend_ref.clone();
378        use_event_with_window("mouseup", move |e: MouseEvent| {
379            if *pressing_clone2 {
380                // Clear timeout
381                *timeout_ref_clone2.borrow_mut() = None;
382
383                // Update states
384                pressing_clone2.set(false);
385
386                // Call onend callback if not long pressed
387                if !*long_pressed_clone2 {
388                    let onend_ref = onend_ref_clone.current();
389                    let onend = &mut *onend_ref.borrow_mut();
390                    if let Some(onend) = onend {
391                        onend(e);
392                    }
393                }
394
395                // Reset long pressed state
396                long_pressed_clone2.set(false);
397            }
398        });
399    }
400
401    // Handle touchend for mobile
402    {
403        let pressing = pressing.clone();
404        let long_pressed = long_pressed.clone();
405        let timeout_ref = timeout_ref.clone();
406        let onend_ref = onend_ref.clone();
407        // cancel is used in the closure below
408
409        let pressing_clone3 = pressing.clone();
410        let timeout_ref_clone3 = timeout_ref.clone();
411        let long_pressed_clone3 = long_pressed.clone();
412        let start_coords_clone6 = start_coords.clone();
413        use_event_with_window("touchmove", move |e: TouchEvent| {
414            if *pressing_clone3 && move_threshold.is_some() {
415                let (start_x, start_y) = *start_coords_clone6.borrow();
416                if let Some(touch) = e.touches().get(0) {
417                    let current_x = touch.client_x() as f64;
418                    let current_y = touch.client_y() as f64;
419                    let distance =
420                        ((current_x - start_x).powi(2) + (current_y - start_y).powi(2)).sqrt();
421
422                    if let Some(move_threshold) = move_threshold {
423                        if distance > move_threshold {
424                            // Cancel the long press if movement exceeds threshold
425                            *timeout_ref_clone3.borrow_mut() = None;
426                            pressing_clone3.set(false);
427                            long_pressed_clone3.set(false);
428                        }
429                    }
430                }
431            }
432        });
433
434        let pressing_clone4 = pressing.clone();
435        let long_pressed_clone4 = long_pressed.clone();
436        let timeout_ref_clone4 = timeout_ref.clone();
437        let onend_ref_clone2 = onend_ref.clone();
438        use_event_with_window("touchend", move |_e: TouchEvent| {
439            if *pressing_clone4 {
440                // Convert TouchEvent to MouseEvent for consistency
441                let mouse_event = MouseEvent::new("mouseup").unwrap();
442
443                // Clear timeout
444                *timeout_ref_clone4.borrow_mut() = None;
445
446                // Update states
447                pressing_clone4.set(false);
448
449                // Call onend callback if not long pressed
450                if !*long_pressed_clone4 {
451                    let onend_ref = onend_ref_clone2.current();
452                    let onend = &mut *onend_ref.borrow_mut();
453                    if let Some(onend) = onend {
454                        onend(mouse_event);
455                    }
456                }
457
458                // Reset long pressed state
459                long_pressed_clone4.set(false);
460            }
461        });
462    }
463
464    // Handle mouseleave/touchcancel
465    {
466        let cancel = cancel.clone();
467
468        use_event(node.clone(), "mouseleave", move |_: MouseEvent| {
469            cancel();
470        });
471    }
472
473    {
474        let cancel = cancel.clone();
475
476        use_event(node.clone(), "touchcancel", move |_: TouchEvent| {
477            cancel();
478        });
479    }
480
481    // Cleanup on unmount
482    {
483        let cancel = cancel.clone();
484        use_effect_with((), move |_| {
485            move || {
486                cancel();
487            }
488        });
489    }
490
491    UseLongPressHandle {
492        pressing,
493        long_pressed,
494        cancel,
495    }
496}