cocoanut 0.2.3

A minimal, declarative macOS GUI framework for Rust
use std::sync::{Arc, Mutex};

/// Reactive state container with change notification
///
/// # Examples
///
/// ```
/// use cocoanut::prelude::*;
///
/// let count = state(0);
/// count.set(42);
/// assert_eq!(count.get(), 42);
/// ```
#[must_use = "State must be used to track or display values"]
pub struct State<T: Send + 'static> {
    inner: Arc<Mutex<StateInner<T>>>,
}

type StateListener<T> = Box<dyn Fn(&T) + Send>;

struct StateInner<T> {
    value: T,
    listeners: Vec<StateListener<T>>,
}

impl<T: Send + 'static> State<T> {
    pub fn new(initial: T) -> Self {
        Self {
            inner: Arc::new(Mutex::new(StateInner {
                value: initial,
                listeners: Vec::new(),
            })),
        }
    }

    pub fn get(&self) -> T
    where
        T: Clone,
    {
        self.inner.lock().unwrap().value.clone()
    }

    pub fn set(&self, value: T) {
        let mut inner = self.inner.lock().unwrap();
        inner.value = value;
        for listener in &inner.listeners {
            listener(&inner.value);
        }
    }

    pub fn update(&self, f: impl FnOnce(&mut T))
    where
        T: Clone,
    {
        let mut inner = self.inner.lock().unwrap();
        f(&mut inner.value);
        for listener in &inner.listeners {
            listener(&inner.value);
        }
    }

    pub fn on_change(&self, f: impl Fn(&T) + Send + 'static) {
        self.inner.lock().unwrap().listeners.push(Box::new(f));
    }

    pub fn modify(&self, f: impl FnOnce(&mut T))
    where
        T: Clone,
    {
        self.update(f);
    }

    pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> U
    where
        T: Clone,
    {
        let inner = self.inner.lock().unwrap();
        f(&inner.value)
    }

    pub fn bind<F>(&self, format_fn: F) -> Binding
    where
        T: Clone + std::str::FromStr,
        F: Fn(&T) -> String + Send + 'static,
    {
        let binding = Binding {
            update_callback_id: Arc::new(Mutex::new(None)),
        };

        let state = self.clone();
        let update_id = crate::event::register_value_with_auto_id(move |s: String| {
            if let Ok(val) = s.parse::<T>() {
                state.set(val);
            }
        });

        *binding.update_callback_id.lock().unwrap() = Some(update_id);

        self.on_change(move |val| {
            let text = format_fn(val);
            crate::event::dispatch_value(update_id, text.clone());
        });

        binding
    }
}

impl<T: Send + 'static> Clone for State<T> {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
        }
    }
}

pub struct Binding {
    update_callback_id: Arc<Mutex<Option<usize>>>,
}

impl Binding {
    pub fn callback_id(&self) -> Option<usize> {
        *self.update_callback_id.lock().unwrap()
    }
}

pub fn counter_state(increment_id: usize, decrement_id: usize, reset_id: usize) -> State<i64> {
    let state = State::new(0_i64);

    {
        let s = state.clone();
        crate::event::register(increment_id, move || {
            s.update(|v| *v += 1);
        });
    }
    {
        let s = state.clone();
        crate::event::register(decrement_id, move || {
            s.update(|v| *v -= 1);
        });
    }
    {
        let s = state.clone();
        crate::event::register(reset_id, move || {
            s.set(0);
        });
    }

    state
}

pub fn state<T: Send + 'static>(initial: T) -> State<T> {
    State::new(initial)
}

impl State<i64> {
    pub fn increment(&self) {
        self.update(|v| *v += 1);
    }

    pub fn decrement(&self) {
        self.update(|v| *v -= 1);
    }

    pub fn reset(&self) {
        self.set(0);
    }
}

impl State<String> {
    pub fn append(&self, s: &str) {
        self.update(|v| v.push_str(s));
    }

    pub fn clear(&self) {
        self.set(String::new());
    }
}

impl State<bool> {
    pub fn toggle(&self) {
        self.update(|v| *v = !*v);
    }
}

/// Bind a State<T> to update an NSTextField label identified by its view tag.
///
/// Use `.tag(N)` on the label view, then `bind_label(&state, N, |v| format!(...))`.
#[cfg(not(test))]
pub fn bind_label<T: Send + std::fmt::Display + 'static>(
    state: &State<T>,
    view_tag: isize,
    format_fn: impl Fn(&T) -> String + Send + 'static,
) {
    state.on_change(move |val| {
        let text = format_fn(val);
        update_label_by_tag(view_tag, &text);
    });
}

/// Update an NSTextField's string value by its NSView tag.
#[cfg(not(test))]
pub fn update_label_by_tag(tag: isize, text: &str) {
    use objc::runtime::{Class, Object};
    use objc::{msg_send, sel, sel_impl};
    unsafe {
        let ns_app: *mut Object =
            msg_send![Class::get("NSApplication").unwrap(), sharedApplication];
        let window: *mut Object = msg_send![ns_app, keyWindow];
        if window.is_null() {
            return;
        }
        let content: *mut Object = msg_send![window, contentView];
        if content.is_null() {
            return;
        }
        let target: *mut Object = msg_send![content, viewWithTag: tag];
        if !target.is_null() {
            let cstr = std::ffi::CString::new(text).unwrap();
            let ns: *mut Object =
                msg_send![objc::class!(NSString), stringWithUTF8String: cstr.as_ptr()];
            let _: () = msg_send![target, setStringValue: ns];
        }
    }
}

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

    #[test]
    fn test_state_get_set() {
        let s = State::new(42_i64);
        assert_eq!(s.get(), 42);
        s.set(100);
        assert_eq!(s.get(), 100);
    }

    #[test]
    fn test_state_update() {
        let s = State::new(10_i64);
        s.update(|v| *v += 5);
        assert_eq!(s.get(), 15);
    }

    #[test]
    fn test_state_on_change() {
        let s = State::new(0_i64);
        let observed = Arc::new(AtomicI64::new(0));
        let o = observed.clone();
        s.on_change(move |v| {
            o.store(*v, Ordering::SeqCst);
        });
        s.set(42);
        assert_eq!(observed.load(Ordering::SeqCst), 42);
        s.update(|v| *v += 8);
        assert_eq!(observed.load(Ordering::SeqCst), 50);
    }

    #[test]
    fn test_state_clone_shares() {
        let s1 = State::new(0_i64);
        let s2 = s1.clone();
        s1.set(99);
        assert_eq!(s2.get(), 99);
    }

    #[test]
    fn test_counter_state() {
        let s = counter_state(901, 902, 903);
        crate::event::dispatch(901);
        assert_eq!(s.get(), 1);
        crate::event::dispatch(901);
        assert_eq!(s.get(), 2);
        crate::event::dispatch(902);
        assert_eq!(s.get(), 1);
        crate::event::dispatch(903);
        assert_eq!(s.get(), 0);
    }

    #[test]
    fn test_state_modify_alias() {
        let s = State::new(3_i64);
        s.modify(|v| *v *= 2);
        assert_eq!(s.get(), 6);
    }

    #[test]
    fn test_state_map() {
        let s = State::new(11_i64);
        let doubled = s.map(|v| *v * 2);
        assert_eq!(doubled, 22);
    }

    #[test]
    fn test_state_string_helpers() {
        let s = State::new(String::from("a"));
        s.append("bc");
        assert_eq!(s.get(), "abc");
        s.clear();
        assert_eq!(s.get(), "");
    }

    #[test]
    fn test_state_bool_toggle() {
        let s = State::new(false);
        s.toggle();
        assert!(s.get());
        s.toggle();
        assert!(!s.get());
    }

    #[test]
    fn test_binding_callback_id() {
        let s = State::new(0_i32);
        let b = s.bind(|v| v.to_string());
        assert!(b.callback_id().is_some());
    }
}