freya-core 0.3.4

Internal core funcionatilies for Freya.
Documentation
#![allow(clippy::type_complexity)]

use freya_engine::prelude::Color;
use freya_native_core::{
    events::EventName,
    prelude::NodeImmutable,
    NodeId,
};
use rustc_hash::FxHashMap;

use super::PlatformEventData;
use crate::{
    dom::FreyaDOM,
    events::{
        is_node_parent_of,
        DomEvent,
        PlatformEvent,
        PotentialEvent,
    },
    states::StyleState,
    types::PotentialEvents,
    values::Fill,
};

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

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

impl NodesState {
    /// Update the node states given the new events and suggest potential collateral new events
    pub fn process_collateral(
        &mut self,
        fdom: &FreyaDOM,
        potential_events: &PotentialEvents,
        dom_events: &mut Vec<DomEvent>,
        events: &[PlatformEvent],
    ) -> PotentialEvents {
        let rdom = fdom.rdom();
        let layout = fdom.layout();
        let mut potential_collateral_events = PotentialEvents::default();

        // Any mouse press event at all
        let recent_mouse_press_event = any_event_of(events, |e| e.is_pressed());

        // Pressed Nodes
        #[allow(unused_variables)]
        self.pressed_nodes.retain(|node_id, _| {
            // Check if a DOM event that presses this Node will get emitted
            let no_desire_to_press = filter_dom_events_by(dom_events, node_id, |e| e.is_pressed());

            // If there has been a mouse press but a DOM event was not emitted to this node, then we safely assume
            // the user does no longer want to press this Node
            if no_desire_to_press && recent_mouse_press_event.is_some() {
                #[cfg(debug_assertions)]
                tracing::info!("Unmarked as pressed {:?}", node_id);

                // Remove the node from the list of pressed nodes
                return false;
            }

            true
        });

        // Any mouse movement event at all
        let recent_mouse_movement_event = any_event_of(events, |e| e.is_moved());

        // Hovered Nodes
        self.hovered_nodes.retain(|node_id, metadata| {
            // Check if a DOM event that moves the cursor in this Node will get emitted
            let no_desire_to_hover = filter_dom_events_by(dom_events, node_id, |e| e.is_moved());

            if no_desire_to_hover {
                // If there has been a mouse movement but a DOM event was not emitted to this node, then we safely assume
                // the user does no longer want to hover this Node
                if let Some(PlatformEventData::Mouse { cursor, button, .. }) =
                    recent_mouse_movement_event
                {
                    if layout.get(*node_id).is_some() {
                        let events = potential_collateral_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,
                            name: EventName::MouseLeave,
                            data: PlatformEventData::Mouse { cursor, button },
                        });

                        #[cfg(debug_assertions)]
                        tracing::info!("Unmarked as hovered {:?}", node_id);
                    }

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

        dom_events.retain(|ev| {
            match ev.name {
                // Only let through enter events when the node was not hovered
                _ if ev.name.is_enter() => !self.hovered_nodes.contains_key(&ev.node_id),

                // Only let through release events when the node was already pressed
                _ if ev.name.is_released() => self.pressed_nodes.contains_key(&ev.node_id),

                _ => true,
            }
        });

        // Update the state of the nodes given the new events.
        for events in potential_events.values() {
            let mut child_node: Option<NodeId> = None;

            for PotentialEvent {
                node_id,
                name,
                layer,
                ..
            } in events.iter().rev()
            {
                if let Some(child_node) = child_node {
                    if !is_node_parent_of(rdom, child_node, *node_id) {
                        continue;
                    }
                }

                let node = rdom.get(*node_id).unwrap();
                let StyleState { background, .. } = &*node.get::<StyleState>().unwrap();

                if background != &Fill::Color(Color::TRANSPARENT) && !name.does_go_through_solid() {
                    // If the background isn't transparent,
                    // we must make sure that next nodes are parent of it
                    // This only matters for events that bubble up (e.g. cursor click events)
                    child_node = Some(*node_id);
                }

                match name {
                    // Update hovered nodes state
                    name if name.is_hovered() => {
                        // Mark the Node as hovered if it wasn't already
                        self.hovered_nodes.entry(*node_id).or_insert_with(|| {
                            #[cfg(debug_assertions)]
                            tracing::info!("Marked as hovered {:?}", node_id);

                            NodeMetadata { layer: *layer }
                        });
                    }

                    // Update pressed nodes state
                    name if name.is_pressed() => {
                        // Mark the Node as pressed if it wasn't already
                        self.pressed_nodes.entry(*node_id).or_insert_with(|| {
                            #[cfg(debug_assertions)]
                            tracing::info!("Marked as pressed {:?}", node_id);

                            NodeMetadata { layer: *layer }
                        });
                    }
                    _ => {}
                }
            }
        }

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

        potential_collateral_events
    }
}

fn any_event_of(
    events: &[PlatformEvent],
    filter: impl Fn(EventName) -> bool,
) -> Option<PlatformEventData> {
    events
        .iter()
        .find_map(|event| {
            if filter(event.name) {
                Some(&event.data)
            } else {
                None
            }
        })
        .cloned()
}

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