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
125
126
127
128
129
130
#![allow(clippy::type_complexity)]

use freya_native_core::NodeId;
use rustc_hash::FxHashMap;

use crate::{
    events::{
        DomEvent,
        PlatformEvent,
    },
    prelude::{
        EventName,
        PotentialEvent,
        PotentialEvents,
    },
};

#[derive(Clone)]
struct NodeMetadata {
    layer: Option<i16>,
}

/// [`NodesState`] stores the nodes states given incoming events.
#[derive(Default)]
pub struct NodesState {
    hovered_nodes: FxHashMap<NodeId, NodeMetadata>,
}

impl NodesState {
    /// Update the node states given the new events
    pub fn process_events(
        &mut self,
        events_to_emit: &[DomEvent],
        events: &[PlatformEvent],
    ) -> (PotentialEvents, Vec<DomEvent>) {
        let mut new_events_to_emit = Vec::default();
        let mut potential_events = PotentialEvents::default();

        let recent_mouse_movement_event = any_recent_mouse_movement(events);

        self.hovered_nodes.retain(|node_id, metadata| {
            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(PlatformEvent::Mouse { cursor, button, .. }) =
                    recent_mouse_movement_event
                {
                    let events = potential_events.entry(EventName::MouseLeave).or_default();
                    // Emit a MouseLeave event as the cursor was moved outside the Node bounds
                    events.push(PotentialEvent {
                        node_id: *node_id,
                        layer: metadata.layer,
                        event: PlatformEvent::Mouse {
                            name: EventName::MouseLeave,
                            cursor,
                            button,
                        },
                    });

                    // Remove the node from the list of hovered nodes as now, the cursor has left
                    return false;
                }
            }
            true
        });

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

        // Emit new colateral events
        for event in events_to_emit {
            if event.name.can_change_hover_state() {
                let is_hovered = hovered_nodes.contains_key(&event.node_id);

                // Mark the Node as hovered if it wasn't already
                if !is_hovered {
                    self.hovered_nodes
                        .insert(event.node_id, NodeMetadata { layer: event.layer });
                }

                if event.name.is_enter() {
                    // If the Node was already hovered, we don't need to emit an `enter` event again.
                    if is_hovered {
                        continue;
                    }
                }
            }

            new_events_to_emit.push(event.clone());
        }

        // Update the internal states of nodes given the events
        // e.g `mouseover` will mark the node as hovered.
        for event in events_to_emit {
            if event.name.was_cursor_moved() && !self.hovered_nodes.contains_key(&event.node_id) {
                self.hovered_nodes
                    .insert(event.node_id, NodeMetadata { layer: event.layer });
            }
        }

        // Order the events by their Nodes layer
        for events in potential_events.values_mut() {
            events.sort_by(|left, right| left.layer.cmp(&right.layer))
        }

        (potential_events, new_events_to_emit)
    }
}

fn any_recent_mouse_movement(events: &[PlatformEvent]) -> Option<PlatformEvent> {
    events
        .iter()
        .find(|event| event.get_name().was_cursor_moved())
        .cloned()
}

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