Guides
Views
Build lifetimes, event queries and callbacks, keys, and changing the state from other goroutines.
The view is a function from your app's state to its interface. MyGo calls it on the main thread to build frames after input, model changes and while something animates. A frame can contain several build passes, so it can show the outcome of an action before it is painted.
The window has one stable *ui.Context. Child builders share it and
switch its current parent for the duration of the callback. Pass it to
helpers that build part of the view.
Each ui.Element value belongs to one build pass. Its owner, slot and
generation are checked before accessing the control. Use Valid for an
optional element and ui.Element{} for absence. Stale use is diagnosed in
development and Tester; production methods return empty results or
ignore the operation.
Keep build values out of app state and background work. Store ui.Handle
for persistent control identity and ui.Services for callbacks between
builds. Model data and widget state such as ListState belong in your app.
// todoList is the app's state, which lasts.
type todoList struct {
todos []Todo
draft string
}
// view builds the interface from it.
func (app *todoList) view(c *ui.Context) {
ui.Column(c).Fill().Padding(16).Gap(8).Children(func() {
for i := range app.todos {
ui.Checkbox(c, &app.todos[i].Done, app.todos[i].Title)
}
if ui.TextInput(c, &app.draft).Placeholder("New to-do").Submitted() {
app.todos = append(app.todos, Todo{Title: app.draft})
app.draft = ""
}
})
}Make the state once, as the app starts, and give its view to the window:
app := &todoList{}
mygo.NewWindow(mygo.WindowOptions{Title: "To-dos", Content: ui.View(app.view)})ui.View(app.view) is the window's content (mygo.WindowOptions.Content);
a Content can serve several windows, each with its own element state.
The examples in these guides are parts of such a view: c is the view's
*ui.Context, and app its receiver, the value of your own type that
holds the state, as todoList here. A field such as app.volume or a
method such as app.save() is one you declare on that type. Values that
last, such as a router, are fields made with the rest of
the state, never in the view, which runs for every frame.
Events and actions
Clicked reports a click since the last frame and keeps ordinary control
flow beside the control:
if ui.Button(c, "Save").Clicked() {
app.save()
}A control takes the user's input as its constructor returns, so an inline save sees the latest edits of the controls built before it.
OnClick schedules the action after the view finishes building and bound
input has been applied:
ui.Button(c, "Save").Disabled(app.saving).OnClick(app.save)Use polling when inline return, break or continue is useful. Use
callbacks for actions that modify the collection being built, so the loop
finishes before its model changes:
for _, item := range app.items {
ui.Row(c.Key(item.ID)).Children(func() {
ui.Text(c, item.Title).Grow(1)
ui.Button(c, "Delete").OnClick(func() {
app.removeItem(item.ID)
})
})
}Handled input is consumed before rebuilding, so an action runs once for that input. MyGo rebuilds changed state before painting the frame.
Widgets that change a value take a pointer to it, so they need no handler:
ui.Checkbox(c, &app.settings.Sync, "Sync") binds the field to the control.
As in Dear ImGui and egui, the control writes the user's edit through the
pointer before its constructor returns, so the next statement reads the new
value. Changed reports that it changed in this build pass, and Submitted
that Enter was pressed in a single-line text input.
if ui.TextInput(c, &app.query).Placeholder("Search").Changed() {
app.results = search(app.query)
}Local copies work too, including values read from a map, compared with the
value they came from or through Changed:
viewed := app.viewed[file.ID]
if ui.Checkbox(c, &viewed, "Viewed").Changed() {
app.viewed[file.ID] = viewed
}
chosen := app.language
ui.RadioGroup(c, func() {
for _, tag := range app.languages {
ui.Radio(c, &chosen, tag, i18n.Autonym(tag))
}
})
if chosen != app.language {
app.setLanguage(chosen)
}A container takes its input with its children, as the call that built it
returns: a radio group's arrow keys move among all of its radios. Input
follows the options of the frame the user acted on: an input shown
read-only takes no typing, and Disabled, ReadOnly or Step set in
this build govern the input of the frames after it, as they show.
OnChange and OnSubmit run after construction and bound input, before the
rebuild. Use callbacks when an action should wait until the view has finished
building, such as modifying the collection being rendered. They can also
commit local copies:
viewed := app.viewed[file.ID]
ui.Checkbox(c, &viewed, "Viewed").OnChange(func() {
app.viewed[file.ID] = viewed
})Keys
Give a control its key before construction, so its state follows the item when the surrounding collection changes:
ui.Checkbox(c.Key(todo.ID), &todo.Done, todo.Title)
ui.TextInput(c.Key("search"), &app.query)Keys are unique among siblings, and compare as Go compares a map's keys: a
pointer keys the object it points to, whatever the object holds, so an
item's pointer serves as its key as well as an ID does. A key Go cannot
compare, as a slice, or not equal to itself, as a NaN, compares by its
value printed. Element.Key can
key a container before its children or local state are built. Use
Context.Key for stateful controls and custom base widgets.
Context.Key sets one pending key for the next control; calling it again
replaces that key. c.Key("account").Key("secret") uses only "secret".
Create a keyed parent to give its children a separate identity scope:
ui.Column(c.Key(app.accountID)).Children(func() {
ui.TextInput(c.Key("secret"), &app.secret).Password()
})Changing the account ID gives the input a new identity, even though its
own key stays "secret". Within the same account, that input keeps its
state from build to build. The new editor starts with app.secret; clear
or load that model value separately when switching accounts.
Change the state from other goroutines
The view reads your state on the main thread. Change it from other
goroutines with Window.Update, which runs a function on the main thread
and then draws a new frame:
win := mygo.NewWindow(mygo.WindowOptions{Content: ui.View(app.view)})
go func() {
items, err := fetchItems()
win.Update(func() { app.items, app.err = items, err })
}()Window.Invalidate only draws a new frame, for state you guard yourself.
Within the view, c.Invalidate() asks for another frame and c.After(d) for
one after a delay, such as a clock's next second.
Update builds the view again after every call, whether the function
changed the state or not. A goroutine that polls data that rarely
changes, as a connection count or a status, compares it on the main thread
with mygo.RunOnMain, which draws nothing, and calls Invalidate only
when it changed:
for n := range counts {
mygo.RunOnMain(func() {
if n != app.connections {
app.connections = n
win.Invalidate()
}
})
}State of an element
An element keeps state of its own from frame to frame with ui.Local, for
widgets you build yourself; see custom widgets. A
value of it that is an io.Closer closes when the element is no longer
built, which stops the work the widget started
(stopping what a widget started).
A view that only needs to run a function as an element goes, such as the
cancel of a streaming request, a ticker or a subscription it started,
gives it to OnUnmount instead of defining a type with Close:
func Feed(c *ui.Context, win *mygo.Window, app *App, url string) {
e := ui.Column(c)
cancel := ui.Local(e, "stream", func() context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
go read(ctx, url, func(line string) {
win.Update(func() { app.lines = append(app.lines, line) })
})
return cancel
})
e.OnUnmount(*cancel)
e.Children(func() {
for _, line := range app.lines {
ui.Text(c, line)
}
})
}The function runs as a Local's Close does: once, on the main thread,
after the first frame that no longer builds the element, as the view
switches to another screen, a list's row scrolls out of view or a popover
closes. An element leaving with an exit transition runs it once its exit
ended, and every element runs it as its window closes. Call OnUnmount
as the element is built, after Key: each call replaces the function,
and the last one given runs; a nil function takes it away.
Check build lifetimes
Run mygo vet from your app directory to find build values stored in
structs, package variables or goroutines. See Migration
for the source migration command and Performance for
measured frame costs.