cranpose-ui 0.1.143

UI primitives for Cranpose
Documentation
use std::{cell::RefCell, rc::Rc};

use cranpose_foundation::DRAG_THRESHOLD;

use super::*;

#[derive(Default)]
struct Log {
    lines: RefCell<Vec<String>>,
}

impl Log {
    fn push(&self, line: impl Into<String>) {
        self.lines.borrow_mut().push(line.into());
    }

    fn take(&self) -> Vec<String> {
        std::mem::take(&mut *self.lines.borrow_mut())
    }
}

fn event(kind: PointerEventKind, x: f32, y: f32) -> PointerEvent {
    PointerEvent::new(kind, Point { x, y }, Point { x, y })
        .with_screen_position(Some(Point { x: x + 100.0, y }))
}

fn logged_source(log: &Rc<Log>) -> DragAndDropSource {
    let started = Rc::clone(log);
    let ended = Rc::clone(log);
    DragAndDropSource::new(7u64)
        .on_started(move |point| started.push(format!("started {:?}", point.screen)))
        .on_ended(move |outcome| ended.push(format!("ended {outcome:?}")))
}

fn logged_target(log: &Rc<Log>, accept: bool) -> DragAndDropTarget {
    let entered = Rc::clone(log);
    let moved = Rc::clone(log);
    let exited = Rc::clone(log);
    let dropped = Rc::clone(log);
    DragAndDropTarget::new()
        .on_entered(move |payload| entered.push(format!("entered {}", number(payload))))
        .on_moved(move |_, at| moved.push(format!("moved {},{}", at.x, at.y)))
        .on_exited(move |_| exited.push("exited"))
        .on_drop(move |payload, at| {
            dropped.push(format!("drop {} at {},{}", number(payload), at.x, at.y));
            accept
        })
}

fn number(payload: &DragAndDropPayload) -> u64 {
    payload.downcast_ref::<u64>().copied().unwrap_or(0)
}

fn drag(gesture: &mut SourceGesture, source: &DragAndDropSource, state: &DragAndDropState) {
    gesture.on_event(&event(PointerEventKind::Down, 0.0, 0.0), source, state);
    gesture.on_event(
        &event(PointerEventKind::Move, DRAG_THRESHOLD + 2.0, 0.0),
        source,
        state,
    );
}

#[test]
fn a_press_that_stays_within_the_threshold_starts_nothing() {
    let state = DragAndDropState::default();
    let source = DragAndDropSource::new(7u64);
    let mut gesture = SourceGesture::default();
    gesture.on_event(&event(PointerEventKind::Down, 0.0, 0.0), &source, &state);
    let small_move = event(PointerEventKind::Move, DRAG_THRESHOLD - 1.0, 0.0);
    gesture.on_event(&small_move, &source, &state);
    assert!(!state.is_active(), "no transfer below the drag threshold");
    assert!(
        !small_move.is_consumed(),
        "the press stays a press for others"
    );
    gesture.on_event(&event(PointerEventKind::Up, 3.0, 0.0), &source, &state);
    assert!(!state.route(|_| None), "nothing to route");
}

#[test]
fn a_drag_past_the_threshold_starts_moves_and_drops() {
    let log = Rc::new(Log::default());
    let state = DragAndDropState::default();
    let source = logged_source(&log);
    let mut gesture = SourceGesture::default();
    drag(&mut gesture, &source, &state);
    assert!(state.is_active(), "the transfer started");
    let moved = event(PointerEventKind::Move, 30.0, 0.0);
    gesture.on_event(&moved, &source, &state);
    assert!(moved.is_consumed(), "a running transfer owns the pointer");
    gesture.on_event(&event(PointerEventKind::Up, 40.0, 0.0), &source, &state);
    assert!(state.route(|_| None));
    assert_eq!(
        log.take(),
        vec![
            format!("started {:?}", Some(Point { x: 110.0, y: 0.0 })),
            "ended Missed".to_string()
        ]
    );
    assert!(!state.is_active(), "a release ends the transfer");
}

#[test]
fn routing_tells_the_target_enter_move_exit_and_drop() {
    let log = Rc::new(Log::default());
    let state = DragAndDropState::default();
    state.register_target(5, logged_target(&log, true));
    let source = logged_source(&log);
    let mut gesture = SourceGesture::default();
    drag(&mut gesture, &source, &state);
    let over_target = |point: DragAndDropPoint| (point.local.x > 50.0).then_some((5, point.local));
    assert!(state.route(over_target));
    assert_eq!(log.take(), vec!["started Some(Point { x: 110.0, y: 0.0 })"]);

    gesture.on_event(&event(PointerEventKind::Move, 60.0, 5.0), &source, &state);
    state.route(over_target);
    assert_eq!(log.take(), vec!["entered 7", "moved 60,5"]);

    gesture.on_event(&event(PointerEventKind::Move, 20.0, 5.0), &source, &state);
    state.route(over_target);
    assert_eq!(log.take(), vec!["exited"]);

    gesture.on_event(&event(PointerEventKind::Move, 70.0, 5.0), &source, &state);
    gesture.on_event(&event(PointerEventKind::Up, 70.0, 5.0), &source, &state);
    state.route(over_target);
    assert_eq!(
        log.take(),
        vec![
            "entered 7",
            "moved 70,5",
            "moved 70,5",
            "drop 7 at 70,5",
            "ended Dropped"
        ]
    );
    assert!(!state.is_active());
}

#[test]
fn a_target_that_declines_leaves_the_source_with_a_miss() {
    let log = Rc::new(Log::default());
    let state = DragAndDropState::default();
    state.register_target(5, logged_target(&log, false));
    let source = logged_source(&log);
    let mut gesture = SourceGesture::default();
    drag(&mut gesture, &source, &state);
    gesture.on_event(&event(PointerEventKind::Up, 70.0, 5.0), &source, &state);
    state.route(|point| Some((5, point.local)));
    assert_eq!(
        log.take(),
        vec![
            "started Some(Point { x: 110.0, y: 0.0 })",
            "entered 7",
            "moved 10,0",
            "moved 70,5",
            "drop 7 at 70,5",
            "ended Missed"
        ]
    );
}

#[test]
fn a_cancelled_gesture_exits_the_target_and_ends_cancelled() {
    let log = Rc::new(Log::default());
    let state = DragAndDropState::default();
    state.register_target(5, logged_target(&log, true));
    let source = logged_source(&log);
    let mut gesture = SourceGesture::default();
    drag(&mut gesture, &source, &state);
    state.route(|point| Some((5, point.local)));
    log.take();
    gesture.on_event(&event(PointerEventKind::Cancel, 10.0, 0.0), &source, &state);
    state.route(|point| Some((5, point.local)));
    assert_eq!(log.take(), vec!["exited", "ended Cancelled"]);
    assert!(!state.is_active());
}

#[test]
fn a_target_that_leaves_the_tree_is_forgotten() {
    let state = DragAndDropState::default();
    state.register_target(5, DragAndDropTarget::new());
    assert!(state.is_target(5));
    state.unregister_target(5);
    assert!(!state.is_target(5));
}

#[test]
fn the_target_modifier_registers_its_node_while_attached() {
    let node = Rc::new(std::cell::Cell::new(None));
    let captured = Rc::clone(&node);
    let composition = crate::run_test_composition(move || {
        let id = crate::widgets::Box(
            Modifier::empty().drag_and_drop_target(DragAndDropTarget::new()),
            crate::widgets::BoxSpec::default(),
            || {},
        );
        captured.set(Some(id));
    });
    let id = node.get().expect("the target composed");
    let registered =
        current_app_context().is_some_and(|context| context.drag_and_drop().is_target(id));
    assert!(registered, "the node is a target while attached");
    drop(composition);
}