1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#![allow(clippy::type_complexity)]

use dioxus_native_core::NodeId;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::events::{does_event_move_cursor, DomEvent, FreyaEvent};

/// [`ElementsState`] stores the elements states given incoming events.
#[derive(Default)]
pub struct ElementsState {
    hovered_elements: FxHashSet<NodeId>,
}

impl ElementsState {
    /// Update the Element states given the new events
    pub fn process_events(
        &mut self,
        events_to_emit: &[DomEvent],
        events: &[FreyaEvent],
    ) -> (FxHashMap<String, Vec<(NodeId, FreyaEvent)>>, Vec<DomEvent>) {
        let mut new_events_to_emit = Vec::default();
        let mut new_events = FxHashMap::<String, Vec<(NodeId, FreyaEvent)>>::default();

        let recent_mouse_movement_event = any_recent_mouse_movement(events);

        // Suggest emitting `mouseleave` in elements not being hovered anymore
        self.hovered_elements.retain(|node_id| {
            let no_recent_mouse_movement_on_me =
                has_node_been_hovered_recently(events_to_emit, node_id);

            if no_recent_mouse_movement_on_me {
                if let Some(FreyaEvent::Mouse { cursor, button, .. }) = recent_mouse_movement_event
                {
                    let events = new_events.entry("mouseleave".to_string()).or_default();
                    events.push((
                        *node_id,
                        FreyaEvent::Mouse {
                            name: "mouseleave".to_string(),
                            cursor,
                            button,
                        },
                    ));

                    // Remove the node from the list of hovered elements
                    return false;
                }
            }
            true
        });

        // All these events will mark the node as being hovered
        // "mouseover" "mouseenter" "pointerover"  "pointerenter"

        // We clone this here so events emitted in the same batch that mark an element as hovered will not affect the other events
        let hovered_elements = self.hovered_elements.clone();

        // Emit valid events
        for event in events_to_emit {
            let id = &event.node_id;

            let should_trigger = match event.name.as_str() {
                name @ "mouseover"
                | name @ "mouseenter"
                | name @ "pointerover"
                | name @ "pointerenter" => {
                    let is_hovered = hovered_elements.contains(id);

                    if !is_hovered {
                        self.hovered_elements.insert(*id);
                    }

                    if name == "mouseenter" || name == "pointerenter" {
                        // If the event is already being hovered then it's pointless to trigger the movement event
                        !is_hovered
                    } else {
                        true
                    }
                }
                _ => true,
            };

            if should_trigger {
                new_events_to_emit.push(event.clone());
            }
        }

        // Update the internal states of elements given the events
        // e.g `mouseover` will mark the element as hovered.
        for event in events_to_emit {
            let id = &event.node_id;
            if does_event_move_cursor(event.name.as_str()) && !self.hovered_elements.contains(id) {
                self.hovered_elements.insert(*id);
            }
        }

        (new_events, new_events_to_emit)
    }
}

fn any_recent_mouse_movement(events: &[FreyaEvent]) -> Option<FreyaEvent> {
    events
        .iter()
        .find(|event| {
            if let FreyaEvent::Mouse { name, .. } = event {
                does_event_move_cursor(name)
            } else {
                false
            }
        })
        .cloned()
}

fn has_node_been_hovered_recently(events_to_emit: &[DomEvent], element: &NodeId) -> bool {
    events_to_emit
        .iter()
        .find_map(|event| {
            if event.does_move_cursor() && &event.node_id == element {
                Some(false)
            } else {
                None
            }
        })
        .unwrap_or(true)
}