Skip to content

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 focusColor draws 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:

FieldEffect
disabled = trueShown greyed out; cannot be picked.
separator = trueA 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 ​