Desktop
On macOS, Windows, Linux and BSD, Bellui draws its windows with the system’s window manager and adds a set of desktop features. This page uses two examples: one for menus, the tray, notifications and windows, and one for the keyboard.
The main menu
Window.SetMainMenu
sets the menu bar. Build it from
bellui.NewMainMenu,
bellui.NewMenu and
bellui.NewMenuItem.
newItem := bellui.NewMenuItem("New window", func() { openSecondWindow(a) })
newItem.Shortcut = &desktop.CustomShortcut{
KeyName: event.KeyN,
Modifier: event.KeyModifierShortcutDefault, // Cmd on macOS, Ctrl elsewhere
}
file := bellui.NewMenu("File", newItem)
help := bellui.NewMenu("Help",
bellui.NewMenuItem("About", func() {
dialog.ShowInformation("About", "A desktop example.", w)
}))
w.SetMainMenu(bellui.NewMainMenu(file, help))- On macOS the menu goes into the system menu bar at the top of the screen. Elsewhere it is drawn at the top of the window.
- A menu item’s
Shortcutis shown next to its label, and pressing the keys runs the item’s action while the window has focus.event.KeyModifierShortcutDefaultis Command on macOS and Control elsewhere. - Outside macOS, the first menu always ends with a Quit item. If you do not add one, Bellui adds it; a last item labelled “Quit” is treated as that item.
MenuItemalso hasChecked,Disabled,Icon,ChildMenufor sub-menus, andIsSeparator. After changing items, callRefresh()on the menu.
The system tray
The system tray (the menu bar extras on macOS, the notification area on Windows) can hold an icon for your app with its own menu.
// The tray is reached through the driver: in v1.0.0 the app value
// itself does not implement all of desktop.App.
if desk, ok := a.Driver().(desktop.App); ok {
desk.SetSystemTrayMenu(bellui.NewMenu("Tray",
bellui.NewMenuItem("Show window", w.Show),
))
desk.SetSystemTrayIcon(theme.HomeIcon())
desk.SetSystemTrayTooltip("Desktop example")
// Tapping the tray icon shows w, and closing w now only hides it.
desk.SetSystemTrayWindow(w)
}| Method | What it does |
|---|---|
SetSystemTrayMenu | Shows the tray icon with this menu. |
SetSystemTrayIcon | Sets the tray icon. Without it, the app icon is used. A themed resource adapts to light and dark menu bars where the system does that. |
SetSystemTrayTooltip | Sets the hover text, where the system shows one. |
SetSystemTrayWindow | Makes a left click on the icon show this window. It also makes closing the window hide it instead, so the app keeps running. |
SetSystemTrayOnTapped | Runs your own function on a left click instead. This and SetSystemTrayWindow share one slot; the last one set wins. |
Because closing the tray window only hides it, the app keeps running in the
tray. Give the tray menu an item that calls App.Quit so users can still leave.
desktop.App on a.Driver(), as above. The app value
returned by app.New does not implement the tooltip and tap methods, so
a.(desktop.App) fails.Notifications
notify := widget.NewButton("Send notification", func() {
bellui.SendNotification(bellui.NewNotification("Export finished", "3 files written"))
})
rich := widget.NewButton("Send notification with actions", func() {
bellui.SendNotificationWithOptions(&bellui.NotificationOptions{
Title: "Download ready",
Content: "report.pdf",
OnTapped: w.Show,
Actions: []bellui.NotificationAction{
{Label: "Open", Callback: func() { w.Show() }},
},
})
})bellui.SendNotification
shows a title and text in the system’s notification area.
bellui.SendNotificationWithOptions
adds a function to run when the user clicks the notification, action buttons,
and a per-notification icon. Your functions run on the UI thread, but only
while the app is still running.
Where actions or click handling are not available, the notification still
appears with its title and text. On macOS the rich form needs a packaged app
(a .app bundle with an app ID); a program started with go run shows plain
notifications.
More than one window
func openSecondWindow(a bellui.App) {
w2 := a.NewWindow("Second window")
w2.SetContent(widget.NewLabel("Each desktop window is its own OS window."))
w2.Resize(geo.NewSize(300, 120))
w2.Show()
}On the desktop every window from App.NewWindow is a separate OS window, and
any number can be open. The app quits when its last window closes. To make
one window the main one, so that closing it quits the app even while others
are open, call SetMaster on it (the
bellui.MasterWindow
interface). To ask before a window closes, use SetCloseIntercept.
The driver/desktop
package has more window control. Assert the window to:
desktop.Windowto request a position, keep the window on top, or go full screen on a second monitor;desktop.PositionableWindowto read the position back and be told when the window moves, which is how you save and restore window placement.
a.Driver().(desktop.ScreensDriver) lists the connected monitors with their
size, usable area, scale and which one is primary. Positions are ignored on
Wayland, which does not let apps place windows.
Keyboard shortcuts
A shortcut added to a window’s canvas runs while that window has focus:
save := &desktop.CustomShortcut{KeyName: event.KeyS, Modifier: event.KeyModifierShortcutDefault}
w.Canvas().AddShortcut(save, func(core.Shortcut) {
status.SetText("Saved")
})desktop.CustomShortcut
combines a key with modifiers such as event.KeyModifierShift,
KeyModifierControl, KeyModifierAlt and KeyModifierSuper. A focused
widget that handles shortcuts itself, such as an entry handling copy and
paste, gets them first; menu shortcuts are checked before both.
Key down and key up
The usual key callbacks report keys as they are typed. For games and other
apps that need to know when a key goes down and when it comes up, assert the
canvas to
desktop.Canvas:
if dc, ok := w.Canvas().(desktop.Canvas); ok {
dc.SetOnKeyDown(func(ev *event.KeyEvent) {
keys.SetText("Down: " + string(ev.Name))
})
dc.SetOnKeyUp(func(ev *event.KeyEvent) {
keys.SetText("Up: " + string(ev.Name))
})
}Global shortcuts
A global shortcut works even when your app is in the background:
if g, ok := a.Driver().(desktop.GlobalShortcutApp); ok {
show := &desktop.CustomShortcut{
KeyName: event.KeySpace,
Modifier: event.KeyModifierControl | event.KeyModifierAlt,
}
if err := g.RegisterGlobalShortcut(show, w.RequestFocus); err != nil {
status.SetText("Global shortcut unavailable: " + err.Error())
}
}Registration can fail, for example because another app already owns the keys,
so always check the error. On Wayland the request goes through the desktop
portal, which may ask the user for permission; a nil error there means the
request was accepted, not that the shortcut is already active.
Other desktop features
| Feature | How |
|---|---|
| Badge on the dock or taskbar icon | a.(bellui.BadgeApp).SetBadge(n); 0 clears it. |
| Start at login | a.(bellui.LaunchAtLoginApp).SetLaunchAtLogin(true). Meant for installed apps; on macOS it needs a .app bundle. |
| One running copy | a.(bellui.SingleInstanceApp).SetSingleInstance(func(args []string) {…}), called before Run. A second launch passes its arguments to the first and should exit. |
| Files and links opened with the app | a.(bellui.OpenHandlerApp) with SetOnFileOpened and SetOnURLOpened, together with file associations and URL schemes in the manifest. |
| Mouse buttons and cursors | Widgets can implement desktop.Mouseable and desktop.Cursorable. |
The complete examples
// Desktop integration: main menu, system tray, notifications, more windows.
package main
import (
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/dialog"
"github.com/belluikit/bellui/driver/desktop"
"github.com/belluikit/bellui/event"
"github.com/belluikit/bellui/geo"
"github.com/belluikit/bellui/theme"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.NewWithID("com.example.desktop")
w := a.NewWindow("Desktop")
w.Resize(geo.NewSize(420, 300))
newItem := bellui.NewMenuItem("New window", func() { openSecondWindow(a) })
newItem.Shortcut = &desktop.CustomShortcut{
KeyName: event.KeyN,
Modifier: event.KeyModifierShortcutDefault, // Cmd on macOS, Ctrl elsewhere
}
file := bellui.NewMenu("File", newItem)
help := bellui.NewMenu("Help",
bellui.NewMenuItem("About", func() {
dialog.ShowInformation("About", "A desktop example.", w)
}))
w.SetMainMenu(bellui.NewMainMenu(file, help))
// The tray is reached through the driver: in v1.0.0 the app value
// itself does not implement all of desktop.App.
if desk, ok := a.Driver().(desktop.App); ok {
desk.SetSystemTrayMenu(bellui.NewMenu("Tray",
bellui.NewMenuItem("Show window", w.Show),
))
desk.SetSystemTrayIcon(theme.HomeIcon())
desk.SetSystemTrayTooltip("Desktop example")
// Tapping the tray icon shows w, and closing w now only hides it.
desk.SetSystemTrayWindow(w)
}
notify := widget.NewButton("Send notification", func() {
bellui.SendNotification(bellui.NewNotification("Export finished", "3 files written"))
})
rich := widget.NewButton("Send notification with actions", func() {
bellui.SendNotificationWithOptions(&bellui.NotificationOptions{
Title: "Download ready",
Content: "report.pdf",
OnTapped: w.Show,
Actions: []bellui.NotificationAction{
{Label: "Open", Callback: func() { w.Show() }},
},
})
})
w.SetContent(container.NewVBox(
widget.NewLabel("Use the File menu, the tray icon or the buttons."),
notify, rich,
))
bellui.ShowAndRun(w)
}
func openSecondWindow(a bellui.App) {
w2 := a.NewWindow("Second window")
w2.SetContent(widget.NewLabel("Each desktop window is its own OS window."))
w2.Resize(geo.NewSize(300, 120))
w2.Show()
}// Keyboard shortcuts and raw key events on the desktop.
package main
import (
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/core"
"github.com/belluikit/bellui/driver/desktop"
"github.com/belluikit/bellui/event"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.NewWithID("com.example.shortcuts")
w := a.NewWindow("Shortcuts")
status := widget.NewLabel("Press Ctrl+S (Cmd+S on macOS)")
keys := widget.NewLabel("")
save := &desktop.CustomShortcut{KeyName: event.KeyS, Modifier: event.KeyModifierShortcutDefault}
w.Canvas().AddShortcut(save, func(core.Shortcut) {
status.SetText("Saved")
})
if dc, ok := w.Canvas().(desktop.Canvas); ok {
dc.SetOnKeyDown(func(ev *event.KeyEvent) {
keys.SetText("Down: " + string(ev.Name))
})
dc.SetOnKeyUp(func(ev *event.KeyEvent) {
keys.SetText("Up: " + string(ev.Name))
})
}
if g, ok := a.Driver().(desktop.GlobalShortcutApp); ok {
show := &desktop.CustomShortcut{
KeyName: event.KeySpace,
Modifier: event.KeyModifierControl | event.KeyModifierAlt,
}
if err := g.RegisterGlobalShortcut(show, w.RequestFocus); err != nil {
status.SetText("Global shortcut unavailable: " + err.Error())
}
}
w.SetContent(container.NewVBox(status, keys))
bellui.ShowAndRun(w)
}