Recipe: menus
Two kinds of menu come up in most games: a main menu (a column of big buttons) and, in tools and editors, a menu bar with dropdowns.
A main menu
Put the buttons in a panel with a vertical layout. Because the parent is a VBox, the Up and Down arrows already move focus between the buttons (see spatial navigation). Focus the first button so the keyboard works from the first frame, and give Escape a global bind.
ts
import { Button, DEFAULT_FOCUS_MANAGER, DEFAULT_STAGE, Insets, LAYOUT_VBOX, Label, MOD_NONE, Panel, Screen, key } from "willowui";
const screen: Screen = new Screen();
const menu: Panel = new Panel("main-menu");
menu.setLayout(LAYOUT_VBOX);
menu.setSpacing(10.0);
menu.padding = new Insets(20.0, 20.0, 20.0, 20.0);
menu.setPosition(280.0, 140.0);
menu.setSize(240.0, 260.0);
screen.add(menu);
const status: Label = new Label("status", "", "default", 14.0);
status.setPosition(280.0, 420.0);
screen.add(status);
function entry(name: string, text: string, action: Function): Button {
const b: Button = new Button(name, text, "default", 18.0);
b.setSize(200.0, 44.0);
b.setOnClick((): void => { action(); });
menu.addChild(b);
return b;
}
const play: Button = entry("play", "Play", (): void => { status.setText("Starting..."); });
entry("continue", "Continue", (): void => { status.setText("Loading the last save"); }).setEnabled(false);
entry("settings", "Settings", (): void => { status.setText("Opening settings"); });
entry("quit", "Quit", (): void => { System.exit(0); });
menu.updateLayout();
DEFAULT_FOCUS_MANAGER.setFocus(play);
DEFAULT_FOCUS_MANAGER.bind(key(Key.ESCAPE, MOD_NONE), (): boolean => {
status.setText("Press Quit to leave");
return true;
});
DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(screen);
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });Notes:
- The disabled Continue button is skipped by the arrows and by Tab, so there is no dead stop.
- The theme's
focusColordraws the focus ring. See Theming.
A menu bar
MenuBar takes a list of MenuBarEntry(label, items). Each item is a MenuItem(label, onSelect). An item can also be:
| Field | Effect |
|---|---|
disabled = true | Shown greyed out; cannot be picked. |
separator = true | A thin divider. The label and function are ignored. |
shortcut = "Ctrl+S" | A hint drawn on the right. It is display only: bind the key yourself with DEFAULT_FOCUS_MANAGER.bind. |
ts
import { DEFAULT_STAGE, Label, MenuBar, MenuBarEntry, MenuItem, Screen } from "willowui";
const screen: Screen = new Screen();
const status: Label = new Label("status", "Pick something from the menu", "default", 14.0);
status.setPosition(20.0, 60.0);
screen.add(status);
function item(label: string, shortcut: string): MenuItem {
const m: MenuItem = new MenuItem(label, (): void => { status.setText("Picked " + label); });
m.shortcut = shortcut;
return m;
}
function separator(): MenuItem {
const m: MenuItem = new MenuItem("", null);
m.separator = true;
return m;
}
const exportItem: MenuItem = item("Export", "");
exportItem.disabled = true;
const bar: MenuBar = new MenuBar("menu-bar", "default", 14.0);
bar.setSize(800.0, 32.0);
bar.setEntries([
new MenuBarEntry("File", [item("New map", "Ctrl+N"), item("Open", "Ctrl+O"), separator(), exportItem]),
new MenuBarEntry("Edit", [item("Undo", "Ctrl+Z"), item("Redo", "Ctrl+Y")]),
]);
screen.add(bar);
DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(screen);
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });Once a dropdown is open, the arrow keys move through it and Escape closes it. setOnMenuOpen(fn) gets the entry index; setOnMenuClose(fn) fires when it closes.
A right-click menu
ContextMenu(family, size) is the same list as a popup at any point: setItems([...]), then showAt(x, y) from a click handler, and hide() to close it.
See also
- Input and focus for how the arrows and key binds work.
- Recipe: dialogs for a "Quit the game?" confirmation.