MyGo
Views

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.