adui_dioxus/components/
interaction.rs

1//! Shared interaction helpers (pointer capture, dragging state).
2//!
3//! Centralizes pointer tracking so components like Slider/ColorPicker can reuse the same
4//! capture/release semantics and avoid duplicating DOM handling.
5
6use dioxus::events::PointerData;
7use dioxus::prelude::*;
8use wasm_bindgen::JsCast;
9
10/// Simple pointer tracking state.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct PointerState {
13    pub active_id: Option<i32>,
14    pub dragging: bool,
15}
16
17/// Extract a `PointerEvent` from a Dioxus pointer event payload.
18pub fn as_pointer_event(evt: &Event<PointerData>) -> Option<web_sys::PointerEvent> {
19    evt.data().downcast::<web_sys::PointerEvent>().cloned()
20}
21
22/// Begin tracking a pointer and set capture on the target element.
23pub fn start_pointer(state: &mut Signal<PointerState>, evt: &web_sys::PointerEvent) {
24    state.set(PointerState {
25        active_id: Some(evt.pointer_id()),
26        dragging: true,
27    });
28
29    if let Some(target) = evt
30        .target()
31        .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
32    {
33        let _ = target.set_pointer_capture(evt.pointer_id());
34    }
35}
36
37/// Stop tracking when the active pointer finishes, releasing capture if present.
38pub fn end_pointer(state: &mut Signal<PointerState>, evt: &web_sys::PointerEvent) {
39    let should_end = {
40        let reader = state.read();
41        reader.active_id == Some(evt.pointer_id())
42    };
43    if !should_end {
44        return;
45    }
46
47    if let Some(target) = evt
48        .target()
49        .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
50    {
51        let _ = target.release_pointer_capture(evt.pointer_id());
52    }
53
54    state.set(PointerState {
55        active_id: None,
56        dragging: false,
57    });
58}
59
60/// Whether the event belongs to the currently tracked pointer.
61pub fn is_active_pointer(state: &Signal<PointerState>, evt: &web_sys::PointerEvent) -> bool {
62    state.read().active_id == Some(evt.pointer_id())
63}
64
65/// Reset state manually (e.g., when unmounting).
66pub fn reset_pointer(state: &mut Signal<PointerState>) {
67    state.set(PointerState {
68        active_id: None,
69        dragging: false,
70    });
71}
72
73#[cfg(test)]
74mod interaction_tests {
75    use super::*;
76
77    #[test]
78    fn pointer_state_default() {
79        let state = PointerState::default();
80        assert_eq!(state.active_id, None);
81        assert_eq!(state.dragging, false);
82    }
83
84    #[test]
85    fn pointer_state_partial_eq() {
86        let state1 = PointerState {
87            active_id: Some(1),
88            dragging: true,
89        };
90        let state2 = PointerState {
91            active_id: Some(1),
92            dragging: true,
93        };
94        let state3 = PointerState {
95            active_id: Some(2),
96            dragging: true,
97        };
98        let state4 = PointerState {
99            active_id: Some(1),
100            dragging: false,
101        };
102
103        assert_eq!(state1, state2);
104        assert_ne!(state1, state3);
105        assert_ne!(state1, state4);
106    }
107
108    #[test]
109    fn pointer_state_debug() {
110        let state = PointerState {
111            active_id: Some(42),
112            dragging: true,
113        };
114        let debug_str = format!("{:?}", state);
115        assert!(debug_str.contains("PointerState"));
116        assert!(debug_str.contains("42") || debug_str.contains("Some"));
117    }
118
119    #[test]
120    fn pointer_state_clone() {
121        let state1 = PointerState {
122            active_id: Some(10),
123            dragging: true,
124        };
125        let state2 = state1;
126        assert_eq!(state1, state2);
127    }
128
129    #[test]
130    fn pointer_state_copy() {
131        let state1 = PointerState {
132            active_id: Some(5),
133            dragging: false,
134        };
135        let state2 = state1;
136        // Copy trait should allow both to be used independently
137        assert_eq!(state1.active_id, Some(5));
138        assert_eq!(state2.active_id, Some(5));
139    }
140
141    #[test]
142    fn reset_pointer_logic() {
143        // Test the logic of reset_pointer by creating the expected state
144        let reset_state = PointerState {
145            active_id: None,
146            dragging: false,
147        };
148        assert_eq!(reset_state.active_id, None);
149        assert_eq!(reset_state.dragging, false);
150    }
151
152    #[test]
153    fn is_active_pointer_logic() {
154        // Test the logic: active_id == Some(pointer_id)
155        let state_with_id = PointerState {
156            active_id: Some(42),
157            dragging: true,
158        };
159        // Logic: state.active_id == Some(evt.pointer_id())
160        assert_eq!(state_with_id.active_id, Some(42));
161
162        let state_without_id = PointerState {
163            active_id: None,
164            dragging: false,
165        };
166        assert_eq!(state_without_id.active_id, None);
167    }
168}