Skip to content

Getting Started

mike-ward edited this page Jun 14, 2026 · 2 revisions

Getting Started

This page walks through the minimal go-gui application line by line. By the end you'll understand the window lifecycle, the view function, how state flows, and how widgets are composed.

Prerequisites: Installation complete, go run ./examples/get_started/ works.


The complete program

package main

import (
    "fmt"

    "github.com/go-gui-org/go-gui/gui"
    "github.com/go-gui-org/go-gui/gui/backend"
)

type App struct{ Clicks int }

func main() {
    gui.SetTheme(gui.ThemeDarkBordered)

    w := gui.NewWindow(gui.WindowCfg{
        State:  &App{},
        Title:  "get_started",
        Width:  300,
        Height: 300,
        OnInit: func(w *gui.Window) { w.UpdateView(mainView) },
    })

    backend.Run(w)
}

func mainView(w *gui.Window) gui.View {
    ww, wh := w.WindowSize()
    app := gui.State[App](w)

    return gui.Column(gui.ContainerCfg{
        Width:  float32(ww),
        Height: float32(wh),
        Sizing: gui.FixedFixed,
        HAlign: gui.HAlignCenter,
        VAlign: gui.VAlignMiddle,
        Content: []gui.View{
            gui.Text(gui.TextCfg{
                Text:      "Hello GUI!",
                TextStyle: gui.CurrentTheme().B1,
            }),
            gui.Button(gui.ButtonCfg{
                IDFocus: 1,
                Content: []gui.View{
                    gui.Text(gui.TextCfg{
                        Text: fmt.Sprintf("%d Clicks", app.Clicks),
                    }),
                },
                OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
                    gui.State[App](w).Clicks++
                    e.IsHandled = true
                },
            }),
        },
    })
}

Step by step

Application state

type App struct{ Clicks int }

This is plain Go. No framework base type, no special interface, no observable fields. Go-gui doesn't care what your state type looks like — it just stores a pointer to it and hands it back to you.

Theme

gui.SetTheme(gui.ThemeDarkBordered)

Themes are global and set before any window is created. Built-in options include ThemeDark, ThemeDarkBordered, ThemeLight, and others. See Theming for custom themes.

Creating a window

w := gui.NewWindow(gui.WindowCfg{
    State:  &App{},
    Title:  "get_started",
    Width:  300,
    Height: 300,
    OnInit: func(w *gui.Window) { w.UpdateView(mainView) },
})

WindowCfg is the one configuration point for a window:

  • State — a pointer to your application state. Retrieved anywhere with gui.State[App](w).
  • OnInit — called once after the window is created. w.UpdateView(mainView) registers the view function that will be called every frame to produce the UI.

Running the event loop

backend.Run(w)

This starts the event loop and blocks until the window is closed. backend.Run selects the appropriate renderer automatically (Metal on macOS, OpenGL elsewhere). The call never returns under normal operation.

The view function

func mainView(w *gui.Window) gui.View {

This is the heart of a go-gui application. The framework calls this function every frame (or whenever a redraw is needed). It returns a tree of gui.View values that describe the entire UI.

The function must be pure with respect to w — its output should depend only on the window state and nothing else. No side effects, no I/O. Side effects belong in event callbacks.

Reading state

app := gui.State[App](w)

gui.State[T] is a generic function that retrieves the typed state pointer stored in the window. This is the idiomatic way to read app state inside a view function or event callback. No globals, no closures, no context passing — just the window.

Layout: Column

gui.Column(gui.ContainerCfg{
    Width:  float32(ww),
    Height: float32(wh),
    Sizing: gui.FixedFixed,
    HAlign: gui.HAlignCenter,
    VAlign: gui.VAlignMiddle,
    Content: []gui.View{ … },
})

Column arranges its children vertically. ContainerCfg controls sizing and alignment:

  • Sizing: gui.FixedFixed — use the explicit Width and Height values (fill the window).
  • HAlign: gui.HAlignCenter — center children horizontally.
  • VAlign: gui.VAlignMiddle — center children vertically.
  • Content — the child views, as a []gui.View slice.

Other sizing modes: FillFill (grow on both axes), FillFit (grow horizontally, fit height), FixedFit (fixed width, fit height).

Text

gui.Text(gui.TextCfg{
    Text:      "Hello GUI!",
    TextStyle: gui.CurrentTheme().B1,
})

Text renders a string with the given style. gui.CurrentTheme().B1 is the theme's first body text style. Theme typography tokens are named N1–N5 (neutral/UI) and B1–B5 (body).

Button

gui.Button(gui.ButtonCfg{
    IDFocus: 1,
    Content: []gui.View{
        gui.Text(gui.TextCfg{Text: fmt.Sprintf("%d Clicks", app.Clicks)}),
    },
    OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
        gui.State[App](w).Clicks++
        e.IsHandled = true
    },
})

A few things to notice:

IDFocus: 1 — This marks the button as keyboard-focusable and gives it tab-order position

  1. Any widget that should receive keyboard focus needs an IDFocus value greater than zero. The value also determines tab order: lower numbers are focused first. This is the first introduction to go-gui's ID system — see Focus and Scrolling for the full explanation.

Content — Buttons in go-gui contain arbitrary View children. A button is a container with click behavior, not a widget with a fixed text label. This makes it straightforward to put icons, images, or complex layouts inside a button.

OnClick — The event callback signature is func(*gui.Layout, *gui.Event, *gui.Window). All event callbacks in go-gui share this shape. The *gui.Layout argument is the layout node that received the event, *gui.Event carries the event data, and *gui.Window is the window — the same w available in the view function.

e.IsHandled = true — Setting this stops the event from propagating further up the layout tree. It's idiomatic to set it in any callback that has fully handled the event.

Updating stategui.State[App](w).Clicks++ mutates the state directly. The framework does not need to be notified of the change; the view function is called again next frame and will read the updated value naturally.


What happens each frame

  1. An event arrives (or a redraw is scheduled).
  2. The framework calls mainView(w), which reads app.Clicks and returns a layout tree.
  3. The layout engine sizes and positions every node in the tree.
  4. The renderer emits draw commands for each node.
  5. The backend (Metal or OpenGL) executes those commands.

The view function produces the complete UI every frame from current state. There is no incremental update, no diffing, no reconciliation — just a fresh layout tree each time.


Next steps

The examples/ directory contains 35+ runnable applications. A good sequence:

Example What to look at
examples/get_started/ This walkthrough (slightly extended with tooltip and shadow)
examples/todo/ A realistic app: list state, input, conditional rendering
examples/calculator/ Keyboard handling, custom button layouts
examples/showcase/ Interactive reference for every widget in the framework

Run the showcase and explore — every demo has a documentation button in the upper-right corner that explains the widget and its configuration.

go run ./examples/showcase/

Clone this wiki locally