fui-rs 0.2.3-alpha2

Retained-mode Rust UI SDK for EffinDOM
Documentation
use std::cell::RefCell;
use std::rc::{Rc, Weak};

pub type Callback = Rc<dyn Fn()>;

struct ListenerEntry {
    id: usize,
    callback: Callback,
}

struct ListenerStore {
    next_id: usize,
    listeners: Vec<ListenerEntry>,
}

impl ListenerStore {
    fn new() -> Self {
        Self {
            next_id: 1,
            listeners: Vec::new(),
        }
    }

    fn subscribe(&mut self, callback: Callback) -> usize {
        let id = self.next_id;
        self.next_id += 1;
        self.listeners.push(ListenerEntry { id, callback });
        id
    }

    fn unsubscribe(&mut self, id: usize) {
        self.listeners.retain(|listener| listener.id != id);
    }

    fn callbacks(&self) -> Vec<Callback> {
        self.listeners
            .iter()
            .map(|listener| listener.callback.clone())
            .collect()
    }
}

#[must_use = "subscription is removed when the guard is dropped"]
pub struct SubscriptionGuard {
    store: Weak<RefCell<ListenerStore>>,
    id: usize,
}

pub type Subscription = SubscriptionGuard;

impl SubscriptionGuard {
    fn new(store: &Rc<RefCell<ListenerStore>>, id: usize) -> Self {
        Self {
            store: Rc::downgrade(store),
            id,
        }
    }
}

impl Drop for SubscriptionGuard {
    fn drop(&mut self) {
        if let Some(store) = self.store.upgrade() {
            if let Ok(mut store) = store.try_borrow_mut() {
                store.unsubscribe(self.id);
            }
        }
    }
}

pub struct Signal<T> {
    value: T,
    listeners: Rc<RefCell<ListenerStore>>,
}

impl<T> Signal<T> {
    pub fn new(initial: T) -> Self {
        Self {
            value: initial,
            listeners: Rc::new(RefCell::new(ListenerStore::new())),
        }
    }

    pub fn subscribe(&mut self, callback: Callback) -> SubscriptionGuard {
        let id = self.listeners.borrow_mut().subscribe(callback);
        SubscriptionGuard::new(&self.listeners, id)
    }
}

impl<T: Clone> Signal<T> {
    pub fn get(&self) -> T {
        self.value.clone()
    }
}

impl<T: PartialEq> Signal<T> {
    pub fn set(&mut self, next: T) -> Option<Vec<Callback>> {
        if self.value == next {
            return None;
        }

        self.value = next;
        Some(self.listeners.borrow().callbacks())
    }
}

#[cfg(test)]
mod tests {
    use super::{Callback, Signal};
    use std::cell::Cell;
    use std::rc::Rc;

    #[test]
    fn dropping_subscription_guard_unsubscribes() {
        let fired = Rc::new(Cell::new(0));
        let fired_clone = fired.clone();
        let callback: Callback = Rc::new(move || {
            fired_clone.set(fired_clone.get() + 1);
        });

        let mut signal = Signal::new(1);
        let guard = signal.subscribe(callback);
        let listeners = signal.set(2).unwrap();
        for listener in listeners {
            listener();
        }
        assert_eq!(fired.get(), 1);

        drop(guard);

        let listeners = signal.set(3).unwrap();
        assert!(listeners.is_empty());
        assert_eq!(fired.get(), 1);
    }
}