lgui-core 0.2.2

Platform-neutral runtime and UI primitives for LGUI
Documentation
use std::{
    any::{Any, TypeId},
    collections::HashMap,
    sync::{
        atomic::{AtomicU64, Ordering},
        Arc, Mutex,
    },
};

use super::{EventKey, EventSubscription};

type EventListener = Arc<dyn Fn(&dyn Any) + Send + Sync + 'static>;

pub(super) struct EventTopic {
    pub(super) payload_type: TypeId,
    pub(super) payload_name: &'static str,
    pub(super) listeners: HashMap<u64, EventListener>,
}

#[derive(Default)]
pub(super) struct EventBusInner {
    pub(super) topics: Mutex<HashMap<Arc<str>, EventTopic>>,
    next_subscription: AtomicU64,
}

#[derive(Clone, Default)]
pub(crate) struct EventBus {
    inner: Arc<EventBusInner>,
}

impl EventBus {
    pub(crate) fn emit<T>(&self, key: &EventKey<T>, payload: T) -> usize
    where
        T: Clone + Send + Sync + 'static,
    {
        let (listeners, mismatched_payload) = {
            let topics = self.inner.topics.lock().expect("event bus poisoned");
            match topics.get(key.as_str()) {
                Some(topic) if topic.payload_type != TypeId::of::<T>() => {
                    (Vec::new(), Some(topic.payload_name))
                }
                Some(topic) => (topic.listeners.values().cloned().collect::<Vec<_>>(), None),
                None => (Vec::new(), None),
            }
        };
        if let Some(expected) = mismatched_payload {
            panic_payload_type::<T>(key.as_str(), expected);
        }
        for listener in &listeners {
            listener(&payload);
        }
        listeners.len()
    }

    pub(crate) fn subscribe<T>(
        &self,
        key: EventKey<T>,
        listener: impl Fn(T) + Send + Sync + 'static,
    ) -> EventSubscription
    where
        T: Clone + Send + Sync + 'static,
    {
        let id = self
            .inner
            .next_subscription
            .fetch_add(1, Ordering::Relaxed)
            .wrapping_add(1);
        let listener = Arc::new(move |event: &dyn Any| {
            let event = event
                .downcast_ref::<T>()
                .unwrap_or_else(|| panic!("event payload type mismatch"));
            listener(event.clone());
        }) as EventListener;
        let name = key.shared_name();
        let mut topics = self.inner.topics.lock().expect("event bus poisoned");
        let mismatched_payload = topics.get(key.as_str()).and_then(|topic| {
            (topic.payload_type != TypeId::of::<T>()).then_some(topic.payload_name)
        });
        if let Some(expected) = mismatched_payload {
            drop(topics);
            panic_payload_type::<T>(key.as_str(), expected);
        }
        let topic = topics
            .entry(Arc::clone(&name))
            .or_insert_with(|| EventTopic {
                payload_type: TypeId::of::<T>(),
                payload_name: std::any::type_name::<T>(),
                listeners: HashMap::new(),
            });
        topic.listeners.insert(id, listener);
        drop(topics);
        EventSubscription::new(&self.inner, name, id)
    }

    #[cfg(test)]
    pub(crate) fn topic_count(&self) -> usize {
        self.inner.topics.lock().expect("event bus poisoned").len()
    }
}

fn panic_payload_type<T>(key: &str, expected: &'static str) -> !
where
    T: 'static,
{
    panic!(
        "event key `{key}` is already bound to payload type `{}` and cannot be used with `{}`",
        expected,
        std::any::type_name::<T>()
    );
}