leptos_use/
use_mouse.rs

1#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
2
3use crate::core::{IntoElementMaybeSignal, Position};
4use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions, UseWindow};
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/// On the server this returns simple `Signal`s with the `initial_value`s.
90pub fn use_mouse() -> UseMouseReturn {
91    use_mouse_with_options(Default::default())
92}
93
94/// Variant of [`use_mouse`] that accepts options. Please see [`use_mouse`] for how to use.
95pub fn use_mouse_with_options<El, M, Ex>(options: UseMouseOptions<El, M, Ex>) -> UseMouseReturn
96where
97    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
98    Ex: UseMouseEventExtractor + Clone + 'static,
99{
100    let (x, set_x) = signal(options.initial_value.x);
101    let (y, set_y) = signal(options.initial_value.y);
102    let (source_type, set_source_type) = signal(UseMouseSourceType::Unset);
103
104    let mouse_handler = {
105        let coord_type = options.coord_type.clone();
106
107        move |event: web_sys::MouseEvent| {
108            let result = coord_type.extract_mouse_coords(&event);
109
110            if let Some((x, y)) = result {
111                set_x.set(x);
112                set_y.set(y);
113                set_source_type.set(UseMouseSourceType::Mouse);
114            }
115        }
116    };
117
118    let drag_handler = {
119        let mouse_handler = mouse_handler.clone();
120
121        move |event: web_sys::DragEvent| {
122            let js_value: &JsValue = event.as_ref();
123            mouse_handler(js_value.clone().unchecked_into::<web_sys::MouseEvent>());
124        }
125    };
126
127    let touch_handler = {
128        let coord_type = options.coord_type.clone();
129
130        move |event: web_sys::TouchEvent| {
131            let touches = event.touches();
132            if touches.length() > 0 {
133                let result = coord_type.extract_touch_coords(
134                    &touches
135                        .get(0)
136                        .expect("Just checked that there's at least on touch"),
137                );
138
139                if let Some((x, y)) = result {
140                    set_x.set(x);
141                    set_y.set(y);
142                    set_source_type.set(UseMouseSourceType::Touch);
143                }
144            }
145        }
146    };
147
148    let initial_value = options.initial_value;
149    let reset = move || {
150        set_x.set(initial_value.x);
151        set_y.set(initial_value.y);
152    };
153
154    // TODO : event filters?
155
156    #[cfg(not(feature = "ssr"))]
157    {
158        let target = options.target.into_element_maybe_signal();
159        let event_listener_options = UseEventListenerOptions::default().passive(true);
160
161        let _ = use_event_listener_with_options(
162            target,
163            mousemove,
164            mouse_handler,
165            event_listener_options,
166        );
167        let _ =
168            use_event_listener_with_options(target, dragover, drag_handler, event_listener_options);
169
170        if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) {
171            let _ = use_event_listener_with_options(
172                target,
173                touchstart,
174                touch_handler.clone(),
175                event_listener_options,
176            );
177            let _ = use_event_listener_with_options(
178                target,
179                touchmove,
180                touch_handler,
181                event_listener_options,
182            );
183            if options.reset_on_touch_ends {
184                let _ = use_event_listener_with_options(
185                    target,
186                    touchend,
187                    move |_| reset(),
188                    event_listener_options,
189                );
190            }
191        }
192    }
193
194    UseMouseReturn {
195        x: x.into(),
196        y: y.into(),
197        set_x,
198        set_y,
199        source_type: source_type.into(),
200    }
201}
202
203#[derive(DefaultBuilder)]
204/// Options for [`use_mouse_with_options`].
205pub struct UseMouseOptions<El, M, Ex>
206where
207    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
208    Ex: UseMouseEventExtractor + Clone,
209{
210    /// How to extract the x, y coordinates from mouse events or touches
211    coord_type: UseMouseCoordType<Ex>,
212
213    /// Listen events on `target` element. Defaults to `window`
214    target: El,
215
216    /// Listen to `touchmove` events. Defaults to `true`.
217    touch: bool,
218
219    /// Reset to initial value when `touchend` event fired. Defaults to `false`
220    reset_on_touch_ends: bool,
221
222    /// Initial values. Defaults to `{x: 0.0, y: 0.0}`.
223    initial_value: Position,
224
225    #[builder(skip)]
226    _marker: PhantomData<M>,
227}
228
229impl<M> Default for UseMouseOptions<UseWindow, M, Infallible>
230where
231    UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, M>,
232{
233    fn default() -> Self {
234        Self {
235            coord_type: UseMouseCoordType::default(),
236            target: use_window(),
237            touch: true,
238            reset_on_touch_ends: false,
239            initial_value: Position { x: 0.0, y: 0.0 },
240            _marker: PhantomData,
241        }
242    }
243}
244
245/// Defines how to get the coordinates from the event.
246#[derive(Clone)]
247pub enum UseMouseCoordType<E: UseMouseEventExtractor + Clone> {
248    Page,
249    Client,
250    Screen,
251    Movement,
252    Custom(E),
253}
254
255impl Default for UseMouseCoordType<Infallible> {
256    fn default() -> Self {
257        Self::Page
258    }
259}
260
261/// Trait to implement if you want to specify a custom extractor
262#[allow(unused_variables)]
263pub trait UseMouseEventExtractor {
264    /// Return the coordinates from mouse events (`Some(x, y)`) or `None`
265    fn extract_mouse_coords(&self, event: &web_sys::MouseEvent) -> Option<(f64, f64)> {
266        None
267    }
268
269    /// Return the coordinates from touches (`Some(x, y)`) or `None`
270    fn extract_touch_coords(&self, touch: &web_sys::Touch) -> Option<(f64, f64)> {
271        None
272    }
273}
274
275impl<E: UseMouseEventExtractor + Clone> UseMouseEventExtractor for UseMouseCoordType<E> {
276    fn extract_mouse_coords(&self, event: &web_sys::MouseEvent) -> Option<(f64, f64)> {
277        match self {
278            UseMouseCoordType::Page => Some((event.page_x() as f64, event.page_y() as f64)),
279            UseMouseCoordType::Client => Some((event.client_x() as f64, event.client_y() as f64)),
280            UseMouseCoordType::Screen => Some((event.screen_x() as f64, event.client_y() as f64)),
281            UseMouseCoordType::Movement => {
282                Some((event.movement_x() as f64, event.movement_y() as f64))
283            }
284            UseMouseCoordType::Custom(ref extractor) => extractor.extract_mouse_coords(event),
285        }
286    }
287
288    fn extract_touch_coords(&self, touch: &web_sys::Touch) -> Option<(f64, f64)> {
289        match self {
290            UseMouseCoordType::Page => Some((touch.page_x() as f64, touch.page_y() as f64)),
291            UseMouseCoordType::Client => Some((touch.client_x() as f64, touch.client_y() as f64)),
292            UseMouseCoordType::Screen => Some((touch.screen_x() as f64, touch.client_y() as f64)),
293            UseMouseCoordType::Movement => None,
294            UseMouseCoordType::Custom(ref extractor) => extractor.extract_touch_coords(touch),
295        }
296    }
297}
298
299impl UseMouseEventExtractor for Infallible {
300    fn extract_mouse_coords(&self, _: &web_sys::MouseEvent) -> Option<(f64, f64)> {
301        unreachable!()
302    }
303
304    fn extract_touch_coords(&self, _: &web_sys::Touch) -> Option<(f64, f64)> {
305        unreachable!()
306    }
307}
308
309/// Return type of [`use_mouse`].
310pub struct UseMouseReturn {
311    /// X coordinate of the mouse pointer / touch
312    pub x: Signal<f64>,
313    /// Y coordinate of the mouse pointer / touch
314    pub y: Signal<f64>,
315    /// Sets the value of `x`. This does not actually move the mouse cursor.
316    pub set_x: WriteSignal<f64>,
317    /// Sets the value of `y`. This does not actually move the mouse cursor.
318    pub set_y: WriteSignal<f64>,
319    /// Identifies the source of the reported coordinates
320    pub source_type: Signal<UseMouseSourceType>,
321}
322
323/// Identifies the source of the reported coordinates
324#[derive(Copy, Clone, Debug, PartialEq, Eq)]
325pub enum UseMouseSourceType {
326    /// coordinates come from mouse movement
327    Mouse,
328    /// coordinates come from touch
329    Touch,
330    /// Initially before any event has been recorded the source type is unset
331    Unset,
332}