cranpose-ui 0.20.0

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use cranpose_core::NodeId;

use crate::{
    clear_focus_invalidations, clear_pointer_repasses, clear_semantics_invalidations,
    has_pending_focus_invalidations, has_pending_pointer_repasses,
    has_pending_semantics_invalidations, process_focus_invalidations, process_pointer_repasses,
    process_semantics_invalidations,
    render_state::{AppContext, app_context_test_scope},
    schedule_focus_invalidation, schedule_pointer_repass, schedule_semantics_invalidation,
};

struct Queue {
    name: &'static str,
    schedule: fn(NodeId),
    has_pending: fn() -> bool,
    process: fn(&mut dyn FnMut(NodeId)),
    clear: fn(),
}

impl Queue {
    fn take(&self) -> Vec<NodeId> {
        let mut processed = Vec::new();
        (self.process)(&mut |node_id| processed.push(node_id));
        processed.sort_unstable();
        processed
    }
}

const QUEUES: [Queue; 3] = [
    Queue {
        name: "pointer",
        schedule: schedule_pointer_repass,
        has_pending: has_pending_pointer_repasses,
        process: |processor| process_pointer_repasses(processor),
        clear: clear_pointer_repasses,
    },
    Queue {
        name: "focus",
        schedule: schedule_focus_invalidation,
        has_pending: has_pending_focus_invalidations,
        process: |processor| process_focus_invalidations(processor),
        clear: clear_focus_invalidations,
    },
    Queue {
        name: "semantics",
        schedule: schedule_semantics_invalidation,
        has_pending: has_pending_semantics_invalidations,
        process: |processor| process_semantics_invalidations(processor),
        clear: clear_semantics_invalidations,
    },
];

#[test]
fn a_scheduled_node_reaches_the_processor_once() {
    let _app_context = app_context_test_scope();
    for queue in &QUEUES {
        (queue.clear)();
        assert!(!(queue.has_pending)(), "{} starts empty", queue.name);
        assert!(
            queue.take().is_empty(),
            "{} has nothing to hand",
            queue.name
        );

        (queue.schedule)(NodeId(7));
        (queue.schedule)(NodeId(7));
        (queue.schedule)(NodeId(9));
        assert!((queue.has_pending)(), "{} holds the nodes", queue.name);
        assert_eq!(
            queue.take(),
            vec![NodeId(7), NodeId(9)],
            "{} hands each node once",
            queue.name
        );
        assert!(
            !(queue.has_pending)(),
            "{} is empty after a pass",
            queue.name
        );
    }
}

#[test]
fn a_cleared_queue_hands_nothing() {
    let _app_context = app_context_test_scope();
    for queue in &QUEUES {
        (queue.schedule)(NodeId(3));
        (queue.clear)();
        assert!(
            !(queue.has_pending)(),
            "{} is empty after clear",
            queue.name
        );
        assert!(
            queue.take().is_empty(),
            "{} hands nothing after clear",
            queue.name
        );
    }
}

#[test]
fn nodes_a_panicking_processor_did_not_reach_stay_queued() {
    let _app_context = app_context_test_scope();
    for queue in &QUEUES {
        (queue.clear)();
        (queue.schedule)(NodeId(1));
        (queue.schedule)(NodeId(2));
        let mut first = None;
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            (queue.process)(&mut |node_id| {
                first = Some(node_id);
                panic!("{} processor panic", queue.name);
            });
        }));
        assert!(result.is_err(), "{} passes the panic on", queue.name);

        let unreached = if first == Some(NodeId(1)) {
            NodeId(2)
        } else {
            NodeId(1)
        };
        assert_eq!(
            queue.take(),
            vec![unreached],
            "{} keeps the node the processor did not reach and runs again",
            queue.name
        );
        assert!(!(queue.has_pending)(), "{} is empty again", queue.name);
    }
}

#[test]
fn a_processor_schedules_work_for_the_next_pass() {
    let _app_context = app_context_test_scope();
    for queue in &QUEUES {
        (queue.clear)();
        (queue.schedule)(NodeId(1));
        let mut nested = Vec::new();
        (queue.process)(&mut |_| {
            (queue.schedule)(NodeId(2));
            (queue.process)(&mut |node_id| nested.push(node_id));
        });
        assert!(
            nested.is_empty(),
            "{} runs no pass inside a running pass",
            queue.name
        );
        assert!((queue.has_pending)(), "{} holds the new node", queue.name);
        assert_eq!(
            queue.take(),
            vec![NodeId(2)],
            "{} hands it next pass",
            queue.name
        );
    }
}

#[test]
fn each_app_context_has_its_own_queues() {
    let _app_context = app_context_test_scope();
    let first = AppContext::new_with_density(1.0, None);
    let second = AppContext::new_with_density(1.0, None);
    for queue in &QUEUES {
        first.enter(|| {
            (queue.clear)();
            (queue.schedule)(NodeId(7));
        });
        second.enter(|| {
            (queue.clear)();
            assert!(
                !(queue.has_pending)(),
                "{} of another context is empty",
                queue.name
            );
            (queue.schedule)(NodeId(9));
        });
        first.enter(|| assert_eq!(queue.take(), vec![NodeId(7)], "{} of the first", queue.name));
        second.enter(|| {
            assert_eq!(
                queue.take(),
                vec![NodeId(9)],
                "{} of the second",
                queue.name
            );
        });
    }
}