freya_core/events/
nodes_state.rs

1#![allow(clippy::type_complexity)]
2
3use freya_engine::prelude::Color;
4use freya_native_core::{
5    events::EventName,
6    prelude::NodeImmutable,
7    NodeId,
8};
9use rustc_hash::FxHashMap;
10
11use super::PlatformEventData;
12use crate::{
13    dom::FreyaDOM,
14    events::{
15        is_node_parent_of,
16        DomEvent,
17        PlatformEvent,
18        PotentialEvent,
19    },
20    states::StyleState,
21    types::PotentialEvents,
22    values::Fill,
23};
24
25#[derive(Clone, Debug)]
26struct NodeMetadata {
27    layer: Option<i16>,
28}
29
30/// [`NodesState`] stores the nodes states given incoming events.
31#[derive(Default)]
32pub struct NodesState {
33    pressed_nodes: FxHashMap<NodeId, NodeMetadata>,
34    hovered_nodes: FxHashMap<NodeId, NodeMetadata>,
35}
36
37impl NodesState {
38    /// Update the node states given the new events and suggest potential collateral new events
39    pub fn process_collateral(
40        &mut self,
41        fdom: &FreyaDOM,
42        potential_events: &PotentialEvents,
43        dom_events: &mut Vec<DomEvent>,
44        events: &[PlatformEvent],
45    ) -> PotentialEvents {
46        let rdom = fdom.rdom();
47        let layout = fdom.layout();
48        let mut potential_collateral_events = PotentialEvents::default();
49
50        // Any mouse press event at all
51        let recent_mouse_press_event = any_event_of(events, |e| e.is_pressed());
52
53        // Pressed Nodes
54        #[allow(unused_variables)]
55        self.pressed_nodes.retain(|node_id, _| {
56            // Check if a DOM event that presses this Node will get emitted
57            let no_desire_to_press = filter_dom_events_by(dom_events, node_id, |e| e.is_pressed());
58
59            // If there has been a mouse press but a DOM event was not emitted to this node, then we safely assume
60            // the user does no longer want to press this Node
61            if no_desire_to_press && recent_mouse_press_event.is_some() {
62                #[cfg(debug_assertions)]
63                tracing::info!("Unmarked as pressed {:?}", node_id);
64
65                // Remove the node from the list of pressed nodes
66                return false;
67            }
68
69            true
70        });
71
72        // Any mouse movement event at all
73        let recent_mouse_movement_event = any_event_of(events, |e| e.is_moved());
74
75        // Hovered Nodes
76        self.hovered_nodes.retain(|node_id, metadata| {
77            // Check if a DOM event that moves the cursor in this Node will get emitted
78            let no_desire_to_hover = filter_dom_events_by(dom_events, node_id, |e| e.is_moved());
79
80            if no_desire_to_hover {
81                // If there has been a mouse movement but a DOM event was not emitted to this node, then we safely assume
82                // the user does no longer want to hover this Node
83                if let Some(PlatformEventData::Mouse { cursor, button, .. }) =
84                    recent_mouse_movement_event
85                {
86                    if layout.get(*node_id).is_some() {
87                        let events = potential_collateral_events
88                            .entry(EventName::MouseLeave)
89                            .or_default();
90
91                        // Emit a MouseLeave event as the cursor was moved outside the Node bounds
92                        events.push(PotentialEvent {
93                            node_id: *node_id,
94                            layer: metadata.layer,
95                            name: EventName::MouseLeave,
96                            data: PlatformEventData::Mouse { cursor, button },
97                        });
98
99                        #[cfg(debug_assertions)]
100                        tracing::info!("Unmarked as hovered {:?}", node_id);
101                    }
102
103                    // Remove the node from the list of hovered nodes
104                    return false;
105                }
106            }
107            true
108        });
109
110        dom_events.retain(|ev| {
111            match ev.name {
112                // Only let through enter events when the node was not hovered
113                _ if ev.name.is_enter() => !self.hovered_nodes.contains_key(&ev.node_id),
114
115                // Only let through release events when the node was already pressed
116                _ if ev.name.is_released() => self.pressed_nodes.contains_key(&ev.node_id),
117
118                _ => true,
119            }
120        });
121
122        // Update the state of the nodes given the new events.
123        for events in potential_events.values() {
124            let mut child_node: Option<NodeId> = None;
125
126            for PotentialEvent {
127                node_id,
128                name,
129                layer,
130                ..
131            } in events.iter().rev()
132            {
133                if let Some(child_node) = child_node {
134                    if !is_node_parent_of(rdom, child_node, *node_id) {
135                        continue;
136                    }
137                }
138
139                let node = rdom.get(*node_id).unwrap();
140                let StyleState { background, .. } = &*node.get::<StyleState>().unwrap();
141
142                if background != &Fill::Color(Color::TRANSPARENT) && !name.does_go_through_solid() {
143                    // If the background isn't transparent,
144                    // we must make sure that next nodes are parent of it
145                    // This only matters for events that bubble up (e.g. cursor click events)
146                    child_node = Some(*node_id);
147                }
148
149                match name {
150                    // Update hovered nodes state
151                    name if name.is_hovered() => {
152                        // Mark the Node as hovered if it wasn't already
153                        self.hovered_nodes.entry(*node_id).or_insert_with(|| {
154                            #[cfg(debug_assertions)]
155                            tracing::info!("Marked as hovered {:?}", node_id);
156
157                            NodeMetadata { layer: *layer }
158                        });
159                    }
160
161                    // Update pressed nodes state
162                    name if name.is_pressed() => {
163                        // Mark the Node as pressed if it wasn't already
164                        self.pressed_nodes.entry(*node_id).or_insert_with(|| {
165                            #[cfg(debug_assertions)]
166                            tracing::info!("Marked as pressed {:?}", node_id);
167
168                            NodeMetadata { layer: *layer }
169                        });
170                    }
171                    _ => {}
172                }
173            }
174        }
175
176        // Order the events by their Nodes layer
177        for events in potential_collateral_events.values_mut() {
178            events.sort_by(|left, right| left.layer.cmp(&right.layer))
179        }
180
181        potential_collateral_events
182    }
183}
184
185fn any_event_of(
186    events: &[PlatformEvent],
187    filter: impl Fn(EventName) -> bool,
188) -> Option<PlatformEventData> {
189    events
190        .iter()
191        .find_map(|event| {
192            if filter(event.name) {
193                Some(&event.data)
194            } else {
195                None
196            }
197        })
198        .cloned()
199}
200
201fn filter_dom_events_by(
202    events_to_emit: &[DomEvent],
203    node_id: &NodeId,
204    filter: impl Fn(EventName) -> bool,
205) -> bool {
206    events_to_emit
207        .iter()
208        .find_map(|event| {
209            if filter(event.name) && &event.node_id == node_id {
210                Some(false)
211            } else {
212                None
213            }
214        })
215        .unwrap_or(true)
216}