use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
use crate::geometry::{Point, Size};
use crate::text::Font;
use crate::widget::Widget;
use super::super::geometry::BAR_H;
use super::super::model::MenuItem;
use super::{MenuBar, TopMenu};
fn test_font() -> Arc<Font> {
const FONT_BYTES: &[u8] = include_bytes!("../../../../../demo/assets/CascadiaCode.ttf");
Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
}
#[test]
fn moving_across_top_menus_switches_open_popup() {
let mut bar = MenuBar::new(
test_font(),
vec![
TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
),
TopMenu::new(
"Edit",
vec![MenuItem::action("Copy", "edit.copy").into()],
),
],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
assert_eq!(
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert_eq!(bar.open_index, Some(0));
assert_eq!(
bar.on_event(&Event::MouseMove {
pos: Point::new(60.0, 8.0),
}),
EventResult::Consumed
);
assert_eq!(bar.open_index, Some(1));
let Some(super::super::model::MenuEntry::Item(item)) = bar.popup.items.first() else {
panic!("popup should contain Edit items");
};
assert_eq!(item.action.as_deref(), Some("edit.copy"));
}
#[test]
fn top_level_menu_tracks_hover() {
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
)],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
assert_eq!(
bar.on_event(&Event::MouseMove {
pos: Point::new(8.0, 8.0),
}),
EventResult::Ignored
);
assert_eq!(bar.hover_index, Some(0));
}
#[test]
fn hover_change_advances_invalidation_epoch() {
crate::touch_state::clear_last_touch_event_for_testing();
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
)],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
let before = crate::animation::invalidation_epoch();
bar.on_event(&Event::MouseMove {
pos: Point::new(8.0, 8.0),
});
assert!(
crate::animation::invalidation_epoch() != before,
"MenuBar hover change must advance the invalidation epoch so the \
parent Window's backbuffer cache invalidates"
);
let before = crate::animation::invalidation_epoch();
bar.on_event(&Event::MouseMove {
pos: Point::new(10.0, 8.0),
});
assert_eq!(
crate::animation::invalidation_epoch(),
before,
"MouseMove that doesn't change hover_index should not advance \
the epoch"
);
}
#[test]
fn mouse_down_drag_release_activates_popup_item() {
let viewport = Size::new(300.0, 180.0);
crate::widget::set_current_viewport(viewport);
let actions = Rc::new(RefCell::new(Vec::new()));
let actions_for_cb = Rc::clone(&actions);
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
)],
move |action| actions_for_cb.borrow_mut().push(action.to_string()),
);
bar.layout(Size::new(300.0, BAR_H));
assert_eq!(
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
let row = bar.popup.state.layouts(&bar.popup.items, viewport)[0].rows[0].rect;
let item_pos = Point::new(row.x + 12.0, row.y + 12.0);
assert_eq!(
bar.on_event(&Event::MouseMove { pos: item_pos }),
EventResult::Consumed
);
assert_eq!(
bar.on_event(&Event::MouseUp {
pos: item_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert_eq!(actions.borrow().as_slice(), ["file.new"]);
assert!(!bar.popup.is_open());
}
#[test]
fn simple_mouse_click_opens_menu_without_release_activation() {
let viewport = Size::new(300.0, 180.0);
crate::widget::set_current_viewport(viewport);
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
)],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
assert_eq!(
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert_eq!(
bar.on_event(&Event::MouseUp {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert!(bar.popup.is_open());
assert_eq!(bar.open_index, Some(0));
}
#[test]
fn click_on_currently_open_top_menu_closes_popup() {
crate::touch_state::clear_last_touch_event_for_testing();
let viewport = Size::new(300.0, 180.0);
crate::widget::set_current_viewport(viewport);
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
)],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
bar.on_event(&Event::MouseUp {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert!(bar.popup.is_open());
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
bar.on_event(&Event::MouseUp {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert!(
!bar.popup.is_open(),
"click on the currently-open top menu must close it (desktop toggle)"
);
}
#[test]
fn mobile_tap_sequence_keeps_other_top_menu_open() {
let viewport = Size::new(300.0, 180.0);
crate::widget::set_current_viewport(viewport);
let mut bar = MenuBar::new(
test_font(),
vec![
TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
),
TopMenu::new(
"Edit",
vec![MenuItem::action("Copy", "edit.copy").into()],
),
],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
let file_pos = Point::new(8.0, 8.0);
crate::touch_state::note_touch_event();
bar.on_event(&Event::MouseMove { pos: file_pos });
crate::touch_state::note_touch_event();
bar.on_event(&Event::MouseDown {
pos: file_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
bar.on_event(&Event::MouseUp {
pos: file_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert!(bar.popup.is_open());
assert_eq!(bar.open_index, Some(0));
let edit_pos = Point::new(60.0, 8.0);
crate::touch_state::note_touch_event();
bar.on_event(&Event::MouseMove { pos: edit_pos });
assert_eq!(
bar.open_index,
Some(0),
"synthesised pre-tap MouseMove must not switch the open menu — \
only the subsequent MouseDown should",
);
crate::touch_state::note_touch_event();
bar.on_event(&Event::MouseDown {
pos: edit_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
bar.on_event(&Event::MouseUp {
pos: edit_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert!(
bar.popup.is_open(),
"Edit must stay open after the tap completes",
);
assert_eq!(bar.open_index, Some(1));
}
#[test]
fn tap_on_other_top_menu_switches_open_popup() {
let viewport = Size::new(300.0, 180.0);
crate::widget::set_current_viewport(viewport);
let mut bar = MenuBar::new(
test_font(),
vec![
TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
),
TopMenu::new(
"Edit",
vec![MenuItem::action("Copy", "edit.copy").into()],
),
],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
bar.on_event(&Event::MouseDown {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
bar.on_event(&Event::MouseUp {
pos: Point::new(8.0, 8.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert!(bar.popup.is_open());
assert_eq!(bar.open_index, Some(0));
let edit_pos = Point::new(60.0, 8.0);
assert_eq!(
bar.on_event(&Event::MouseDown {
pos: edit_pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
}),
EventResult::Consumed,
);
assert!(
bar.popup.is_open(),
"tapping a different top menu must keep a popup open (Edit's), not just close File's"
);
assert_eq!(bar.open_index, Some(1));
let Some(super::super::model::MenuEntry::Item(item)) = bar.popup.items.first() else {
panic!("popup should contain Edit items after the tap");
};
assert_eq!(item.action.as_deref(), Some("edit.copy"));
}
#[test]
fn unconsumed_shortcut_fires_top_menu_action() {
let actions = Rc::new(RefCell::new(Vec::new()));
let actions_for_cb = Rc::clone(&actions);
let mut bar = MenuBar::new(
test_font(),
vec![TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new")
.shortcut("Ctrl+N")
.into()],
)],
move |action| actions_for_cb.borrow_mut().push(action.to_string()),
);
assert_eq!(
bar.on_unconsumed_key(
&Key::Char('n'),
Modifiers {
ctrl: true,
..Modifiers::default()
},
),
EventResult::Consumed
);
assert_eq!(actions.borrow().as_slice(), ["file.new"]);
}
#[test]
fn arrow_keys_switch_open_top_menus() {
let mut bar = MenuBar::new(
test_font(),
vec![
TopMenu::new(
"File",
vec![MenuItem::action("New", "file.new").into()],
),
TopMenu::new(
"Edit",
vec![MenuItem::action("Copy", "edit.copy").into()],
),
],
|_| {},
);
bar.layout(Size::new(300.0, BAR_H));
bar.open_menu(0);
assert_eq!(
bar.on_event(&Event::KeyDown {
key: Key::ArrowRight,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert_eq!(bar.open_index, Some(1));
assert_eq!(
bar.on_event(&Event::KeyDown {
key: Key::ArrowLeft,
modifiers: Modifiers::default(),
}),
EventResult::Consumed
);
assert_eq!(bar.open_index, Some(0));
}