Skip to content

Application menus ​

Access the application's native menu bar through app.menus. Configure its structure before app.run; update retained item properties on the UI context. Native menus support macOS.

Manager API ​

MethodReturnsPurpose
register(MenuClass)CryKit::Menus::MenuInstall one reusable definition
register(MenuClass, OtherClass, ...)NilInstall definitions in order
create(id, text, role: nil) { |menu| ... }CryKit::Menus::MenuBuild a menu inline
[id] / [id]?Menu / Menu?Look up a menu; [] raises if missing
find_by(menu_id, action_id)Item?Look up an item, including nested items
each { |menu| ... }—Enumerate menus in registration order
size / empty?Int32 / BoolInspect the registry

Use the same operations in a CryKit::Menu definition, an inline menu, or a nested submenu. Each operation returns a CryKit::Menus::Item.

OperationPurpose
text(id, text, shortcut: nil, enabled: true)Add an application action
checkbox(id, text, checked: false, shortcut: nil, enabled: true)Add a toggle
radio(id, text, checked: false, shortcut: nil, enabled: true)Add a choice in an adjacent radio group
separatorDivide items and end an adjacent radio group
submenu(id, text, enabled: true) { |submenu| ... }Add nested items
command(:close)Add a native command with its standard label and shortcut
ReceiverMemberPurpose
menuid, text, roleRead identity and native role
menuitemsRead a snapshot of top-level items
menu[action_id] / [action_id]?Look up any nested item; [] raises if missing
menuon_click { |event| ... }Subscribe to action activation
menuon_event(:click) { |event| ... } / events { |event| ... }Subscribe through the common event API
itemtext, text=Read or change its label
itemenabled, enabled=Read or change its enabled state
itemchecked, checked=Read state; change it on checkbox/radio items
itemid, kind, shortcut, role, childrenInspect the item and a snapshot of its children

Changing item properties updates native items immediately and emits no click event. Setting checked = true on a radio item clears its adjacent peers. The registered structure is fixed; item insertion, context menus, and per-window menu selection are not exposed.

Define and compose ​

crystal
class ClockMenu
  include CryKit::Menu

  menu :clock, "Clock" do
    text :time_control, "Pause", shortcut: "CmdOrCtrl+Shift+P"
  end
end

app.menus.register(
  CryKit::Menus::AppMenu,
  CryKit::Menus::FileMenu,
  CryKit::Menus::EditMenu,
  ClockMenu,
  CryKit::Menus::WindowMenu,
  CryKit::Menus::HelpMenu,
)

For a small menu, use the builder directly:

crystal
app.menus.create(:tools, "Tools") do |menu|
  menu.checkbox(:show_seconds, "Show seconds", checked: true)
  menu.separator
  menu.radio(:local_time, "Local time", checked: true)
  menu.radio(:utc_time, "UTC")
  menu.submenu(:advanced, "Advanced") do |submenu|
    submenu.text(:reset_clock, "Reset clock", enabled: false)
  end
end
Definition ruleBehavior
Menu IDsUnique within the application
Action IDsUnique within the top-level menu, including nested submenus
shortcut:Key and modifiers, for example CmdOrCtrl+Shift+P; active application shortcuts, including while a WebKit input has focus
applicationThe concrete application injected into each registered definition
menuThis registration's retained menu, available before on_register
on_register / on_unregisterOptional private hooks to acquire and release subscriptions or resources

Construction and registration listeners stay attached to the menu until its release, including when registration occurs in a view callback. Custom-action definitions can also be registered with trays.

Shortcuts ​

Pass shortcut: "CmdOrCtrl+Shift+P" to text, checkbox, or radio. Join modifiers and a final key with +; names are case-insensitive.

ModifierAccepted names
CommandCmd, Command, Super
ControlCtrl, Control
OptionAlt, Option
ShiftShift
Platform command keyCmdOrCtrl, CommandOrControl (Command on macOS, Control elsewhere)
KeyAccepted names
CharacterA single character, such as P or 1; use Plus for +
Function keyF1–F35
ArrowUp, Down, Left, Right
NavigationHome, End, PageUp, PageDown
Return and whitespaceReturn / Enter, Tab, Space
Escape and deletionEscape / Esc, Backspace, Delete / ForwardDelete

Unknown keys or modifiers, repeated modifiers, and missing keys raise ArgumentError.

Standard menus and native roles ​

DefinitionIDNative role
CryKit::Menus::AppMenu:app_menu:app
CryKit::Menus::FileMenu:file_menu—
CryKit::Menus::EditMenu:edit_menu—
CryKit::Menus::WindowMenu:window_menu:window
CryKit::Menus::HelpMenu:help_menu:help

Replace a built-in in the registration list to customize it. Declare menu :window_menu, "Window", role: :window in a definition, or pass role: :window to inline create. Each role can belong to one menu and does not populate items. On macOS the application menu occupies the first native slot; custom-only menus retain a separate native application slot.

Native commands ​

Command IDsBehavior
:about, :servicesAbout panel and Services submenu
:hide, :hide_others, :show_all, :quitApplication visibility and termination
:closeClose the current window
:undo, :redo, :cut, :copy, :paste, :select_allNative editing
:minimize, :zoom, :bring_all_to_frontNative window management

command :close supplies native behavior; text :close, "Close" creates an application action to handle yourself. Native commands use AppKit's responder chain. Close and Quit preserve window/application close approval. Application New/Open/Save actions use text and ordinary menu subscriptions. Tray menus reject native roles and native commands.

Subscribe and update through the application ​

crystal
menu = app.menus[:clock]
unsubscribe = menu.on_click do |event|
  if event.action == :time_control
    app.state.clock.toggle_pause
    event.menu[:time_control].text = app.state.clock.paused? ? "Resume" : "Pause"
  end
end

This example assumes application state with a clock model and toggle_pause.

CryKit::Menus::Menu::Event memberValue
menuRetained menu that owns the action
type:click
actionActivated action's Symbol ID
click?Whether this is a click event

on_click, on_event(:click), and events use the same ordered event registry and return an idempotent unsubscribe function. Call unsubscribe.call to stop a listener. Events are observational: checkbox/radio state updates before listeners run; events cannot cancel that update.

Subscription lifetime and dispatch

Listeners run in registration order on the UI context. A failing listener is logged and does not prevent later listeners. Delivery is not filtered by window focus, mounted page, or subscriber count.

Subscriptions registered in a view's on_mount, UI callbacks, or job.notify blocks are removed on unmount. Other subscriptions remain until explicitly removed or the menu closes. Menus survive navigation and view replacement; native resources live from startup to shutdown. Live updates from background work must return through job.notify. Unsupported native platforms raise CryKit::Menus::UnsupportedError when installing menus.

Clock demonstration ​

The demo's Clock menu and tray menu share application clock state. Both update their Pause/Resume labels and remain active across navigation. The application publishes timestamps; mounted views subscribe while they are present. See application jobs and channels for those lifetimes.

Built with Crystal. Documented with VitePress.