fui-rs 0.2.12

Web-first retained Rust UI for WebAssembly and native desktop applications
Documentation
use crate::node::{Node, NodeRef};
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::rc::Rc;

type LifecycleHandler = Box<dyn Fn(&RetainedView)>;
type DisposeHandler = Box<dyn FnOnce()>;

struct RetainedViewInner {
    root: NodeRef,
    active: Cell<bool>,
    disposed: Cell<bool>,
    resources: RefCell<Vec<Box<dyn Any>>>,
    activation_handlers: RefCell<Vec<LifecycleHandler>>,
    deactivation_handlers: RefCell<Vec<LifecycleHandler>>,
    disposal_handlers: RefCell<Vec<DisposeHandler>>,
}

impl Drop for RetainedViewInner {
    fn drop(&mut self) {
        if !self.disposed.get() {
            self.disposed.set(true);
            for handler in self.disposal_handlers.get_mut().drain(..) {
                handler();
            }
            self.resources.get_mut().clear();
            self.root.dispose();
        }
    }
}

#[derive(Clone)]
pub struct RetainedView {
    inner: Rc<RetainedViewInner>,
}

impl RetainedView {
    pub fn new<T: Node + 'static>(root: &T) -> Self {
        Self {
            inner: Rc::new(RetainedViewInner {
                root: root.node_ref(),
                active: Cell::new(false),
                disposed: Cell::new(false),
                resources: RefCell::new(vec![Box::new(root.clone())]),
                activation_handlers: RefCell::new(Vec::new()),
                deactivation_handlers: RefCell::new(Vec::new()),
                disposal_handlers: RefCell::new(Vec::new()),
            }),
        }
    }

    pub fn root(&self) -> NodeRef {
        self.inner.root.clone()
    }
    pub fn is_active(&self) -> bool {
        self.inner.active.get()
    }
    pub fn is_disposed(&self) -> bool {
        self.inner.disposed.get()
    }

    pub fn keep_alive<T: 'static>(self, resource: T) -> Self {
        if !self.inner.disposed.get() {
            self.inner.resources.borrow_mut().push(Box::new(resource));
        }
        self
    }

    pub fn on_activate(self, handler: impl Fn(&RetainedView) + 'static) -> Self {
        self.inner
            .activation_handlers
            .borrow_mut()
            .push(Box::new(handler));
        self
    }

    pub fn on_deactivate(self, handler: impl Fn(&RetainedView) + 'static) -> Self {
        self.inner
            .deactivation_handlers
            .borrow_mut()
            .push(Box::new(handler));
        self
    }

    pub fn on_dispose(self, handler: impl FnOnce() + 'static) -> Self {
        self.inner
            .disposal_handlers
            .borrow_mut()
            .push(Box::new(handler));
        self
    }

    pub fn activate(&self) {
        if self.inner.disposed.get() || self.inner.active.replace(true) {
            return;
        }
        for handler in self.inner.activation_handlers.borrow().iter() {
            handler(self);
        }
    }

    pub fn deactivate(&self) {
        if self.inner.disposed.get() || !self.inner.active.replace(false) {
            return;
        }
        crate::event::deactivate_subtree(&self.inner.root);
        for handler in self.inner.deactivation_handlers.borrow().iter() {
            handler(self);
        }
    }

    pub fn dispose(&self) {
        if self.inner.disposed.replace(true) {
            return;
        }
        if self.inner.active.replace(false) {
            crate::event::deactivate_subtree(&self.inner.root);
            for handler in self.inner.deactivation_handlers.borrow().iter() {
                handler(self);
            }
        }
        for handler in std::mem::take(&mut *self.inner.disposal_handlers.borrow_mut()) {
            handler();
        }
        self.inner.resources.borrow_mut().clear();
        self.inner.root.dispose();
        self.inner.activation_handlers.borrow_mut().clear();
        self.inner.deactivation_handlers.borrow_mut().clear();
    }
}

pub fn retained_view<T: Node + 'static>(root: &T) -> RetainedView {
    RetainedView::new(root)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ffi::{self, Call};
    use crate::node::{flex_box, Node, ThemeBindable};
    use crate::theme::{current_theme, generate_theme, use_custom_theme};
    use std::cell::RefCell;

    struct DropProbe(Rc<Cell<u32>>);
    impl Drop for DropProbe {
        fn drop(&mut self) {
            self.0.set(self.0.get() + 1);
        }
    }

    #[test]
    fn retained_view_orders_lifecycle_retains_state_and_disposes_once() {
        let root = flex_box();
        let events = Rc::new(RefCell::new(Vec::new()));
        let drops = Rc::new(Cell::new(0));
        let view = retained_view(&root)
            .keep_alive(DropProbe(drops.clone()))
            .on_activate({
                let events = events.clone();
                move |_| events.borrow_mut().push("activate")
            })
            .on_deactivate({
                let events = events.clone();
                move |_| events.borrow_mut().push("deactivate")
            })
            .on_dispose({
                let events = events.clone();
                move || events.borrow_mut().push("dispose")
            });

        view.activate();
        view.activate();
        view.deactivate();
        view.activate();
        view.dispose();
        view.dispose();

        assert_eq!(
            &*events.borrow(),
            &[
                "activate",
                "deactivate",
                "activate",
                "deactivate",
                "dispose"
            ]
        );
        assert_eq!(drops.get(), 1);
        assert!(view.is_disposed());
        assert!(!view.is_active());
        assert!(!root.has_built_handle());
    }

    #[test]
    fn final_owner_drop_disposes_retained_resources_and_root() {
        let root = flex_box();
        root.build();
        let drops = Rc::new(Cell::new(0));
        {
            let _view = retained_view(&root).keep_alive(DropProbe(drops.clone()));
        }
        assert_eq!(drops.get(), 1);
        assert!(!root.has_built_handle());
    }

    #[test]
    fn retained_view_keeps_concrete_root_theme_binding_alive() {
        ffi::test::reset();
        let previous_theme = current_theme();
        let root = flex_box();
        root.bind_theme(|root, theme| {
            root.bg_color(theme.colors.surface);
        });
        root.build();
        let handle = root.handle().raw();
        let view = retained_view(&root);
        drop(root);
        ffi::test::take_calls();

        let changed = generate_theme(false, 0x2468ACFF);
        use_custom_theme(changed.clone());
        let calls = ffi::test::take_calls();
        assert!(calls.iter().any(|call| matches!(
            call,
            Call::SetBgColor {
                handle: actual,
                color,
            } if *actual == handle && *color == changed.colors.surface
        )));

        view.dispose();
        use_custom_theme(previous_theme);
    }
}