rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Callback-based event subscription for [`MapState`](crate::MapState).
//!
//! Complements the existing poll-based
//! [`InteractionManager::drain_events`](crate::interaction_manager::InteractionManager::drain_events)
//! model with a subscribe/unsubscribe API that mirrors
//! `map.on(type, listener)` / `map.off(type, listener)` from
//! MapLibre GL JS.
//!
//! # Thread safety
//!
//! `EventEmitter` is `Send + Sync` because callbacks are stored as
//! `Box<dyn Fn(&InteractionEvent) + Send + Sync>`.  The emitter is
//! designed to be called from the same thread that owns `MapState`
//! (the update path), so no internal locking is required.

use crate::interaction::{InteractionEvent, InteractionEventKind};

/// Opaque handle returned by [`EventEmitter::on`] and [`EventEmitter::once`].
///
/// Pass to [`EventEmitter::off`] to unsubscribe.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListenerId(u64);

/// A single registered listener.
struct Listener {
    id: ListenerId,
    kind: InteractionEventKind,
    callback: Box<dyn Fn(&InteractionEvent) + Send + Sync>,
    once: bool,
}

/// Callback-based event subscription system.
///
/// Embed in [`MapState`](crate::MapState) and call
/// [`dispatch`](Self::dispatch) each frame with the events drained
/// from [`InteractionManager`](crate::interaction_manager::InteractionManager).
pub struct EventEmitter {
    listeners: Vec<Listener>,
    next_id: u64,
}

impl Default for EventEmitter {
    fn default() -> Self {
        Self::new()
    }
}

impl EventEmitter {
    /// Create an empty event emitter.
    pub fn new() -> Self {
        Self {
            listeners: Vec::new(),
            next_id: 0,
        }
    }

    /// Subscribe to events of a given kind.
    ///
    /// Returns a [`ListenerId`] that can later be passed to [`off`](Self::off).
    pub fn on<F>(&mut self, kind: InteractionEventKind, callback: F) -> ListenerId
    where
        F: Fn(&InteractionEvent) + Send + Sync + 'static,
    {
        let id = self.alloc_id();
        self.listeners.push(Listener {
            id,
            kind,
            callback: Box::new(callback),
            once: false,
        });
        id
    }

    /// Subscribe to a single occurrence of an event kind.
    ///
    /// The listener is automatically removed after the first dispatch.
    pub fn once<F>(&mut self, kind: InteractionEventKind, callback: F) -> ListenerId
    where
        F: Fn(&InteractionEvent) + Send + Sync + 'static,
    {
        let id = self.alloc_id();
        self.listeners.push(Listener {
            id,
            kind,
            callback: Box::new(callback),
            once: true,
        });
        id
    }

    /// Unsubscribe a previously registered listener.
    ///
    /// Returns `true` if the listener was found and removed.
    pub fn off(&mut self, id: ListenerId) -> bool {
        let before = self.listeners.len();
        self.listeners.retain(|l| l.id != id);
        self.listeners.len() < before
    }

    /// Dispatch a batch of events to all matching listeners.
    ///
    /// `once` listeners are removed after their first invocation.
    pub fn dispatch(&mut self, events: &[InteractionEvent]) {
        if self.listeners.is_empty() || events.is_empty() {
            return;
        }

        let mut to_remove: Vec<ListenerId> = Vec::new();

        for event in events {
            for listener in &self.listeners {
                if listener.kind == event.kind {
                    (listener.callback)(event);
                    if listener.once {
                        to_remove.push(listener.id);
                    }
                }
            }
        }

        if !to_remove.is_empty() {
            self.listeners.retain(|l| !to_remove.contains(&l.id));
        }
    }

    /// Number of active listeners.
    pub fn listener_count(&self) -> usize {
        self.listeners.len()
    }

    /// Number of listeners for a specific event kind.
    pub fn listener_count_for(&self, kind: InteractionEventKind) -> usize {
        self.listeners.iter().filter(|l| l.kind == kind).count()
    }

    fn alloc_id(&mut self) -> ListenerId {
        let id = ListenerId(self.next_id);
        self.next_id += 1;
        id
    }
}

// SAFETY: Listener contains `Box<dyn Fn + Send + Sync>`, which is Send + Sync.
// Vec<Listener> is therefore Send + Sync as well.
unsafe impl Send for EventEmitter {}
unsafe impl Sync for EventEmitter {}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::interaction::{InteractionEvent, InteractionEventKind, PointerKind, ScreenPoint};
    use std::sync::atomic::{AtomicU32, Ordering};
    use std::sync::Arc;

    fn make_event(kind: InteractionEventKind) -> InteractionEvent {
        InteractionEvent::new(kind, PointerKind::Mouse, ScreenPoint::new(0.0, 0.0))
    }

    #[test]
    fn on_dispatches_repeatedly() {
        let mut emitter = EventEmitter::new();
        let counter = Arc::new(AtomicU32::new(0));
        let c = counter.clone();
        emitter.on(InteractionEventKind::Click, move |_| {
            c.fetch_add(1, Ordering::Relaxed);
        });

        let events = vec![make_event(InteractionEventKind::Click)];
        emitter.dispatch(&events);
        emitter.dispatch(&events);

        assert_eq!(counter.load(Ordering::Relaxed), 2);
    }

    #[test]
    fn once_dispatches_only_once() {
        let mut emitter = EventEmitter::new();
        let counter = Arc::new(AtomicU32::new(0));
        let c = counter.clone();
        emitter.once(InteractionEventKind::Click, move |_| {
            c.fetch_add(1, Ordering::Relaxed);
        });

        let events = vec![make_event(InteractionEventKind::Click)];
        emitter.dispatch(&events);
        emitter.dispatch(&events);

        assert_eq!(counter.load(Ordering::Relaxed), 1);
        assert_eq!(emitter.listener_count(), 0);
    }

    #[test]
    fn off_removes_listener() {
        let mut emitter = EventEmitter::new();
        let counter = Arc::new(AtomicU32::new(0));
        let c = counter.clone();
        let id = emitter.on(InteractionEventKind::Click, move |_| {
            c.fetch_add(1, Ordering::Relaxed);
        });

        let events = vec![make_event(InteractionEventKind::Click)];
        emitter.dispatch(&events);
        assert!(emitter.off(id));
        emitter.dispatch(&events);

        assert_eq!(counter.load(Ordering::Relaxed), 1);
    }

    #[test]
    fn different_kinds_are_isolated() {
        let mut emitter = EventEmitter::new();
        let click_count = Arc::new(AtomicU32::new(0));
        let move_count = Arc::new(AtomicU32::new(0));

        let cc = click_count.clone();
        emitter.on(InteractionEventKind::Click, move |_| {
            cc.fetch_add(1, Ordering::Relaxed);
        });

        let mc = move_count.clone();
        emitter.on(InteractionEventKind::MouseMove, move |_| {
            mc.fetch_add(1, Ordering::Relaxed);
        });

        emitter.dispatch(&[make_event(InteractionEventKind::Click)]);

        assert_eq!(click_count.load(Ordering::Relaxed), 1);
        assert_eq!(move_count.load(Ordering::Relaxed), 0);
    }

    #[test]
    fn listener_count_for_kind() {
        let mut emitter = EventEmitter::new();
        emitter.on(InteractionEventKind::Click, |_| {});
        emitter.on(InteractionEventKind::Click, |_| {});
        emitter.on(InteractionEventKind::MouseMove, |_| {});

        assert_eq!(emitter.listener_count(), 3);
        assert_eq!(emitter.listener_count_for(InteractionEventKind::Click), 2);
        assert_eq!(
            emitter.listener_count_for(InteractionEventKind::MouseMove),
            1
        );
    }
}