Your first app
This page walks you through a complete gift program: a counter with a number and two buttons. It is short, but it contains every part a gift application needs. Read it once from top to bottom, then take it apart piece by piece below.
The whole program
Create a new module, add gift as described in Installation, and put this into main.go:
package main
import (
"fmt"
"os"
"strconv"
"github.com/worldiety/gift"
backend "github.com/worldiety/gift/backend/ebiten"
"github.com/worldiety/gift/font/inter"
"github.com/worldiety/gift/geom"
"github.com/worldiety/gift/ui"
)
func main() {
ui.SetDefaultFont(ui.MustFont(ui.FontQuery{Family: inter.Family}))
app := gift.New(gift.Options{Root: counter})
err := backend.Run(app, backend.Config{Title: "Counter", Width: 400, Height: 300})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func counter(ctx *gift.Context) gift.View {
count := ctx.State("count", 0)
n := ctx.Read(count)
return ui.Window(
ui.VStack(
ui.Text(strconv.Itoa(n)).FontSize(48),
ui.HStack(
ui.Button(ui.Text("-"), func() {
count.Set(count.Get() - 1)
}).Disabled(n == 0),
ui.Button(ui.Text("+"), func() {
count.Set(count.Get() + 1)
}),
).Gap(8),
).Gap(16).Padding(24).Align(geom.Center),
).Align(geom.Center)
}Run it:
go run .A window titled “Counter” opens. Click + and the number goes up. Click - and it goes down, but never below zero:
at zero the - button is disabled. You can also press Tab to move the focus to a button and activate
it with Space or Enter.
Step by step
Install a font
ui.SetDefaultFont(ui.MustFont(ui.FontQuery{Family: inter.Family}))Gift links no font into your binary unless you ask for one. The package font/inter embeds the Inter typeface and
registers it when you import it. It does not make itself the default, because that choice belongs to the
application. ui.MustFont looks up the registered face and ui.SetDefaultFont makes it the font of every
ui.Text that does not name another one.
Install the font before the first frame. If a ui.Text is built without any font, gift panics with a message
that tells you how to fix it. A second bundled typeface is font/ibmplexmono, and ui.LoadFont loads your own
font file. See Text and fonts.
Create the app
app := gift.New(gift.Options{Root: counter})gift.New creates the application and mounts Root as its root component. It builds nothing yet; that happens in
the first update. The goroutine that calls gift.New becomes the UI goroutine of the app. Every view function and
every state write runs on it. See Concurrency for how background work hands its
results back.
Write the view function
func counter(ctx *gift.Context) gift.View {
count := ctx.State("count", 0)
n := ctx.Read(count)
// ...
}A view function takes a *gift.Context and returns a gift.View. It describes what the screen looks like right
now and keeps nothing itself. Whatever has to survive between two builds lives in state:
ctx.State("count", 0)returns the state namedcountof this component. The first build creates it with the value0; every later build returns the same*gift.State[int].ctx.Read(count)returns the current value and records that this component depends on it.count.Get()returns the value without recording a dependency. Use it inside event handlers, like the button actions here.count.Set(...)stores a new value. Gift rebuilds every component that read the state in its next update, not insideSet.
See State for the details.
Compose the views
ui.VStack(
ui.Text(strconv.Itoa(n)).FontSize(48),
ui.HStack(
ui.Button(ui.Text("-"), func() { /* ... */ }).Disabled(n == 0),
ui.Button(ui.Text("+"), func() { /* ... */ }),
).Gap(8),
).Gap(16).Padding(24).Align(geom.Center)Views are plain Go values. ui.VStack arranges its children from top to bottom, ui.HStack from left to right.
ui.Button takes a label, which is itself a view, and an action. Methods such as FontSize, Gap, Padding and
Disabled are modifiers: each one returns a changed copy of the view, so you can chain them. Align(geom.Center)
on the vertical stack centers its children horizontally.
The buttons need no styling: without a style they take their colors from the current theme, which is the light theme by default. See Views, Modifiers, Layout and Theming.
Wrap everything in a window
return ui.Window(
ui.VStack(/* ... */),
).Align(geom.Center)Gift draws exactly what the view tree describes and nothing else. It paints no background of its own, so a root
view that does not cover the window leaves holes in it. ui.Window fills the whole window with the theme’s
background color and puts your content on top. Align(geom.Center) places the content in the middle of the
window. Make ui.Window the outermost view of every root function.
Open the window
err := backend.Run(app, backend.Config{Title: "Counter", Width: 400, Height: 300})The package backend/ebiten connects the app to Ebitengine. Run opens a window,
forwards mouse, keyboard and touch input to the app, and draws it on the GPU until the window is closed. Call it
from the goroutine that called gift.New.
Config sets the window title and the initial size in logical pixels; zero selects 1280x720. It has more fields
for kiosks and measurements, for example DrawOnDemand, which skips drawing frames in which nothing changed. See
Kiosks and the
package documentation.
Test it
Because the interface is plain Go, you can test it without a window. gifttest builds the view tree headlessly
and clicks what the user sees:
package main
import (
"testing"
"github.com/worldiety/gift/font/inter"
"github.com/worldiety/gift/gifttest"
"github.com/worldiety/gift/ui"
)
func TestMain(m *testing.M) { gifttest.Main(m) }
func TestCounter(t *testing.T) {
h := gifttest.New(t, gifttest.Options{
Root: counter,
Font: ui.MustFont(ui.FontQuery{Family: inter.Family}),
})
h.Find(gifttest.ByText("+")).Click()
h.Find(gifttest.ByText("+")).Click()
h.Find(gifttest.ByText("2"))
h.Find(gifttest.ByText("-")).Click()
h.Find(gifttest.ByText("1"))
}
func TestCounterPaintsTheWindow(t *testing.T) {
h := gifttest.New(t, gifttest.Options{
Root: counter,
Font: ui.MustFont(ui.FontQuery{Family: inter.Family}),
})
h.AssertOpaque()
}Options.Font installs the font for the duration of the test and restores the previous one afterwards.
Find fails the test unless exactly one node matches, so h.Find(gifttest.ByText("2")) is already an
assertion. Click moves the mouse onto the node, presses and releases it, and lets the app settle before it
returns.
AssertOpaque checks that the root view paints every pixel of the window, which is what ui.Window is for. It
needs real pixels: a plain go test skips it, and go test -tags giftgpu runs it. The TestMain line is required
for that mode and does nothing without the tag. See Testing.
Next steps
The counter example is the same program with more to it: styled buttons with hover and pressed states, a scroll view, and a button that switches between the light and the dark theme at runtime. Run it from a checkout of the repository:
go run ./cmd/example-counter