use cranpose_foundation::PointerEvent;
use cranpose_ui_graphics::Point;
use super::*;
use crate::modifier::{ModifierNodeSlices, collect_slices_from_modifier};
fn button_handler(modifier: &Modifier) -> (Rc<dyn Fn(PointerEvent)>, ModifierNodeSlices) {
let slices = collect_slices_from_modifier(modifier);
assert_eq!(
slices.pointer_inputs().len(),
1,
"menu button must install exactly one pointer-input gesture"
);
let handler = slices.pointer_inputs()[0].clone();
(handler, slices)
}
fn down(x: f32, y: f32) -> PointerEvent {
PointerEvent::new(PointerEventKind::Down, Point { x, y }, Point { x, y })
}
fn up(x: f32, y: f32) -> PointerEvent {
PointerEvent::new(PointerEventKind::Up, Point { x, y }, Point { x, y })
}
#[test]
fn menu_button_consumes_the_tap_and_runs_the_action() {
let _app_context = crate::render_state::app_context_test_scope();
let ran = Rc::new(Cell::new(false));
let action: Rc<dyn Fn()> = {
let ran = Rc::clone(&ran);
Rc::new(move || ran.set(true))
};
let modifier = menu_item_pointer_input("Copy", action, Rc::new(Cell::new(false)));
let (handler, _slices) = button_handler(&modifier);
let press = down(5.0, 5.0);
handler(press.clone());
assert!(
press.is_consumed(),
"the press must be consumed so it never reaches the field and collapses the selection"
);
assert!(!ran.get(), "the action fires on release, not on press");
let release = up(6.0, 6.0);
handler(release.clone());
assert!(release.is_consumed(), "the release must be consumed too");
assert!(
ran.get(),
"releasing after a press on the button runs the action"
);
}
#[test]
fn menu_button_release_without_press_is_consumed_but_inert() {
let _app_context = crate::render_state::app_context_test_scope();
let ran = Rc::new(Cell::new(false));
let action: Rc<dyn Fn()> = {
let ran = Rc::clone(&ran);
Rc::new(move || ran.set(true))
};
let modifier = menu_item_pointer_input("Cut", action, Rc::new(Cell::new(false)));
let (handler, _slices) = button_handler(&modifier);
let release = up(5.0, 5.0);
handler(release.clone());
assert!(
release.is_consumed(),
"a release on the menu is consumed so it never hits the field"
);
assert!(!ran.get(), "a release with no matching press must not act");
}
#[test]
fn menu_button_accepts_release_from_a_continuous_press() {
let _app_context = crate::render_state::app_context_test_scope();
let ran = Rc::new(Cell::new(false));
let action: Rc<dyn Fn()> = {
let ran = Rc::clone(&ran);
Rc::new(move || ran.set(true))
};
let modifier = menu_item_pointer_input("Copy", action, Rc::new(Cell::new(true)));
let (handler, _slices) = button_handler(&modifier);
let release = up(5.0, 5.0);
handler(release.clone());
assert!(release.is_consumed());
assert!(ran.get());
}
#[test]
fn menu_geometry_matches_the_reference() {
assert_eq!(MENU_HEIGHT, 44.0);
assert_eq!(ITEM_PADDING, 20.0);
assert_eq!(SEPARATOR_WIDTH, 1.0);
assert_eq!(SEPARATOR_HEIGHT, 17.0);
assert_eq!(MENU_GAP_ABOVE_LINE, 15.0);
assert_eq!(MENU_SCREEN_MARGIN, 20.0);
}
#[test]
fn pagination_reserves_the_disc_only_when_overflowing() {
let _app_context = crate::render_state::app_context_test_scope();
let style = menu_text_style(false);
let items: Vec<TextMenuItem> = ["Copy", "Cut", "Paste", "Select all"]
.iter()
.map(|label| TextMenuItem::new(*label, || {}))
.collect();
let one = paginate(&items, &style, f32::INFINITY);
assert_eq!(one.len(), 1, "everything fits on one page");
assert_eq!(one[0].len(), 4);
let total: f32 = items
.iter()
.map(|i| item_width(&i.label, &style))
.sum::<f32>()
+ 3.0 * SEPARATOR_WIDTH;
let narrow = paginate(&items, &style, total * 0.55);
assert!(narrow.len() > 1, "a narrow window must page");
assert!(narrow.iter().all(|page| !page.is_empty()));
let all: Vec<usize> = narrow.iter().flatten().copied().collect();
assert_eq!(all, vec![0, 1, 2, 3], "pages cover every item in order");
}