683 lines
16 KiB
Text
683 lines
16 KiB
Text
---
|
|
title: Application Menus
|
|
description: Create native menu bars for your desktop application
|
|
sidebar:
|
|
order: 1
|
|
---
|
|
|
|
import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components";
|
|
|
|
## The Problem
|
|
|
|
Professional desktop applications need menu bars—File, Edit, View, Help. But menus work differently on each platform:
|
|
- **macOS**: Global menu bar at top of screen
|
|
- **Windows**: Menu bar in window title bar
|
|
- **Linux**: Varies by desktop environment
|
|
|
|
Building platform-appropriate menus manually is tedious and error-prone.
|
|
|
|
## The Wails Solution
|
|
|
|
Wails provides a **unified API** that creates platform-native menus automatically. Write once, get native behaviour on all platforms.
|
|
|
|
{/* VISUAL PLACEHOLDER: Menu Bar Comparison
|
|
Description: Three screenshots side-by-side showing the same Wails menu on:
|
|
1. macOS - Global menu bar at top of screen with app name
|
|
2. Windows - Menu bar in window title bar
|
|
3. Linux (GNOME) - Menu bar in window
|
|
All showing identical menu structure: File, Edit, View, Tools, Help
|
|
Style: Clean screenshots with subtle borders, labels indicating platform
|
|
*/}
|
|
|
|
## Quick Start
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"runtime"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
func main() {
|
|
app := application.New(application.Options{
|
|
Name: "My App",
|
|
})
|
|
|
|
// Create menu
|
|
menu := app.NewMenu()
|
|
|
|
// Add standard menus (platform-appropriate)
|
|
if runtime.GOOS == "darwin" {
|
|
menu.AddRole(application.AppMenu) // macOS only
|
|
}
|
|
menu.AddRole(application.FileMenu)
|
|
menu.AddRole(application.EditMenu)
|
|
menu.AddRole(application.WindowMenu)
|
|
menu.AddRole(application.HelpMenu)
|
|
|
|
// Set the application menu
|
|
app.Menu.Set(menu)
|
|
|
|
// Create window with UseApplicationMenu to inherit the menu on Windows/Linux
|
|
app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
UseApplicationMenu: true,
|
|
})
|
|
|
|
app.Run()
|
|
}
|
|
```
|
|
|
|
**That's it!** You now have platform-native menus with standard items. The `UseApplicationMenu` option ensures Windows and Linux windows display the menu without additional code.
|
|
|
|
## Creating Menus
|
|
|
|
### Basic Menu Creation
|
|
|
|
```go
|
|
// Create a new menu
|
|
menu := app.NewMenu()
|
|
|
|
// Add a top-level menu
|
|
fileMenu := menu.AddSubmenu("File")
|
|
|
|
// Add menu items
|
|
fileMenu.Add("New").OnClick(func(ctx *application.Context) {
|
|
// Handle New
|
|
})
|
|
|
|
fileMenu.Add("Open").OnClick(func(ctx *application.Context) {
|
|
// Handle Open
|
|
})
|
|
|
|
fileMenu.AddSeparator()
|
|
|
|
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
|
|
app.Quit()
|
|
})
|
|
```
|
|
|
|
### Setting the Menu
|
|
|
|
**Recommended approach** — Use `UseApplicationMenu` for cross-platform consistency:
|
|
|
|
```go
|
|
// Set the application menu once
|
|
app.Menu.Set(menu)
|
|
|
|
// Create windows that inherit the menu on Windows/Linux
|
|
app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
UseApplicationMenu: true, // Window uses the app menu
|
|
})
|
|
```
|
|
|
|
This approach:
|
|
- On **macOS**: The menu appears at the top of screen (standard behaviour)
|
|
- On **Windows/Linux**: Each window with `UseApplicationMenu: true` displays the app menu
|
|
|
|
**Platform-specific details:**
|
|
|
|
<Tabs syncKey="platform">
|
|
<TabItem label="macOS" icon="apple">
|
|
**Global menu bar** (one per application):
|
|
|
|
```go
|
|
app.Menu.Set(menu)
|
|
```
|
|
|
|
The menu appears at the top of the screen and persists even when all windows are closed. The `UseApplicationMenu` option has no effect on macOS since all apps use the global menu.
|
|
</TabItem>
|
|
|
|
<TabItem label="Windows" icon="seti:windows">
|
|
**Per-window menu bar**:
|
|
|
|
```go
|
|
// Option 1: Use application menu (recommended)
|
|
app.Menu.Set(menu)
|
|
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
UseApplicationMenu: true,
|
|
})
|
|
|
|
// Option 2: Set menu directly on window
|
|
window.SetMenu(menu)
|
|
```
|
|
|
|
Each window can have its own menu, or inherit the application menu. The menu appears in the window's title bar.
|
|
</TabItem>
|
|
|
|
<TabItem label="Linux" icon="linux">
|
|
**Per-window menu bar** (usually):
|
|
|
|
```go
|
|
// Option 1: Use application menu (recommended)
|
|
app.Menu.Set(menu)
|
|
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
UseApplicationMenu: true,
|
|
})
|
|
|
|
// Option 2: Set menu directly on window
|
|
window.SetMenu(menu)
|
|
```
|
|
|
|
Behaviour varies by desktop environment. Some (like Unity) support global menus.
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
:::tip[Simplify Cross-Platform Menus]
|
|
Using `UseApplicationMenu: true` eliminates the need for platform-specific code like:
|
|
```go
|
|
// Old approach - no longer needed
|
|
if runtime.GOOS == "darwin" {
|
|
app.Menu.Set(menu)
|
|
} else {
|
|
window.SetMenu(menu)
|
|
}
|
|
```
|
|
:::
|
|
|
|
**Per-window custom menus:**
|
|
|
|
If a window needs a different menu than the application menu, set it directly:
|
|
|
|
```go
|
|
window.SetMenu(customMenu) // Overrides UseApplicationMenu
|
|
```
|
|
|
|
## Menu Roles
|
|
|
|
Wails provides **predefined menu roles** that create platform-appropriate menu structures automatically.
|
|
|
|
### Available Roles
|
|
|
|
| Role | Description | Platform Notes |
|
|
|------|-------------|----------------|
|
|
| `AppMenu` | Application menu with About, Preferences, Quit | **macOS only** |
|
|
| `FileMenu` | File operations (New, Open, Save, etc.) | All platforms |
|
|
| `EditMenu` | Text editing (Undo, Redo, Cut, Copy, Paste) | All platforms |
|
|
| `WindowMenu` | Window management (Minimise, Zoom, etc.) | All platforms |
|
|
| `HelpMenu` | Help and information | All platforms |
|
|
|
|
### Using Roles
|
|
|
|
```go
|
|
menu := app.NewMenu()
|
|
|
|
// macOS: Add application menu
|
|
if runtime.GOOS == "darwin" {
|
|
menu.AddRole(application.AppMenu)
|
|
}
|
|
|
|
// All platforms: Add standard menus
|
|
menu.AddRole(application.FileMenu)
|
|
menu.AddRole(application.EditMenu)
|
|
menu.AddRole(application.WindowMenu)
|
|
menu.AddRole(application.HelpMenu)
|
|
```
|
|
|
|
**What you get:**
|
|
|
|
<Tabs syncKey="platform">
|
|
<TabItem label="macOS" icon="apple">
|
|
**AppMenu** (with app name):
|
|
- About [App Name]
|
|
- Preferences... (⌘,)
|
|
- ---
|
|
- Services
|
|
- ---
|
|
- Hide [App Name] (⌘H)
|
|
- Hide Others (⌥⌘H)
|
|
- Show All
|
|
- ---
|
|
- Quit [App Name] (⌘Q)
|
|
|
|
**FileMenu**:
|
|
- New (⌘N)
|
|
- Open... (⌘O)
|
|
- ---
|
|
- Close Window (⌘W)
|
|
|
|
**EditMenu**:
|
|
- Undo (⌘Z)
|
|
- Redo (⇧⌘Z)
|
|
- ---
|
|
- Cut (⌘X)
|
|
- Copy (⌘C)
|
|
- Paste (⌘V)
|
|
- Select All (⌘A)
|
|
|
|
**WindowMenu**:
|
|
- Minimise (⌘M)
|
|
- Zoom
|
|
- ---
|
|
- Bring All to Front
|
|
|
|
**HelpMenu**:
|
|
- [App Name] Help
|
|
</TabItem>
|
|
|
|
<TabItem label="Windows" icon="seti:windows">
|
|
**FileMenu**:
|
|
- New (Ctrl+N)
|
|
- Open... (Ctrl+O)
|
|
- ---
|
|
- Exit (Alt+F4)
|
|
|
|
**EditMenu**:
|
|
- Undo (Ctrl+Z)
|
|
- Redo (Ctrl+Y)
|
|
- ---
|
|
- Cut (Ctrl+X)
|
|
- Copy (Ctrl+C)
|
|
- Paste (Ctrl+V)
|
|
- Select All (Ctrl+A)
|
|
|
|
**WindowMenu**:
|
|
- Minimise
|
|
- Maximise
|
|
|
|
**HelpMenu**:
|
|
- About [App Name]
|
|
</TabItem>
|
|
|
|
<TabItem label="Linux" icon="linux">
|
|
Similar to Windows, but keyboard shortcuts may vary by desktop environment.
|
|
</TabItem>
|
|
</Tabs>
|
|
|
|
### Customising Role Menus
|
|
|
|
Add items to role menus:
|
|
|
|
```go
|
|
fileMenu := menu.AddRole(application.FileMenu)
|
|
|
|
// Add custom items
|
|
fileMenu.Add("Import...").OnClick(handleImport)
|
|
fileMenu.Add("Export...").OnClick(handleExport)
|
|
```
|
|
|
|
## Custom Menus
|
|
|
|
Create your own menus for application-specific features:
|
|
|
|
```go
|
|
// Add a custom top-level menu
|
|
toolsMenu := menu.AddSubmenu("Tools")
|
|
|
|
// Add items
|
|
toolsMenu.Add("Settings").OnClick(func(ctx *application.Context) {
|
|
showSettingsWindow()
|
|
})
|
|
|
|
toolsMenu.AddSeparator()
|
|
|
|
// Add checkbox
|
|
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(func(ctx *application.Context) {
|
|
isDark := ctx.ClickedMenuItem().Checked()
|
|
setTheme(isDark)
|
|
})
|
|
|
|
// Add radio group
|
|
toolsMenu.AddRadio("Small", true).OnClick(handleFontSize)
|
|
toolsMenu.AddRadio("Medium", false).OnClick(handleFontSize)
|
|
toolsMenu.AddRadio("Large", false).OnClick(handleFontSize)
|
|
|
|
// Add submenu
|
|
advancedMenu := toolsMenu.AddSubmenu("Advanced")
|
|
advancedMenu.Add("Configure...").OnClick(showAdvancedSettings)
|
|
```
|
|
|
|
**For more menu item types**, see [Menu Reference](/features/menus/reference).
|
|
|
|
## Dynamic Menus
|
|
|
|
Update menus based on application state:
|
|
|
|
### Enable/Disable Items
|
|
|
|
```go
|
|
var saveMenuItem *application.MenuItem
|
|
|
|
func createMenu() {
|
|
menu := app.NewMenu()
|
|
fileMenu := menu.AddSubmenu("File")
|
|
|
|
saveMenuItem = fileMenu.Add("Save")
|
|
saveMenuItem.SetEnabled(false) // Initially disabled
|
|
saveMenuItem.OnClick(handleSave)
|
|
|
|
app.SetMenu(menu)
|
|
}
|
|
|
|
func onDocumentChanged() {
|
|
saveMenuItem.SetEnabled(hasUnsavedChanges())
|
|
menu.Update() // Important!
|
|
}
|
|
```
|
|
|
|
:::caution[Always Call menu.Update()]
|
|
After changing menu state (enable/disable, label, checked), **always call `menu.Update()`**. This is especially critical on Windows where menus are reconstructed.
|
|
|
|
See [Menu Reference](/features/menus/reference#enabled-state) for details.
|
|
:::
|
|
|
|
### Change Labels
|
|
|
|
```go
|
|
updateMenuItem := menu.Add("Check for Updates")
|
|
|
|
updateMenuItem.OnClick(func(ctx *application.Context) {
|
|
updateMenuItem.SetLabel("Checking...")
|
|
menu.Update()
|
|
|
|
checkForUpdates()
|
|
|
|
updateMenuItem.SetLabel("Check for Updates")
|
|
menu.Update()
|
|
})
|
|
```
|
|
|
|
### Rebuild Menus
|
|
|
|
For major changes, rebuild the entire menu:
|
|
|
|
```go
|
|
func rebuildFileMenu() {
|
|
menu := app.NewMenu()
|
|
fileMenu := menu.AddSubmenu("File")
|
|
|
|
fileMenu.Add("New").OnClick(handleNew)
|
|
fileMenu.Add("Open").OnClick(handleOpen)
|
|
|
|
// Add recent files dynamically
|
|
if hasRecentFiles() {
|
|
recentMenu := fileMenu.AddSubmenu("Open Recent")
|
|
for _, file := range getRecentFiles() {
|
|
filePath := file // Capture for closure
|
|
recentMenu.Add(filepath.Base(file)).OnClick(func(ctx *application.Context) {
|
|
openFile(filePath)
|
|
})
|
|
}
|
|
recentMenu.AddSeparator()
|
|
recentMenu.Add("Clear Recent").OnClick(clearRecentFiles)
|
|
}
|
|
|
|
fileMenu.AddSeparator()
|
|
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
|
|
app.Quit()
|
|
})
|
|
|
|
app.SetMenu(menu)
|
|
}
|
|
```
|
|
|
|
## Window Control from Menus
|
|
|
|
Menu items can control windows:
|
|
|
|
```go
|
|
viewMenu := menu.AddSubmenu("View")
|
|
|
|
// Toggle fullscreen
|
|
viewMenu.Add("Toggle Fullscreen").OnClick(func(ctx *application.Context) {
|
|
window := app.GetWindowByName("main")
|
|
window.SetFullscreen(!window.IsFullscreen())
|
|
})
|
|
|
|
// Zoom controls
|
|
viewMenu.Add("Zoom In").SetAccelerator("CmdOrCtrl++").OnClick(func(ctx *application.Context) {
|
|
// Increase zoom
|
|
})
|
|
|
|
viewMenu.Add("Zoom Out").SetAccelerator("CmdOrCtrl+-").OnClick(func(ctx *application.Context) {
|
|
// Decrease zoom
|
|
})
|
|
|
|
viewMenu.Add("Reset Zoom").SetAccelerator("CmdOrCtrl+0").OnClick(func(ctx *application.Context) {
|
|
// Reset zoom
|
|
})
|
|
```
|
|
|
|
**Get the active window:**
|
|
|
|
```go
|
|
menuItem.OnClick(func(ctx *application.Context) {
|
|
window := application.ContextWindow(ctx)
|
|
// Use window
|
|
})
|
|
```
|
|
|
|
## Platform-Specific Considerations
|
|
|
|
### macOS
|
|
|
|
**Menu bar behaviour:**
|
|
- Appears at **top of screen** (global)
|
|
- Persists when all windows closed
|
|
- First menu is **always the application menu**
|
|
- Use `menu.AddRole(application.AppMenu)` for standard items
|
|
|
|
**Standard locations:**
|
|
- **About**: Application menu
|
|
- **Preferences**: Application menu (⌘,)
|
|
- **Quit**: Application menu (⌘Q)
|
|
- **Help**: Help menu
|
|
|
|
**Example:**
|
|
|
|
```go
|
|
if runtime.GOOS == "darwin" {
|
|
menu.AddRole(application.AppMenu) // Adds About, Preferences, Quit
|
|
|
|
// Don't add Quit to File menu on macOS
|
|
// Don't add About to Help menu on macOS
|
|
}
|
|
```
|
|
|
|
### Windows
|
|
|
|
**Menu bar behaviour:**
|
|
- Appears in **window title bar**
|
|
- Each window has its own menu
|
|
- No application menu
|
|
|
|
**Standard locations:**
|
|
- **Exit**: File menu (Alt+F4)
|
|
- **Settings**: Tools or Edit menu
|
|
- **About**: Help menu
|
|
|
|
**Example:**
|
|
|
|
```go
|
|
if runtime.GOOS == "windows" {
|
|
fileMenu := menu.AddRole(application.FileMenu)
|
|
// Exit is added automatically
|
|
|
|
helpMenu := menu.AddRole(application.HelpMenu)
|
|
// About is added automatically
|
|
}
|
|
```
|
|
|
|
### Linux
|
|
|
|
**Menu bar behaviour:**
|
|
- Usually per-window (like Windows)
|
|
- Some DEs support global menus (Unity, GNOME with extension)
|
|
- Appearance varies by desktop environment
|
|
|
|
**Best practice:** Follow Windows conventions, test on target DEs.
|
|
|
|
## Complete Example
|
|
|
|
Here's a production-ready menu structure:
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"runtime"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
func main() {
|
|
app := application.New(application.Options{
|
|
Name: "My Application",
|
|
})
|
|
|
|
// Create and set menu
|
|
createMenu(app)
|
|
|
|
// Create main window with UseApplicationMenu for cross-platform menu support
|
|
app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
UseApplicationMenu: true,
|
|
})
|
|
|
|
app.Run()
|
|
}
|
|
|
|
func createMenu(app *application.Application) {
|
|
menu := app.NewMenu()
|
|
|
|
// Platform-specific application menu (macOS only)
|
|
if runtime.GOOS == "darwin" {
|
|
menu.AddRole(application.AppMenu)
|
|
}
|
|
|
|
// File menu
|
|
fileMenu := menu.AddRole(application.FileMenu)
|
|
fileMenu.Add("Import...").SetAccelerator("CmdOrCtrl+I").OnClick(handleImport)
|
|
fileMenu.Add("Export...").SetAccelerator("CmdOrCtrl+E").OnClick(handleExport)
|
|
|
|
// Edit menu
|
|
menu.AddRole(application.EditMenu)
|
|
|
|
// View menu
|
|
viewMenu := menu.AddSubmenu("View")
|
|
viewMenu.Add("Toggle Fullscreen").SetAccelerator("F11").OnClick(toggleFullscreen)
|
|
viewMenu.AddSeparator()
|
|
viewMenu.AddCheckbox("Show Sidebar", true).OnClick(toggleSidebar)
|
|
viewMenu.AddCheckbox("Show Toolbar", true).OnClick(toggleToolbar)
|
|
|
|
// Tools menu
|
|
toolsMenu := menu.AddSubmenu("Tools")
|
|
|
|
// Settings location varies by platform
|
|
if runtime.GOOS == "darwin" {
|
|
// On macOS, Preferences is in Application menu (added by AppMenu role)
|
|
} else {
|
|
toolsMenu.Add("Settings").SetAccelerator("CmdOrCtrl+,").OnClick(showSettings)
|
|
}
|
|
|
|
toolsMenu.AddSeparator()
|
|
toolsMenu.AddCheckbox("Dark Mode", false).OnClick(toggleDarkMode)
|
|
|
|
// Window menu
|
|
menu.AddRole(application.WindowMenu)
|
|
|
|
// Help menu
|
|
helpMenu := menu.AddRole(application.HelpMenu)
|
|
helpMenu.Add("Documentation").OnClick(openDocumentation)
|
|
|
|
// About location varies by platform
|
|
if runtime.GOOS == "darwin" {
|
|
// On macOS, About is in Application menu (added by AppMenu role)
|
|
} else {
|
|
helpMenu.AddSeparator()
|
|
helpMenu.Add("About").OnClick(showAbout)
|
|
}
|
|
|
|
// Set the application menu
|
|
app.Menu.Set(menu)
|
|
}
|
|
|
|
func handleImport(ctx *application.Context) {
|
|
// Implementation
|
|
}
|
|
|
|
func handleExport(ctx *application.Context) {
|
|
// Implementation
|
|
}
|
|
|
|
func toggleFullscreen(ctx *application.Context) {
|
|
window := application.ContextWindow(ctx)
|
|
window.SetFullscreen(!window.IsFullscreen())
|
|
}
|
|
|
|
func toggleSidebar(ctx *application.Context) {
|
|
// Implementation
|
|
}
|
|
|
|
func toggleToolbar(ctx *application.Context) {
|
|
// Implementation
|
|
}
|
|
|
|
func showSettings(ctx *application.Context) {
|
|
// Implementation
|
|
}
|
|
|
|
func toggleDarkMode(ctx *application.Context) {
|
|
isDark := ctx.ClickedMenuItem().Checked()
|
|
// Apply theme
|
|
}
|
|
|
|
func openDocumentation(ctx *application.Context) {
|
|
// Open browser
|
|
}
|
|
|
|
func showAbout(ctx *application.Context) {
|
|
// Show about dialog
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ Do
|
|
|
|
- **Use menu roles** for standard menus (File, Edit, etc.)
|
|
- **Follow platform conventions** for menu structure
|
|
- **Add keyboard shortcuts** to common actions
|
|
- **Call menu.Update()** after changing menu state
|
|
- **Test on all platforms** - behaviour varies
|
|
- **Keep menus shallow** - 2-3 levels maximum
|
|
- **Use clear labels** - "Save Project" not "Save"
|
|
|
|
### ❌ Don't
|
|
|
|
- **Don't hardcode platform shortcuts** - Use `CmdOrCtrl`
|
|
- **Don't put Quit in File menu on macOS** - It's in Application menu
|
|
- **Don't put About in Help menu on macOS** - It's in Application menu
|
|
- **Don't forget menu.Update()** - Menus won't work properly
|
|
- **Don't nest too deeply** - Users get lost
|
|
- **Don't use jargon** - Keep labels user-friendly
|
|
|
|
## Next Steps
|
|
|
|
<CardGrid>
|
|
<Card title="Menu Reference" icon="document">
|
|
Complete reference for menu item types and properties.
|
|
|
|
[Learn More →](/features/menus/reference)
|
|
</Card>
|
|
|
|
<Card title="Context Menus" icon="puzzle">
|
|
Create right-click context menus.
|
|
|
|
[Learn More →](/features/menus/context)
|
|
</Card>
|
|
|
|
<Card title="System Tray Menus" icon="star">
|
|
Add system tray/menu bar integration.
|
|
|
|
[Learn More →](/features/menus/systray)
|
|
</Card>
|
|
|
|
<Card title="Menu Patterns" icon="open-book">
|
|
Common menu patterns and best practices.
|
|
|
|
[Learn More →](/guides/patterns/menus)
|
|
</Card>
|
|
</CardGrid>
|
|
|
|
---
|
|
|
|
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [menu example](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/menu).
|