The UI thread
Bellui runs the user interface on one goroutine, the main goroutine (the UI thread). Input events, widget callbacks such as a button’s tap function, and drawing all happen there, one at a time. This has two consequences:
- Code in a callback can change the UI directly. No locking is needed.
- Code in a callback must return quickly. While it runs, the window cannot redraw or respond to input.
Slow work goes on another goroutine
To do something slow, such as a network request or a large file read, start a
goroutine from the callback. When the work is done, hand the UI update back
to the main goroutine with
bellui.Do:
status := widget.NewLabel("Idle")
var start *widget.Button
start = widget.NewButton("Start slow work", func() {
// This callback runs on the main goroutine. Blocking here would
// freeze the window, so the work moves to its own goroutine.
start.Disable()
status.SetText("Working...")
go func() {
time.Sleep(3 * time.Second) // stands in for real work
bellui.Do(func() {
status.SetText("Done")
start.Enable()
})
}()
})The button’s function runs on the main goroutine, so it disables the button
and sets the label directly. The goroutine sleeps without blocking anything.
Its result goes back through bellui.Do.
Updating the UI from a goroutine
The same applies to any goroutine you start, such as a timer:
clock := canvas.NewText("", theme.Color(theme.ColorNameForeground))
clock.TextSize = 28
go func() {
for t := range time.Tick(time.Second) {
// clock.Text is a plain field. Changing it and refreshing must
// happen on the main goroutine, so hand the work to bellui.Do.
bellui.Do(func() {
clock.Text = t.Format("15:04:05")
clock.Refresh()
})
}
}()clock.Text is a plain struct field, and Refresh redraws the object. Both
must run on the main goroutine, so they are wrapped in bellui.Do.
The whole program:
// A clock updated from a background goroutine, and a button that starts slow
// work without freezing the window.
package main
import (
"time"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/canvas"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/theme"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.NewWithID("com.example.thread")
w := a.NewWindow("UI thread")
clock := canvas.NewText("", theme.Color(theme.ColorNameForeground))
clock.TextSize = 28
go func() {
for t := range time.Tick(time.Second) {
// clock.Text is a plain field. Changing it and refreshing must
// happen on the main goroutine, so hand the work to bellui.Do.
bellui.Do(func() {
clock.Text = t.Format("15:04:05")
clock.Refresh()
})
}
}()
status := widget.NewLabel("Idle")
var start *widget.Button
start = widget.NewButton("Start slow work", func() {
// This callback runs on the main goroutine. Blocking here would
// freeze the window, so the work moves to its own goroutine.
start.Disable()
status.SetText("Working...")
go func() {
time.Sleep(3 * time.Second) // stands in for real work
bellui.Do(func() {
status.SetText("Done")
start.Enable()
})
}()
})
w.SetContent(container.NewVBox(container.NewCenter(clock), status, start))
bellui.ShowAndRun(w)
}The functions
| Function | What it does |
|---|---|
bellui.Do(fn) | Queues fn to run on the main goroutine and returns at once. |
bellui.DoAndWait(fn) | Runs fn on the main goroutine and waits until it has finished. Use it when you need a result back, or need the change made before you go on. |
bellui.RunOnMain(fn) | Runs fn straight away if the caller is already on the main goroutine; otherwise works like DoAndWait. |
bellui.IsMainGoroutine() | Reports whether the caller is on the main goroutine. |
Do and DoAndWait are for goroutines you created. Do not call DoAndWait
from the main goroutine, for example from inside a callback. By default it
then logs an error and runs your function on a new goroutine, not on the main
one. If a
function can be reached both from callbacks and from background goroutines,
use RunOnMain. It is safe from either.
What is already safe
Some widget setters move their work to the main goroutine for you, using
RunOnMain. Examples are Label.SetText, Container.Add, and
Enable and Disable on widgets that can be disabled. You can call those
from any goroutine.
Do not rely on that for everything:
- Assigning to a field (
label.Text = "...",rect.FillColor = ...) is never marshalled. Neither is theRefreshyou call afterwards. - Methods on canvas primitives are not marshalled.
- Not every widget method is marshalled.
Container.Removeis not, for example.
When in doubt, wrap the change in bellui.Do.
OnTapped, OnChanged
and OnSubmitted on widgets, and the event methods in
Input events, already run on the main goroutine. They do
not need bellui.Do.