cranpose-ui 0.21.1

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use super::*;

#[test]
fn active_focus_target_tracking() {
    let _app_context = crate::render_state::app_context_test_scope();
    set_active_focus_target(None);
    assert_eq!(active_focus_target(), None);

    let node: NodeId = NodeId(42);
    set_active_focus_target(Some(node));
    assert_eq!(active_focus_target(), Some(node));

    set_active_focus_target(None);
    assert_eq!(active_focus_target(), None);
}

#[test]
fn active_focus_target_is_scoped_by_app_context() {
    let _app_context = crate::render_state::app_context_test_scope();
    let first = crate::render_state::AppContext::new_with_density(1.0, None);
    let second = crate::render_state::AppContext::new_with_density(1.0, None);

    first.enter(|| set_active_focus_target(Some(NodeId(17))));
    second.enter(|| {
        assert_eq!(active_focus_target(), None);
        set_active_focus_target(Some(NodeId(19)));
    });

    first.enter(|| assert_eq!(active_focus_target(), Some(NodeId(17))));
    second.enter(|| assert_eq!(active_focus_target(), Some(NodeId(19))));
}

struct RecordingTarget {
    states: RefCell<Vec<FocusState>>,
    on_active: RefCell<Option<Box<dyn Fn()>>>,
}

impl RecordingTarget {
    fn new() -> Rc<Self> {
        Rc::new(Self {
            states: RefCell::new(Vec::new()),
            on_active: RefCell::new(None),
        })
    }

    fn states(&self) -> Vec<FocusState> {
        self.states.borrow().clone()
    }
}

impl FocusTargetHandle for RecordingTarget {
    fn set_focus_state(&self, state: FocusState) {
        self.states.borrow_mut().push(state);
        if state == FocusState::Active
            && let Some(callback) = self.on_active.borrow_mut().take()
        {
            callback();
        }
    }
}

fn as_handle(target: &Rc<RecordingTarget>) -> Rc<dyn FocusTargetHandle> {
    Rc::clone(target) as Rc<dyn FocusTargetHandle>
}

#[test]
fn request_focus_moves_focus_between_two_registered_targets() {
    let _app_context = crate::render_state::app_context_test_scope();
    clear_focus_invalidations();
    set_active_focus_target(None);

    let a = RecordingTarget::new();
    let b = RecordingTarget::new();
    register_focus_target(NodeId(1), as_handle(&a));
    register_focus_target(NodeId(2), as_handle(&b));

    assert!(request_focus(NodeId(1)));
    assert_eq!(a.states(), vec![FocusState::Active]);
    assert_eq!(active_focus_target(), Some(NodeId(1)));

    assert!(request_focus(NodeId(2)));
    assert_eq!(a.states(), vec![FocusState::Active, FocusState::Inactive]);
    assert_eq!(b.states(), vec![FocusState::Active]);
    assert_eq!(active_focus_target(), Some(NodeId(2)));
}

#[test]
fn request_focus_on_an_unregistered_node_fails_without_changing_anything() {
    let _app_context = crate::render_state::app_context_test_scope();
    clear_focus_invalidations();
    set_active_focus_target(None);

    assert!(!request_focus(NodeId(99)));
    assert_eq!(active_focus_target(), None);
}

#[test]
fn unregistering_the_active_targets_last_handle_clears_active_focus() {
    let _app_context = crate::render_state::app_context_test_scope();
    clear_focus_invalidations();
    set_active_focus_target(None);

    let a = RecordingTarget::new();
    let handle = as_handle(&a);
    register_focus_target(NodeId(1), Rc::clone(&handle));
    assert!(request_focus(NodeId(1)));
    assert_eq!(active_focus_target(), Some(NodeId(1)));

    unregister_focus_target(NodeId(1), &handle);
    assert_eq!(active_focus_target(), None);
    assert!(!request_focus(NodeId(1)));
}

#[test]
fn request_focus_from_inside_a_callback_does_not_double_borrow_or_recurse() {
    let _app_context = crate::render_state::app_context_test_scope();
    clear_focus_invalidations();
    set_active_focus_target(None);

    let a = RecordingTarget::new();
    let b = RecordingTarget::new();
    register_focus_target(NodeId(1), as_handle(&a));
    register_focus_target(NodeId(2), as_handle(&b));

    *a.on_active.borrow_mut() = Some(Box::new({
        let bounced_back = RefCell::new(false);
        move || {
            if !*bounced_back.borrow() {
                *bounced_back.borrow_mut() = true;
                assert!(request_focus(NodeId(2)));
            }
        }
    }));

    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        assert!(request_focus(NodeId(1)));
    }));
    assert!(
        result.is_ok(),
        "a request_focus call from inside a callback must not panic or overflow the stack"
    );

    assert_eq!(a.states(), vec![FocusState::Active, FocusState::Inactive]);
    assert_eq!(b.states(), vec![FocusState::Active]);
    assert_eq!(active_focus_target(), Some(NodeId(2)));
}

#[test]
fn a_panicking_callback_still_releases_the_dispatch_lock() {
    let _app_context = crate::render_state::app_context_test_scope();
    clear_focus_invalidations();
    set_active_focus_target(None);

    struct PanicsOnActivate;
    impl FocusTargetHandle for PanicsOnActivate {
        fn set_focus_state(&self, state: FocusState) {
            assert!(
                state != FocusState::Active,
                "focus target panicked while activating"
            );
        }
    }

    register_focus_target(
        NodeId(1),
        Rc::new(PanicsOnActivate) as Rc<dyn FocusTargetHandle>,
    );
    let b = RecordingTarget::new();
    register_focus_target(NodeId(2), as_handle(&b));

    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        request_focus(NodeId(1));
    }));
    assert!(result.is_err());

    assert!(request_focus(NodeId(2)));
    assert_eq!(
        b.states(),
        vec![FocusState::Active],
        "the dispatch lock must be released after a callback panic so later requests proceed"
    );
}