cocoanut 0.2.3

A minimal, declarative macOS GUI framework for Rust
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

#[cfg(not(test))]
use objc::declare::ClassDecl;
#[cfg(not(test))]
use objc::runtime::{Class, Object, Sel};
#[cfg(not(test))]
#[allow(unused_imports)]
use objc::{msg_send, sel, sel_impl};

/// Type-safe wrapper for callback IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CallbackId(pub usize);

impl CallbackId {
    pub fn new(id: usize) -> Self {
        Self(id)
    }

    pub fn as_usize(&self) -> usize {
        self.0
    }
}

impl From<usize> for CallbackId {
    fn from(id: usize) -> Self {
        Self(id)
    }
}

impl From<CallbackId> for usize {
    fn from(id: CallbackId) -> usize {
        id.0
    }
}

type Callback = Box<dyn Fn() + Send>;
type ValueCallback = Box<dyn Fn(String) + Send>;
type BoolCallback = Box<dyn Fn(bool) + Send>;
type SizeCallback = Box<dyn Fn(f64, f64) + Send>;
type CallbackRegistry = HashMap<usize, Callback>;
type ValueCallbackRegistry = HashMap<usize, ValueCallback>;
type BoolCallbackRegistry = HashMap<usize, BoolCallback>;
type SizeCallbackRegistry = HashMap<usize, SizeCallback>;

static CALLBACKS: Mutex<Option<CallbackRegistry>> = Mutex::new(None);
static VALUE_CALLBACKS: Mutex<Option<ValueCallbackRegistry>> = Mutex::new(None);
static BOOL_CALLBACKS: Mutex<Option<BoolCallbackRegistry>> = Mutex::new(None);
static SIZE_CALLBACKS: Mutex<Option<SizeCallbackRegistry>> = Mutex::new(None);
static TAG_MAP: Mutex<Option<HashMap<isize, usize>>> = Mutex::new(None);
static NEXT_ID: AtomicUsize = AtomicUsize::new(1_000_000);

pub fn next_id() -> usize {
    NEXT_ID.fetch_add(1, Ordering::SeqCst)
}

pub fn init() {
    let mut cbs = CALLBACKS.lock().unwrap();
    if cbs.is_none() {
        *cbs = Some(HashMap::new());
    }
    let mut vcbs = VALUE_CALLBACKS.lock().unwrap();
    if vcbs.is_none() {
        *vcbs = Some(HashMap::new());
    }
    let mut bcbs = BOOL_CALLBACKS.lock().unwrap();
    if bcbs.is_none() {
        *bcbs = Some(HashMap::new());
    }
    let mut scbs = SIZE_CALLBACKS.lock().unwrap();
    if scbs.is_none() {
        *scbs = Some(HashMap::new());
    }
    let mut tags = TAG_MAP.lock().unwrap();
    if tags.is_none() {
        *tags = Some(HashMap::new());
    }
}

pub fn register(callback_id: usize, f: impl Fn() + Send + 'static) {
    init();
    let mut cbs = CALLBACKS.lock().unwrap();
    if let Some(map) = cbs.as_mut() {
        map.insert(callback_id, Box::new(f));
    }
}

pub fn register_with_auto_id(f: impl Fn() + Send + 'static) -> usize {
    let id = next_id();
    register(id, f);
    id
}

pub fn register_value(callback_id: usize, f: impl Fn(String) + Send + 'static) {
    init();
    let mut vcbs = VALUE_CALLBACKS.lock().unwrap();
    if let Some(map) = vcbs.as_mut() {
        map.insert(callback_id, Box::new(f));
    }
}

pub fn register_value_with_auto_id(f: impl Fn(String) + Send + 'static) -> usize {
    let id = next_id();
    register_value(id, f);
    id
}

pub fn register_bool(callback_id: usize, f: impl Fn(bool) + Send + 'static) {
    init();
    let mut bcbs = BOOL_CALLBACKS.lock().unwrap();
    if let Some(map) = bcbs.as_mut() {
        map.insert(callback_id, Box::new(f));
    }
}

pub fn register_bool_with_auto_id(f: impl Fn(bool) + Send + 'static) -> usize {
    let id = next_id();
    register_bool(id, f);
    id
}

pub fn register_size(callback_id: usize, f: impl Fn(f64, f64) + Send + 'static) {
    init();
    let mut scbs = SIZE_CALLBACKS.lock().unwrap();
    if let Some(map) = scbs.as_mut() {
        map.insert(callback_id, Box::new(f));
    }
}

pub fn register_size_with_auto_id(f: impl Fn(f64, f64) + Send + 'static) -> usize {
    let id = next_id();
    register_size(id, f);
    id
}

pub fn map_tag(tag: isize, callback_id: usize) {
    init();
    let mut tags = TAG_MAP.lock().unwrap();
    if let Some(map) = tags.as_mut() {
        map.insert(tag, callback_id);
    }
}

pub fn dispatch_by_tag(tag: isize) {
    let callback_id = {
        let tags = TAG_MAP.lock().unwrap();
        tags.as_ref().and_then(|m| m.get(&tag).copied())
    };
    if let Some(id) = callback_id {
        dispatch(id);
    }
}

pub fn dispatch(callback_id: usize) {
    let cbs = CALLBACKS.lock().unwrap();
    if let Some(map) = cbs.as_ref()
        && let Some(f) = map.get(&callback_id)
    {
        f();
    }
}

pub fn dispatch_value(callback_id: usize, value: String) {
    let vcbs = VALUE_CALLBACKS.lock().unwrap();
    if let Some(map) = vcbs.as_ref()
        && let Some(f) = map.get(&callback_id)
    {
        f(value);
    }
}

pub fn dispatch_bool(callback_id: usize, value: bool) {
    let bcbs = BOOL_CALLBACKS.lock().unwrap();
    if let Some(map) = bcbs.as_ref()
        && let Some(f) = map.get(&callback_id)
    {
        f(value);
    }
}

pub fn dispatch_size(callback_id: usize, width: f64, height: f64) {
    let scbs = SIZE_CALLBACKS.lock().unwrap();
    if let Some(map) = scbs.as_ref()
        && let Some(f) = map.get(&callback_id)
    {
        f(width, height);
    }
}

/// Register the ObjC action handler class (call once at app startup)
#[cfg(not(test))]
pub fn register_action_class() -> &'static Class {
    use std::sync::Once;
    static REGISTER: Once = Once::new();
    static mut CLASS: Option<&'static Class> = None;

    REGISTER.call_once(|| {
        let superclass = Class::get("NSObject").unwrap();
        let mut decl = ClassDecl::new("CocoanutActionHandler", superclass).unwrap();

        extern "C" fn handle_action(_this: &Object, _cmd: Sel, sender: *mut Object) {
            unsafe {
                let tag: isize = objc::msg_send![sender, tag];
                dispatch_by_tag(tag);
            }
        }

        unsafe {
            decl.add_method(
                objc::sel!(handleAction:),
                handle_action as extern "C" fn(&Object, Sel, *mut Object),
            );
            CLASS = Some(decl.register());
        }
    });

    unsafe { CLASS.unwrap() }
}

/// Create a singleton action handler instance
#[cfg(not(test))]
pub fn action_handler() -> *mut Object {
    use std::sync::Once;
    static INIT: Once = Once::new();
    static mut HANDLER: *mut Object = std::ptr::null_mut();

    INIT.call_once(|| {
        let cls = register_action_class();
        unsafe {
            let obj: *mut Object = objc::msg_send![cls, new];
            HANDLER = obj;
        }
    });

    unsafe { HANDLER }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use std::sync::Mutex;
    use std::sync::atomic::{AtomicUsize, Ordering};

    #[test]
    fn test_register_and_dispatch() {
        let counter = Arc::new(AtomicUsize::new(0));
        let c = counter.clone();
        register(100, move || {
            c.fetch_add(1, Ordering::SeqCst);
        });
        dispatch(100);
        assert_eq!(counter.load(Ordering::SeqCst), 1);
        dispatch(100);
        assert_eq!(counter.load(Ordering::SeqCst), 2);
    }

    #[test]
    fn test_tag_mapping() {
        let counter = Arc::new(AtomicUsize::new(0));
        let c = counter.clone();
        register(200, move || {
            c.fetch_add(1, Ordering::SeqCst);
        });
        map_tag(42, 200);
        dispatch_by_tag(42);
        assert_eq!(counter.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_dispatch_nonexistent() {
        dispatch(99999); // should not panic
        dispatch_by_tag(99999); // should not panic
    }

    #[test]
    fn callback_id_conversions() {
        let c = CallbackId::new(5);
        assert_eq!(c.as_usize(), 5);
        let u: usize = c.into();
        assert_eq!(u, 5);
        let c2: CallbackId = 7.into();
        assert_eq!(c2.as_usize(), 7);
    }

    #[test]
    fn register_with_auto_id_increments() {
        let id1 = register_with_auto_id(|| {});
        let id2 = register_with_auto_id(|| {});
        assert_ne!(id1, id2);
        dispatch(id1);
        dispatch(id2);
    }

    #[test]
    fn register_value_dispatch_value() {
        let seen = Arc::new(Mutex::new(String::new()));
        let s = seen.clone();
        register_value(3001, move |t| {
            *s.lock().unwrap() = t;
        });
        dispatch_value(3001, "payload".into());
        assert_eq!(*seen.lock().unwrap(), "payload");
    }

    #[test]
    fn register_bool_dispatch_bool() {
        let seen = Arc::new(Mutex::new(None::<bool>));
        let s = seen.clone();
        register_bool(3002, move |b| {
            *s.lock().unwrap() = Some(b);
        });
        dispatch_bool(3002, true);
        assert_eq!(*seen.lock().unwrap(), Some(true));
    }

    #[test]
    fn register_size_dispatch_size() {
        let seen = Arc::new(Mutex::new((0.0_f64, 0.0_f64)));
        let s = seen.clone();
        register_size(3003, move |w, h| {
            *s.lock().unwrap() = (w, h);
        });
        dispatch_size(3003, 640.0, 480.0);
        assert_eq!(*seen.lock().unwrap(), (640.0, 480.0));
    }

    #[test]
    fn test_register_value_with_auto_id() {
        let id = register_value_with_auto_id(|_| {});
        dispatch_value(id, "x".into());
    }
}