use std::sync::{Arc, Mutex, OnceLock};
use crossbeam_channel::{unbounded, Receiver, Sender};
use crate::menu::{MenuEvent, MenuId};
pub type MenuEventReceiver = Receiver<MenuEvent>;
type EventHandler = Arc<dyn Fn(MenuEvent) + Send + Sync + 'static>;
struct GlobalChannel {
sender: Sender<MenuEvent>,
receiver: MenuEventReceiver,
}
fn channel() -> &'static GlobalChannel {
static CHANNEL: OnceLock<GlobalChannel> = OnceLock::new();
CHANNEL.get_or_init(|| {
let (sender, receiver) = unbounded();
GlobalChannel { sender, receiver }
})
}
fn handler_slot() -> &'static Mutex<Option<EventHandler>> {
static HANDLER: OnceLock<Mutex<Option<EventHandler>>> = OnceLock::new();
HANDLER.get_or_init(|| Mutex::new(None))
}
impl MenuEvent {
pub fn receiver() -> &'static MenuEventReceiver {
&channel().receiver
}
pub fn set_event_handler<F>(handler: Option<F>)
where
F: Fn(MenuEvent) + Send + Sync + 'static,
{
let handler: Option<EventHandler> = handler.map(|f| Arc::new(f) as EventHandler);
if let Ok(mut slot) = handler_slot().lock() {
*slot = handler;
}
}
}
pub(crate) fn emit(id: MenuId) {
let event = MenuEvent { id };
let _ = channel().sender.send(event.clone());
let handler = handler_slot().lock().ok().and_then(|slot| slot.clone());
if let Some(handler) = handler {
handler(event);
}
}
#[cfg(test)]
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
fn drain_prefixed(prefix: &str) -> Vec<String> {
let rx = MenuEvent::receiver();
let mut out = Vec::new();
while let Ok(ev) = rx.try_recv() {
if ev.id.0.starts_with(prefix) {
out.push(ev.id.0);
}
}
out
}
#[test]
fn emit_places_event_on_the_global_channel() {
let _guard = super::test_lock();
let _ = drain_prefixed("event_emit_");
emit(MenuId::from("event_emit_alpha"));
emit(MenuId::from("event_emit_beta"));
let seen = drain_prefixed("event_emit_");
assert!(seen.contains(&"event_emit_alpha".to_string()));
assert!(seen.contains(&"event_emit_beta".to_string()));
}
#[test]
fn set_event_handler_receives_emitted_events() {
let _guard = super::test_lock();
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let hits = Arc::new(AtomicUsize::new(0));
let hits2 = Arc::clone(&hits);
MenuEvent::set_event_handler(Some(move |ev: MenuEvent| {
if ev.id.0 == "event_handler_probe" {
hits2.fetch_add(1, Ordering::SeqCst);
}
}));
emit(MenuId::from("event_handler_probe"));
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
assert_eq!(hits.load(Ordering::SeqCst), 1);
let _ = drain_prefixed("event_handler_probe");
}
#[test]
fn reentrant_set_event_handler_from_handler_does_not_deadlock() {
let _guard = super::test_lock();
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let hits = Arc::new(AtomicUsize::new(0));
let hits2 = Arc::clone(&hits);
MenuEvent::set_event_handler(Some(move |ev: MenuEvent| {
if ev.id.0 == "event_reentrant_probe" {
hits2.fetch_add(1, Ordering::SeqCst);
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
}
}));
emit(MenuId::from("event_reentrant_probe"));
assert_eq!(hits.load(Ordering::SeqCst), 1);
MenuEvent::set_event_handler(None::<fn(MenuEvent)>);
let _ = drain_prefixed("event_reentrant_probe");
}
}