Chapter 7 - Progressbar

Updated February 25th 2024

Goals

The intent of this section is to add a progressbar

Progressbar

Outline

I’ve looked forward to this chapter ever since I started writing this series. We will cover quite some ground and introduce multiple new ideas:

  • Try out a new widget, the material.Progressbar
  • Start using state variables to control behaviour
  • Use a concurrency technique to create and share a beating pulse that progresses the progressbar

Let’s look at these in turn pieces.

Feature 1 - The progressbar

A progressbar is obviously a bar that displays progress. But which progress? And how to control it? How fast should it grow, can it pause, or even reverse? From the docs we find ProgressBar(th *Theme, progress float32) receives progress as a decimal between 0 and 1.

Code

We start by definding two variables for progress. The first is simply the progress as a number. We also define a channel used to send progress information through, which we’ll look closer at later. Both are defined at root level, outside main, so that they are once and we have access to them throughout the whole program:

var progress float32
var progressIncrementer chan float32

A bit below we’ll see how these are updated, but first let’s take a look at how progress is used when laying out the progressbar. To to that we turn to our sturdy Flexbox and insert the bar through a rigid:

// Inside System.FrameEvent
layout.Flex{
  // ...
}.Layout(gtx,
  layout.Rigid(
    func(gtx C) D {
      bar := material.ProgressBar(th, progress)  // Here progress is used
      return bar.Layout(gtx)
    },
  ),

Notice how the widget itself has no state. State is maintained in the rest of the program, the widget only knows how to display the progress we send it. Any logic to increase, pause, reverse or reset we control outside the widget.

Feature 2 - State variables

We mentioned progress, the variable that contains progress state. Another useful state to track is whether or not the start button has been clicked. In our app that means tracking if the egg has started to boil:

Code

// is the egg boiling?
var boiling bool

We want to flip that boolean when the start button is clicked. Thus we listen for a app.FrameEvent from the GUI and check if startButton.Clicked() is true:

case system.FrameEvent:
  gtx := layout.NewContext(&ops, e)
  // Let's try out the flexbox layout concept
  if startButton.Clicked(gtx) {
    boiling = !boiling
  }

Again, the only job of the button is shout out if it recently was clicked. Beyond that, the rest of the program takes care of any actions that needs to be taken.

One such example is what the text on the button should be. We decide that before calling the material.Button( ) function by first checking what the state of boiling is.

// ...the same function we earlier used to create a button
func(gtx C) D {
  var text string
  if !boiling {
    text = "Start"
  } else {
    text = "Stop"
  }
  btn := material.Button(th, &startButton, text)
  return btn.Layout(gtx)
},

Feature 3 - A beating pulse

A good progressbar must grow smoothly and precisely. To achieve that, we first create a separate go-routine that beats with a steady pulse. Later we listen for events, and pick up on these beats to grow the bar.

Code

Here’s the code, first the tick-generator:

func main() {
  // Setup a separate channel to provide ticks to increment progress
  progressIncrementer = make(chan float32)
  go func() {
    for {
      time.Sleep(time.Second / 25)
      progressIncrementer <- 0.004
    }
  }()

  // ...

progressIncrementer is the channel into which we send values, in this case of type float32.

Again, this is done in an anonymous function, called when our program starts, meaning this for-loop spins for the entirety of the program. Every 1/25th of a second the number 0.004 is injected into the channel.

Later we pick up from the channel, with this code inside draw( ):

// .. inside draw()
// listen for events in the incrementer channel
go func() {
    for p := range progressIncrementer {
      if boiling && progress < 1 {
        progress += p
        w.Invalidate()
      }
    }
}()

Here we start up an anonymous function to listen to the progressIncrementer channel. This is a concurrency feature of Go. Create a listener and let it do its thing. For us, that thing is to add p to progress if the control variable boiling is true, and progress is less than 1. Since p is 0.004, and progress increased 25 times per second, it will take 10 seconds to reach 1. Feel free to adjust either of these two to find a combination of speed and smoothness that works for you.

Finally we force the window to draw, by calling w.Invalidate(). What is does is to inform Gio that the old rendering is now, well, invalid, and hence a new drawing must be made. Without such notice, Gio would simply not update until forced to do so by a mouse click or button press or other events. Invalidating at every frame though can be costly, and alternatives exists. It’s a bit of an advanced topic though, so for now let’s leave it as is, but return to it in the Bonus chapter on improved animation.

By using a channel like this we get

  1. Precise timing, where we control the execution exactly as we want it
  2. Consistent timing, similar across fast and slow hardware
  3. Concurrent timing, the rest of the application continues as before

While all of these make sense, the 2nd point deserves some extra attention. If you recompile the app without the time.Sleep(time.Second / 25), your machine will work hard to render as quickly as possible. That can consume a lot of cpu resources, which in turn can drain battery as well. It also ensures consistency across devices, all run at the same pulse. As an example, pprof’s from 3 different machines are included in the code folder. These include a 1/25th sleep, ensuring the same end result. Please have a look.

Comments

By combining all these building blocks we now have a stateful program we can control with ease. The user interface tells us when something happens, and the rest of the program uses that to take care of business. We had to pull a few tricks out of the bag, including creating, writing and listening to a channel. Now that we’re true tricksters, let’s add some custom graphics in the next chapter.


Next chapter View it on GitHub