Skip to content

Advanced

mike-ward edited this page May 17, 2026 · 2 revisions

Advanced

This page covers multi-window applications, the command/shortcut system, touch gestures, native OS integration, audio, and accessibility.


Multi-window

Applications with more than one window use gui.NewApp() and backend.RunApp.

app := gui.NewApp()
app.ExitMode = gui.ExitOnMainClose   // quit when the first window closes

w1 := gui.NewWindow(gui.WindowCfg{State: &Main{}, Title: "Main"})
w2 := gui.NewWindow(gui.WindowCfg{State: &Inspector{}, Title: "Inspector"})

backend.RunApp(app, w1, w2)

Open additional windows at runtime:

app.OpenWindow(gui.WindowCfg{State: &Detail{}, Title: "Detail"})

Cross-window communication:

// Call fn in every open window's render goroutine.
app.Broadcast(func(w *gui.Window) {
    gui.State[Main](w).NotifyCount++
})

// Target a specific window.
w2.QueueCommand(func(w *gui.Window) {
    gui.State[Inspector](w).SelectedID = newID
})

QueueCommand defers the callback to the next frame — it is safe to call from any goroutine.


Commands and keyboard shortcuts

The command system centralises actions with keyboard shortcuts, enable/disable logic, and automatic integration with the CommandPalette widget.

Registering commands

w.RegisterCommand(gui.Command{
    ID:       "file.save",
    Label:    "Save",
    Group:    "File",
    Icon:     gui.IconSave,
    Shortcut: gui.Shortcut{Key: gui.KeyS, Modifiers: gui.ModCtrl},
    Execute: func(e *gui.Event, w *gui.Window) {
        save(gui.State[App](w))
    },
    CanExecute: func(w *gui.Window) bool {
        return gui.State[App](w).HasUnsavedChanges
    },
    Global: true,   // fires before focus dispatch
})

Global: true means the shortcut fires even when a text input has focus. Use false (the default) for commands that should yield to the focused widget's own key handling.

When CanExecute returns false, the shortcut is skipped and any button bound to the command disables itself automatically.

Dispatch order per key event:

  1. Global commands (Global: true)
  2. Focused widget's OnKeyDown
  3. Tab / Shift+Tab focus cycling
  4. Non-global commands (Global: false)

CommandButton

gui.CommandButton creates a button that is automatically labelled and disabled from a registered command:

gui.CommandButton(w, "file.save", gui.ButtonCfg{ID: "btn-save"})

The button shows the command's Label and shortcut hint, and is disabled whenever CanExecute returns false — no manual wiring needed.

Modifier constants

Constant Key
ModCtrl Ctrl
ModShift Shift
ModAlt Alt / Option
ModSuper ⌘ on macOS, Win on Windows

Combine: gui.ModCtrl | gui.ModShift.


Gestures

Touch gestures are delivered through the same EventHandlers callbacks as mouse events. Add OnGesture to any container or DrawCanvas:

gui.DrawCanvas(gui.DrawCanvasCfg{
    ID:     "gesture-pad",
    Width:  400,
    Height: 300,
    Color:  gui.RGBA(30, 30, 40, 255),
    OnGesture: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
        a := gui.State[App](w)
        switch e.GestureType {
        case gui.GestureTap:
            a.Markers = append(a.Markers, Point{e.CentroidX, e.CentroidY})
        case gui.GesturePan:
            if e.GesturePhase == gui.GesturePhaseChanged {
                a.OffsetX += e.GestureDX
                a.OffsetY += e.GestureDY
            }
        case gui.GesturePinch:
            a.Scale *= e.PinchScale
        case gui.GestureRotate:
            a.Rotation += e.GestureRotation
        }
        e.IsHandled = true
    },
})

Gesture types:

Constant Description
GestureTap Single finger tap
GestureDoubleTap Two taps in quick succession
GestureLongPress Finger held without movement
GesturePan Single finger drag
GestureSwipe Fast pan ending with high velocity
GesturePinch Two-finger spread or squeeze
GestureRotate Two-finger twist

Gesture phases (for continuous gestures: Pan, Pinch, Rotate):

Constant When
GesturePhaseBegan First recognition
GesturePhaseChanged Ongoing update
GesturePhaseEnded Final event
GesturePhaseCancelled Cancelled by system

Relevant event fields:

Field Available for
e.CentroidX, e.CentroidY All gestures — centre of active touches
e.GestureDX, e.GestureDY Pan, Swipe — delta from previous event
e.VelocityX, e.VelocityY Swipe — velocity at end
e.PinchScale Pinch — cumulative scale (1.0 = unchanged)
e.GestureRotation Rotate — cumulative radians

On desktop, the framework synthesises mouse events so gesture-enabled widgets also respond to mouse click and drag.


Native dialogs

All native dialog methods are on *gui.Window and are non-blocking. Results arrive via OnDone callback on the main goroutine.

File dialogs

w.NativeOpenDialog(gui.NativeOpenDialogCfg{
    Title:         "Open File",
    StartDir:      "/home/user/documents",
    AllowMultiple: false,
    Filters: []gui.NativeFileFilter{
        {Name: "Go source", Extensions: []string{"go"}},
        {Name: "All files", Extensions: []string{"*"}},
    },
    OnDone: func(result gui.NativeDialogResult, w *gui.Window) {
        if result.Status != gui.DialogOK || len(result.Paths) == 0 {
            return
        }
        loadFile(w, result.Paths[0].Path)
    },
})
w.NativeSaveDialog(gui.NativeSaveDialogCfg{
    Title:            "Save As",
    DefaultName:      "output.json",
    DefaultExtension: "json",
    ConfirmOverwrite: true,
    Filters: []gui.NativeFileFilter{
        {Name: "JSON", Extensions: []string{"json"}},
    },
    OnDone: func(result gui.NativeDialogResult, w *gui.Window) {
        if result.Status == gui.DialogOK && len(result.Paths) > 0 {
            writeFile(w, result.Paths[0].Path)
        }
    },
})

w.NativeFolderDialog(cfg) opens a folder picker with the same OnDone pattern.

Message and confirm dialogs

w.NativeMessageDialog(gui.NativeMessageDialogCfg{
    Title: "Export complete",
    Body:  "The file was saved successfully.",
    Level: gui.AlertInfo,
})

w.NativeConfirmDialog(gui.NativeConfirmDialogCfg{
    Title: "Delete item?",
    Body:  "This action cannot be undone.",
    Level: gui.AlertWarning,
    OnDone: func(result gui.NativeAlertResult, w *gui.Window) {
        if result.Status == gui.DialogOK {
            deleteSelected(w)
        }
    },
})

gui.NativeSaveDiscardDialog adds a three-way Discard option, returning DialogDiscard for "don't save", DialogOK for "save", and DialogCancel for "cancel".

Alert levels: AlertInfo, AlertWarning, AlertCritical.


Audio

Audio is opt-in via the github.com/mike-ward/go-gui/gui/audio package, which wraps SDL_mixer. It is excluded from JS, Android, and iOS builds via a build tag.

Initialise once before loading any sounds, typically in main:

import "github.com/mike-ward/go-gui/gui/audio"

if err := audio.Init(); err != nil {
    log.Fatal(err)
}
defer audio.Quit()

Sound effects

// Load from file (WAV, OGG, MP3, FLAC, MOD).
snd, err := audio.LoadSound("click.wav")

// Or from embedded bytes.
snd, err := audio.LoadSoundBytes(clickWAV)

// Play once on the first available channel.
snd.PlayOnce()

// Per-sound volume (0.0–1.0).
snd.SetVolume(0.6)

// Free when no longer needed.
snd.Free()

snd.Play(channel, loops) targets a specific mixer channel and loops n times (0 = once, -1 = infinite).

Music

mus, err := audio.LoadMusic("background.ogg")
mus.Play(-1)          // loop indefinitely
// ...
audio.HaltMusic()
audio.FadeOutMusic(500)  // fade over 500 ms
audio.PauseMusic()
audio.ResumeMusic()
mus.Free()

Volume

audio.SetMasterVolume(0.8)   // sound effects, 0.0–1.0
audio.SetMusicVolume(0.5)    // music track, 0.0–1.0

Accessibility

Standard widgets (Button, TextInput, Slider, ProgressBar, etc.) expose accessibility metadata automatically. The framework builds an A11yNode tree from the layout after each frame and hands it to the platform backend:

  • macOS — NSAccessibility (VoiceOver)
  • Linux — AT-SPI2 (Orca, Accerciser)
  • Web — ARIA attributes

For custom DrawCanvas widgets, set A11YLabel and A11YDescription on the config:

gui.DrawCanvas(gui.DrawCanvasCfg{
    ID:              "chart",
    A11YLabel:       "Sales chart",
    A11YDescription: "Bar chart showing monthly sales for 2025",
    OnDraw:          renderChart,
})

These strings are picked up by the a11y tree walk and surfaced to assistive technology.


Time-travel debugging

Opt-in with DebugTimeTravel: true on WindowCfg. Implement the Snapshotter interface on your state struct:

type App struct { Count int }

func (s *App) Snapshot() any  { c := *s; return &c }
func (s *App) Restore(v any)  { *s = *v.(*App) }

app := gui.NewApp()
main := gui.NewWindow(gui.WindowCfg{
    State:           &App{},
    DebugTimeTravel: true,
})
backend.RunApp(app, main)

A scrubber window opens alongside the app. Drag the slider or use //Home/End to step through captured states. Space freezes the app; Esc resumes live.

Use w.Now() instead of time.Now() anywhere the view depends on the clock — this lets the scrubber produce correct output when rewound. The feature is zero-cost when the flag is off and read-only: rewinding does not undo past I/O side effects.

Clone this wiki locally