Skip to content

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 Shortcut is shown next to its label, and pressing the keys runs the item’s action while the window has focus. event.KeyModifierShortcutDefault is 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.
  • MenuItem also has Checked, Disabled, Icon, ChildMenu for sub-menus, and IsSeparator. After changing items, call Refresh() 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)
}
MethodWhat it does
SetSystemTrayMenuShows the tray icon with this menu.
SetSystemTrayIconSets 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.
SetSystemTrayTooltipSets the hover text, where the system shows one.
SetSystemTrayWindowMakes a left click on the icon show this window. It also makes closing the window hide it instead, so the app keeps running.
SetSystemTrayOnTappedRuns 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.

On Linux the tray icon appears only if the desktop runs a StatusNotifierItem host. KDE Plasma and most Wayland compositors do; stock GNOME needs the AppIndicator extension, and older trays that only support the XEmbed protocol are not supported. Never make the tray the only way to reach a feature.
In this release, assert 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.Window to request a position, keep the window on top, or go full screen on a second monitor;
  • desktop.PositionableWindow to 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

FeatureHow
Badge on the dock or taskbar icona.(bellui.BadgeApp).SetBadge(n); 0 clears it.
Start at logina.(bellui.LaunchAtLoginApp).SetLaunchAtLogin(true). Meant for installed apps; on macOS it needs a .app bundle.
One running copya.(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 appa.(bellui.OpenHandlerApp) with SetOnFileOpened and SetOnURLOpened, together with file associations and URL schemes in the manifest.
Mouse buttons and cursorsWidgets 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)
}