leptos_use/
use_mouse.rs

1#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
2
3use crate::core::{IntoElementMaybeSignal, Position};
4use crate::{UseEventListenerOptions, UseWindow, use_event_listener_with_options, use_window};
5use default_struct_builder::DefaultBuilder;
6use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart};
7use leptos::prelude::*;
8use std::convert::Infallible;
9use std::marker::PhantomData;
10use wasm_bindgen::{JsCast, JsValue};
11
12/// Reactive mouse position
13///
14/// ## Demo
15///
16/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_mouse)
17///
18/// ## Basic Usage
19///
20/// ```
21/// # use leptos::prelude::*;
22/// # use leptos_use::{use_mouse, UseMouseReturn};
23/// #
24/// # #[component]
25/// # fn Demo() -> impl IntoView {
26/// let UseMouseReturn {
27///     x, y, source_type, ..
28/// } = use_mouse();
29/// # view! { }
30/// # }
31/// ```
32///
33/// Touch is enabled by default. To only detect mouse changes, set `touch` to `false`.
34/// The `dragover` event is used to track mouse position while dragging.
35///
36/// ```
37/// # use leptos::prelude::*;
38/// # use leptos_use::{use_mouse_with_options, UseMouseOptions, UseMouseReturn};
39/// #
40/// # #[component]
41/// # fn Demo() -> impl IntoView {
42/// let UseMouseReturn {
43///     x, y, ..
44/// } = use_mouse_with_options(
45///     UseMouseOptions::default().touch(false)
46/// );
47/// # view! { }
48/// # }
49/// ```
50///
51/// ## Custom Extractor
52///
53/// It's also possible to provide a custom extractor to get the position from the events.
54///
55/// ```
56/// # use leptos::prelude::*;
57/// # use leptos::html::Div;
58/// use web_sys::MouseEvent;
59/// use leptos_use::{use_mouse_with_options, UseMouseOptions, UseMouseReturn, UseMouseEventExtractor, UseMouseCoordType};
60///
61/// #[derive(Clone)]
62/// struct MyExtractor;
63///
64/// impl UseMouseEventExtractor for MyExtractor {
65///     fn extract_mouse_coords(&self, event: &MouseEvent) -> Option<(f64, f64)> {
66///         Some((event.offset_x() as f64, event.offset_y() as f64))
67///     }
68///
69///     // don't implement fn extract_touch_coords to ignore touch events
70/// }
71///
72/// #[component]
73/// fn Demo() -> impl IntoView {
74///     let element = NodeRef::<Div>::new();
75///
76///     let UseMouseReturn {
77///         x, y, source_type, ..
78///     } = use_mouse_with_options(
79///         UseMouseOptions::default()
80///             .target(element)
81///             .coord_type(UseMouseCoordType::Custom(MyExtractor))
82///     );
83///     view! { <div node_ref=element></div> }
84/// }
85/// ```
86///
87/// ## Server-Side Rendering
88///
89/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
90///
91/// On the server this returns simple `Signal`s with the `initial_value`s.
92pub fn use_mouse() -> UseMouseReturn {
93    use_mouse_with_options(Default::default())
94}
95
96/// Variant of [`use_mouse`] that accepts options. Please see [`use_mouse`] for how to use.
97pub fn use_mouse_with_options<El, M, Ex>(options: UseMouseOptions<El, M, Ex>) -> UseMouseReturn
98where
99    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
100    Ex: UseMouseEventExtractor + Clone + 'static,
101{
102    let (x, set_x) = signal(options.initial_value.x);
103    let (y, set_y) = signal(options.initial_value.y);
104    let (source_type, set_source_type) = signal(UseMouseSourceType::Unset);
105
106    let mouse_handler = {
107        let coord_type = options.coord_type.clone();
108
109        move |event: web_sys::MouseEvent| {
110            let result = coord_type.extract_mouse_coords(&event);
111
112            if let Some((x, y)) = result {
113                set_x.set(x);
114                set_y.set(y);
115                set_source_type.set(UseMouseSourceType::Mouse);
116            }
117        }
118    };
119
120    let drag_handler = {
121        let mouse_handler = mouse_handler.clone();
122
123        move |event: web_sys::DragEvent| {
124            let js_value: &JsValue = event.as_ref();
125            mouse_handler(js_value.clone().unchecked_into::<web_sys::MouseEvent>());
126        }
127    };
128
129    let touch_handler = {
130        let coord_type = options.coord_type.clone();
131
132        move |event: web_sys::TouchEvent| {
133            let touches = event.touches();
134            if touches.length() > 0 {
135                let result = coord_type.extract_touch_coords(
136                    &touches
137                        .get(0)
138                        .expect("Just checked that there's at least on touch"),
139                );
140
141                if let Some((x, y)) = result {
142                    set_x.set(x);
143                    set_y.set(y);
144                    set_source_type.set(UseMouseSourceType::Touch);
145                }
146            }
147        }
148    };
149
150    let initial_value = options.initial_value;
151    let reset = move || {
152        set_x.set(initial_value.x);
153        set_y.set(initial_value.y);
154    };
155
156    // TODO : event filters?
157
158    #[cfg(not(feature = "ssr"))]
159    {
160        let target = options.target.into_element_maybe_signal();
161        let event_listener_options = UseEventListenerOptions::default().passive(true);
162
163        let _ = use_event_listener_with_options(
164            target,
165            mousemove,
166            mouse_handler,
167            event_listener_options,
168        );
169        let _ =
170            use_event_listener_with_options(target, dragover, drag_handler, event_listener_options);
171
172        if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) {
173            let _ = use_event_listener_with_options(
174                target,
175                touchstart,
176                touch_handler.clone(),
177                event_listener_options,
178            );
179            let _ = use_event_listener_with_options(
180                target,
181                touchmove,
182                touch_handler,
183                event_listener_options,
184            );
185            if options.reset_on_touch_ends {
186                let _ = use_event_listener_with_options(
187                    target,
188                    touchend,
189                    move |_| reset(),
190                    event_listener_options,
191                );
192            }
193        }
194    }
195
196    UseMouseReturn {
197        x: x.into(),
198        y: y.into(),
199        set_x,
200        set_y,
201        source_type: source_type.into(),
202    }
203}
204
205#[derive(DefaultBuilder)]
206/// Options for [`use_mouse_with_options`].
207pub struct UseMouseOptions<El, M, Ex>
208where
209    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
210    Ex: UseMouseEventExtractor + Clone,
211{
212    /// How to extract the x, y coordinates from mouse events or touches
213    coord_type: UseMouseCoordType<Ex>,
214
215    /// Listen events on `target` element. Defaults to `window`
216    target: El,
217
218    /// Listen to `touchmove` events. Defaults to `true`.
219    touch: bool,
220
221    /// Reset to initial value when `touchend` event fired. Defaults to `false`
222    reset_on_touch_ends: bool,
223
224    /// Initial values. Defaults to `{x: 0.0, y: 0.0}`.
225    initial_value: Position,
226
227    #[builder(skip)]
228    _marker: PhantomData<M>,
229}
230
231impl<M> Default for UseMouseOptions<UseWindow, M, Infallible>
232where
233    UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, M>,
234{
235    fn default() -> Self {
236        Self {
237            coord_type: UseMouseCoordType::default(),
238            target: use_window(),
239            touch: true,
240            reset_on_touch_ends: false,
241            initial_value: Position { x: 0.0, y: 0.0 },
242            _marker: PhantomData,
243        }
244    }
245}
246
247/// Defines how to get the coordinates from the event.
248#[derive(Clone, Default)]
249pub enum UseMouseCoordType<E: UseMouseEventExtractor + Clone> {
250    #[default]
251    Page,
252    Client,
253    Screen,
254    Movement,
255    Custom(E),
256}
257
258/// Trait to implement if you want to specify a custom extractor
259#[allow(unused_variables)]
260pub trait UseMouseEventExtractor {
261    /// Return the coordinates from mouse events (`Some(x, y)`) or `None`
262    fn extract_mouse_coords(&self, event: &web_sys::MouseEvent) -> Option<(f64, f64)> {
263        None
264    }
265
266    /// Return the coordinates from touches (`Some(x, y)`) or `None`
267    fn extract_touch_coords(&self, touch: &web_sys::Touch) -> Option<(f64, f64)> {
268        None
269    }
270}
271
272impl<E: UseMouseEventExtractor + Clone> UseMouseEventExtractor for UseMouseCoordType<E> {
273    fn extract_mouse_coords(&self, event: &web_sys::MouseEvent) -> Option<(f64, f64)> {
274        match self {
275            UseMouseCoordType::Page => Some((event.page_x() as f64, event.page_y() as f64)),
276            UseMouseCoordType::Client => Some((event.client_x() as f64, event.client_y() as f64)),
277            UseMouseCoordType::Screen => Some((event.screen_x() as f64, event.client_y() as f64)),
278            UseMouseCoordType::Movement => {
279                Some((event.movement_x() as f64, event.movement_y() as f64))
280            }
281            UseMouseCoordType::Custom(extractor) => extractor.extract_mouse_coords(event),
282        }
283    }
284
285    fn extract_touch_coords(&self, touch: &web_sys::Touch) -> Option<(f64, f64)> {
286        match self {
287            UseMouseCoordType::Page => Some((touch.page_x() as f64, touch.page_y() as f64)),
288            UseMouseCoordType::Client => Some((touch.client_x() as f64, touch.client_y() as f64)),
289            UseMouseCoordType::Screen => Some((touch.screen_x() as f64, touch.client_y() as f64)),
290            UseMouseCoordType::Movement => None,
291            UseMouseCoordType::Custom(extractor) => extractor.extract_touch_coords(touch),
292        }
293    }
294}
295
296impl UseMouseEventExtractor for Infallible {
297    fn extract_mouse_coords(&self, _: &web_sys::MouseEvent) -> Option<(f64, f64)> {
298        unreachable!()
299    }
300
301    fn extract_touch_coords(&self, _: &web_sys::Touch) -> Option<(f64, f64)> {
302        unreachable!()
303    }
304}
305
306/// Return type of [`use_mouse`].
307pub struct UseMouseReturn {
308    /// X coordinate of the mouse pointer / touch
309    pub x: Signal<f64>,
310    /// Y coordinate of the mouse pointer / touch
311    pub y: Signal<f64>,
312    /// Sets the value of `x`. This does not actually move the mouse cursor.
313    pub set_x: WriteSignal<f64>,
314    /// Sets the value of `y`. This does not actually move the mouse cursor.
315    pub set_y: WriteSignal<f64>,
316    /// Identifies the source of the reported coordinates
317    pub source_type: Signal<UseMouseSourceType>,
318}
319
320/// Identifies the source of the reported coordinates
321#[derive(Copy, Clone, Debug, PartialEq, Eq)]
322pub enum UseMouseSourceType {
323    /// coordinates come from mouse movement
324    Mouse,
325    /// coordinates come from touch
326    Touch,
327    /// Initially before any event has been recorded the source type is unset
328    Unset,
329}