use denise::{
BufferAge, ElementState, Frame, InputEvent, KeyCode, Modifiers, PixelFormat, Point, Rect, Size,
theme,
};
use denise_ui::widgets::{Menu, MenuBar, MenuEvent, MenuItem, open_menu, title_layout};
use denise_ui::{NodeId, Ui};
const SIZE: Size = Size::new(640, 400);
const BAR: Rect = Rect::new(0, 0, 640, 28);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Msg {
Open(usize),
Menu(MenuEvent),
}
fn file_menu() -> Vec<MenuItem> {
vec![
MenuItem::new("Open…").with_shortcut("Cmd+O"),
MenuItem::separator(),
MenuItem::submenu(
"Recent",
[
MenuItem::new("a.log"),
MenuItem::new("b.log"),
MenuItem::separator(),
MenuItem::new("Clear Recent"),
],
),
MenuItem::separator(),
MenuItem::new("Quit").with_shortcut("Cmd+Q"),
]
}
struct Fixture {
ui: Ui<Msg>,
bar: NodeId,
menu: NodeId,
}
impl Fixture {
fn open() -> Self {
let mut ui: Ui<Msg> = Ui::new(SIZE, theme::DARK);
let root = ui.root();
let bar = ui
.add(root, MenuBar::new(["File", "Edit", "View"], Msg::Open), BAR)
.expect("bar");
ui.show_cursor(false);
ui.tick(0);
let menu = open_menu(&mut ui, bar, 0, &file_menu(), Msg::Menu).expect("menu");
ui.drain_messages();
Self { ui, bar, menu }
}
fn menu(&self) -> &Menu<Msg> {
self.ui.widget::<Menu<Msg>>(self.menu).expect("menu")
}
fn panels(&self) -> Vec<Rect> {
self.menu().panels().collect()
}
fn centre(&self, panel: usize, row: usize) -> Point {
let r = self.menu().row_rect(panel, row).expect("row");
Point::new(r.x + r.width / 2, r.y + r.height / 2)
}
fn move_to(&mut self, p: Point) {
self.ui.handle(&[InputEvent::PointerMoved { position: p }]);
}
fn click(&mut self, p: Point) {
self.move_to(p);
self.ui.handle(&[
InputEvent::PointerButton {
button: denise::PointerButton::Left,
state: ElementState::Down,
position: p,
modifiers: Modifiers::default(),
},
InputEvent::PointerButton {
button: denise::PointerButton::Left,
state: ElementState::Up,
position: p,
modifiers: Modifiers::default(),
},
]);
}
fn key(&mut self, code: KeyCode) {
self.ui.handle(&[InputEvent::Key {
code,
state: ElementState::Down,
repeat: false,
modifiers: Modifiers::default(),
}]);
}
fn messages(&mut self) -> Vec<Msg> {
self.ui.drain_messages().collect()
}
fn title(&mut self, index: usize) -> Rect {
let titles: Vec<String> = self
.ui
.widget::<MenuBar<Msg>>(self.bar)
.expect("bar")
.titles()
.to_vec();
let style = self
.ui
.widget::<MenuBar<Msg>>(self.bar)
.expect("bar")
.style();
title_layout(&titles, BAR, style, self.ui.text_mut())[index]
}
fn lit(&mut self) -> Vec<(usize, usize)> {
let mut pixels = vec![0u32; (SIZE.width * SIZE.height) as usize];
{
let mut frame = Frame::new(
&mut pixels,
SIZE,
SIZE.width,
PixelFormat::Xrgb8888,
BufferAge::Undefined,
)
.expect("frame");
self.ui.paint(&mut frame);
}
let at = |x: i32, y: i32| pixels[(y as u32 * SIZE.width + x as u32) as usize] & 0x00FF_FFFF;
let mut lit = Vec::new();
for (k, panel) in self.panels().into_iter().enumerate() {
let ground = at(panel.x + panel.width / 2, panel.y + 2);
let mut row = 0;
while let Some(r) = self.menu().row_rect(k, row) {
if at(r.right() - 3, r.y + r.height / 2) != ground {
lit.push((k, row));
}
row += 1;
}
}
lit
}
}
#[test]
fn a_pointer_that_leaves_the_menu_leaves_no_row_lit_for_it() {
let mut f = Fixture::open();
f.move_to(f.centre(0, 0));
assert_eq!(f.lit(), vec![(0, 0)]);
let panel = f.panels()[0];
f.move_to(Point::new(panel.x + panel.width / 2, panel.bottom() + 40));
assert!(f.lit().is_empty(), "nothing under the pointer, nothing lit");
let mut f = Fixture::open();
f.move_to(f.centre(0, 2));
f.move_to(f.centre(0, 4));
assert_eq!(f.lit(), vec![(0, 2), (0, 4)]);
let panel = f.panels()[0];
f.move_to(Point::new(panel.x + panel.width / 2, panel.bottom() + 40));
assert_eq!(f.lit(), vec![(0, 2)], "the row the pointer crossed is not");
f.ui.tick(500);
assert_eq!(f.panels().len(), 1, "the submenu still closed");
assert!(f.lit().is_empty());
}
#[test]
fn inside_a_submenu_the_panel_it_opened_from_lights_only_the_row_that_opened_it() {
let mut f = Fixture::open();
f.move_to(f.centre(0, 2));
f.move_to(f.centre(0, 4)); f.move_to(f.centre(1, 0));
assert_eq!(f.lit(), vec![(0, 2), (1, 0)]);
}
#[test]
fn a_pointer_that_leaves_the_window_leaves_no_row_lit() {
let mut f = Fixture::open();
f.move_to(f.centre(0, 0));
assert_eq!(f.lit(), vec![(0, 0)]);
f.ui.handle(&[InputEvent::PointerLeft]);
assert!(
f.lit().is_empty(),
"the pointer is not over the menu any more"
);
f.move_to(f.centre(0, 4));
assert_eq!(f.lit(), vec![(0, 4)], "and lights the row it comes back to");
}
#[test]
fn the_root_hangs_under_its_title_and_a_release_on_a_row_chooses_it() {
let mut f = Fixture::open();
let panels = f.panels();
assert_eq!(panels.len(), 1);
let title = f.title(0);
assert_eq!(panels[0].x, title.x, "aligned with the title");
assert_eq!(panels[0].y, BAR.bottom(), "directly under the bar");
let quit = f.centre(0, 4);
f.click(quit);
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Picked(8))]);
}
#[test]
fn a_submenu_opens_beside_the_row_the_pointer_rests_on_and_its_rows_count_after_it() {
let mut f = Fixture::open();
f.move_to(f.centre(0, 2));
let panels = f.panels();
assert_eq!(panels.len(), 2, "the submenu opened");
assert!(panels[1].x > panels[0].x, "beside the root, to the right");
let recent = f.menu().row_rect(0, 2).expect("recent");
let first = f.menu().row_rect(1, 0).expect("first of the submenu");
assert_eq!(
first.y, recent.y,
"its first row level with the row that opened it"
);
let b = f.centre(1, 1);
f.click(b);
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Picked(4))]);
let clear = f.centre(1, 3);
f.click(clear);
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Picked(6))]);
}
#[test]
fn a_submenu_closes_once_the_pointer_has_rested_on_another_row() {
let mut f = Fixture::open();
f.move_to(f.centre(0, 2));
assert_eq!(f.panels().len(), 2);
f.move_to(f.centre(0, 4));
f.ui.tick(60);
assert_eq!(
f.panels().len(),
2,
"not yet: the pointer may be passing through"
);
f.move_to(f.centre(1, 0));
f.ui.tick(400);
assert_eq!(f.panels().len(), 2, "the pointer made it into the submenu");
f.move_to(f.centre(0, 4));
f.ui.tick(500);
assert_eq!(f.panels().len(), 2);
f.ui.tick(700);
assert_eq!(
f.panels().len(),
1,
"closed after the pointer rested elsewhere"
);
}
#[test]
fn a_release_over_a_separator_or_a_submenu_row_chooses_nothing() {
let mut f = Fixture::open();
f.click(f.centre(0, 1));
assert_eq!(f.messages(), vec![]);
f.click(f.centre(0, 2));
assert_eq!(
f.messages(),
vec![],
"a submenu row opens rather than picks"
);
assert_eq!(f.panels().len(), 2);
}
#[test]
fn a_press_outside_dismisses_and_a_press_on_the_open_title_does_too() {
let mut f = Fixture::open();
f.click(Point::new(600, 380));
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Dismissed)]);
let mut f = Fixture::open();
let title = f.title(0);
f.click(Point::new(title.x + 2, title.y + 2));
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Dismissed)]);
}
#[test]
fn sliding_along_the_bar_reports_the_title_once() {
let mut f = Fixture::open();
let edit = f.title(1);
f.move_to(Point::new(edit.x + 3, edit.y + 3));
f.move_to(Point::new(edit.x + 6, edit.y + 4));
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Title(1))]);
let file = f.title(0);
f.move_to(Point::new(file.x + 3, file.y + 3));
let view = f.title(2);
f.move_to(Point::new(view.x + 3, view.y + 3));
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Title(2))]);
}
#[test]
fn the_keyboard_walks_past_rules_opens_submenus_and_chooses() {
let mut f = Fixture::open();
f.key(KeyCode::ArrowDown); f.key(KeyCode::ArrowDown); f.key(KeyCode::ArrowRight);
assert_eq!(f.panels().len(), 2, "Right opened the submenu");
f.key(KeyCode::ArrowDown); f.key(KeyCode::Enter);
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Picked(4))]);
f.key(KeyCode::ArrowLeft);
assert_eq!(f.panels().len(), 1, "Left closed it");
f.key(KeyCode::ArrowLeft);
assert_eq!(
f.messages(),
vec![Msg::Menu(MenuEvent::Title(2))],
"Left at the root goes to the previous title, wrapping"
);
f.key(KeyCode::ArrowDown); f.key(KeyCode::Enter);
assert_eq!(f.messages(), vec![Msg::Menu(MenuEvent::Picked(8))]);
}