Egg timer#
After the 0-hello-world and 1-hello-world , now we are going to check Jon Strand’s tutorial “Implementing an egg timer”, try to play with it, then we will complete this chapter and move on to other examples.
» Jon Strand’s Tutorial: Implementing an egg timer
Egg timer codes#
Here are the final code of each chapter:
(Please visit Jon Strand’s page and go through it first, because it has many details that shouldn’t be missed.)
Chapter 1 - Window
package main
import "gioui.org/app"
func main() {
go func() {
w := app.NewWindow()
for {
w.NextEvent()
}
}()
app.Main()
}
Chapter 2 - Title
package main
import (
"gioui.org/app"
"gioui.org/unit"
)
func main() {
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
for {
w.NextEvent()
}
}()
app.Main()
}
Chapter 3 - Button
package main
import (
"os"
"gioui.org/app"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
func main() {
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
btn := material.Button(th, &startButton, "Start")
btn.Layout(gtx)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}()
app.Main()
}
Chapter 4 - Layout
package main
import (
"os"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
func main() {
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
},
),
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: unit.Dp(25)}.Layout(gtx)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}()
app.Main()
}
Chapter 5 - Refactoring
package main
import (
"log"
"os"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
C = layout.Context
D = layout.Dimensions
)
func main() {
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func draw(w *app.Window) error {
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx C) D {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
},
),
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Height: unit.Dp(25)}.Layout(gtx)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}
Chapter 6 - Margin
package main
import (
"log"
"os"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
C = layout.Context
D = layout.Dimensions
)
func main() {
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func draw(w *app.Window) error {
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx C) D {
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margins.Layout(gtx,
func(gtx C) D {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}
Chapter 7 - Progressbar
package main
import (
"log"
"os"
"time"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
C = layout.Context
D = layout.Dimensions
)
var progress float32
var progressIncrementer chan float32
var boiling bool
func main() {
progressIncrementer = make(chan float32)
go func() {
for {
time.Sleep(time.Second / 25)
progressIncrementer <- 0.004
}
}()
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func draw(w *app.Window) error {
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
go func() {
for p := range progressIncrementer {
if boiling && progress < 1 {
progress += p
w.Invalidate()
}
}
}()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
if startButton.Clicked(gtx) {
boiling = !boiling
}
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx C) D {
bar := material.ProgressBar(th, progress)
return bar.Layout(gtx)
},
),
layout.Rigid(
func(gtx C) D {
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margins.Layout(gtx,
func(gtx C) D {
var text string
if !boiling {
text = "Start"
} else {
text = "Stop"
}
btn := material.Button(th, &startButton, text)
return btn.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}
Chapter 8 - Circle
package main
import (
"image"
"image/color"
"log"
"os"
"time"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
C = layout.Context
D = layout.Dimensions
)
var progress float32
var progressIncrementer chan float32
var boiling bool
func main() {
progressIncrementer = make(chan float32)
go func() {
for {
time.Sleep(time.Second / 25)
progressIncrementer <- 0.004
}
}()
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func draw(w *app.Window) error {
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
go func() {
for p := range progressIncrementer {
if boiling && progress < 1 {
progress += p
w.Invalidate()
}
}
}()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
if startButton.Clicked(gtx) {
boiling = !boiling
}
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx C) D {
circle := clip.Ellipse{
Min: image.Pt(80, 0),
Max: image.Pt(320, 240),
}.Op(gtx.Ops)
color := color.NRGBA{R: 200, A: 255}
paint.FillShape(gtx.Ops, color, circle)
d := image.Point{Y: 400}
return layout.Dimensions{Size: d}
},
),
layout.Rigid(
func(gtx C) D {
bar := material.ProgressBar(th, progress)
return bar.Layout(gtx)
},
),
layout.Rigid(
func(gtx C) D {
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margins.Layout(gtx,
func(gtx C) D {
var text string
if !boiling {
text = "Start"
} else {
text = "Stop"
}
btn := material.Button(th, &startButton, text)
return btn.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}
Chapter 9 - Egg
package main
import (
"image"
"image/color"
"log"
"math"
"os"
"time"
"gioui.org/app"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
type (
C = layout.Context
D = layout.Dimensions
)
var progress float32
var progressIncrementer chan float32
var boiling bool
func main() {
progressIncrementer = make(chan float32)
go func() {
for {
time.Sleep(time.Second / 25)
progressIncrementer <- 0.004
}
}()
go func() {
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
func draw(w *app.Window) error {
var ops op.Ops
var startButton widget.Clickable
th := material.NewTheme()
go func() {
for p := range progressIncrementer {
if boiling && progress < 1 {
progress += p
w.Invalidate()
}
}
}()
for {
switch e := w.NextEvent().(type) {
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
if startButton.Clicked(gtx) {
boiling = !boiling
}
layout.Flex{
Axis: layout.Vertical,
Spacing: layout.SpaceStart,
}.Layout(gtx,
layout.Rigid(
func(gtx C) D {
// Draw a custom path, shaped like an egg
var eggPath clip.Path
op.Offset(image.Pt(gtx.Dp(200), gtx.Dp(150))).Add(gtx.Ops)
eggPath.Begin(gtx.Ops)
// Rotate from 0 to 360 degrees
for deg := 0.0; deg <= 360; deg++ {
// Egg math (really) at this brilliant site. Thanks!
// https://observablehq.com/@toja/egg-curve
// Convert degrees to radians
rad := deg / 360 * 2 * math.Pi
// Trig gives the distance in X and Y direction
cosT := math.Cos(rad)
sinT := math.Sin(rad)
// Constants to define the eggshape
a := 110.0
b := 150.0
d := 20.0
// The x/y coordinates
x := a * cosT
y := -(math.Sqrt(b*b-d*d*cosT*cosT) + d*sinT) * sinT
// Finally the point on the outline
p := f32.Pt(float32(x), float32(y))
// Draw the line to this point
eggPath.LineTo(p)
}
// Close the path
eggPath.Close()
// Get hold of the actual clip
eggArea := clip.Outline{Path: eggPath.End()}.Op()
// Fill the shape
// color := color.NRGBA{R: 255, G: 239, B: 174, A: 255}
color := color.NRGBA{R: 255, G: uint8(239 * (1 - progress)), B: uint8(174 * (1 - progress)), A: 255}
paint.FillShape(gtx.Ops, color, eggArea)
d := image.Point{Y: 375}
return layout.Dimensions{Size: d}
},
),
layout.Rigid(
func(gtx C) D {
bar := material.ProgressBar(th, progress)
return bar.Layout(gtx)
},
),
layout.Rigid(
func(gtx C) D {
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
return margins.Layout(gtx,
func(gtx C) D {
var text string
if !boiling {
text = "Start"
} else {
text = "Stop"
}
btn := material.Button(th, &startButton, text)
return btn.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
case app.DestroyEvent:
os.Exit(0)
}
}
}
Chapter 10 - Input
In this step, the progressIncrementer been changed to chan bool, from chan float32, please be noted.
package main
import (
"fmt"
"image"
"image/color"
"log"
"math"
"os"
"strconv"
"strings"
"time"
"gioui.org/app"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
// Define the progress variables, a channel and a variable
var progressIncrementer chan bool
var progress float32
func main() {
// Setup a separate channel to provide ticks to increment progress
progressIncrementer = make(chan bool)
go func() {
for {
time.Sleep(time.Second / 25)
progressIncrementer <- true
}
}()
go func() {
// create new window
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
type C = layout.Context
type D = layout.Dimensions
func draw(w *app.Window) error {
// ops are the operations from the UI
var ops op.Ops
// startButton is a clickable widget
var startButton widget.Clickable
// boilDurationInput is a textfield to input boil duration
var boilDurationInput widget.Editor
// is the egg boiling?
var boiling bool
var boilDuration float32
// th defines the material design style
th := material.NewTheme()
// listen for events in the incrementor channel
go func() {
for range progressIncrementer {
if boiling && progress < 1 {
progress += 1.0 / 25.0 / boilDuration
if progress >= 1 {
progress = 1
}
w.Invalidate()
}
}
}()
for {
// listen for events in the window.
switch e := w.NextEvent().(type) {
// this is sent when the application should re-render.
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
// Let's try out the flexbox layout concept
if startButton.Clicked(gtx) {
// Start (or stop) the boil
boiling = !boiling
// Resetting the boil
if progress >= 1 {
progress = 0
}
// Read from the input
inputString := boilDurationInput.Text()
inputString = strings.TrimSpace(inputString)
inputFloat, _ := strconv.ParseFloat(inputString, 32)
boilDuration = float32(inputFloat)
boilDuration = boilDuration / (1 - progress)
}
layout.Flex{
// Vertical alignment, from top to bottom
Axis: layout.Vertical,
// Empty space is left at the start, i.e. at the top
Spacing: layout.SpaceStart,
}.Layout(
gtx,
// The egg
layout.Rigid(
func(gtx C) D {
// Draw a custom path, shaped like an egg
var eggPath clip.Path
op.Offset(image.Pt(gtx.Dp(200), gtx.Dp(150))).Add(gtx.Ops)
eggPath.Begin(gtx.Ops)
// rotate from zero to 360 deg
for deg := 0.0; deg <= 360; deg++ {
// covert degrees to radians
rad := deg / 360 * 2 * math.Pi
// trigger gives the distance in X and Y direction
cosT := math.Cos(rad)
sinT := math.Sin(rad)
// constants to define the eggshapes
a := 110.0
b := 150.0
d := 20.0
// the x/y coordinate
x := a * cosT
y := -(math.Sqrt(b*b-d*d*cosT*cosT) + d*sinT) * sinT
// finally, the point on the outline
p := f32.Pt(float32(x), float32(y))
// draw the line to this point
eggPath.LineTo(p)
}
// close the path
eggPath.Close()
// get the hold of the actual clip
eggArea := clip.Outline{Path: eggPath.End()}.Op()
// fill the shape
color := color.NRGBA{
R: 255,
G: uint8(239 * (1 - progress)),
B: uint8(174 * (1 - progress)),
A: 255,
}
paint.FillShape(gtx.Ops, color, eggArea)
d := image.Point{Y: 375}
return layout.Dimensions{Size: d}
},
),
// The inputbox
layout.Rigid(
func(gtx C) D {
// Wrap the editor in material design
ed := material.Editor(th, &boilDurationInput, "sec")
// Define characteristics of the input box
boilDurationInput.SingleLine = true
boilDurationInput.Alignment = text.Middle
// Count down the text when boiling
if boiling && progress < 1 {
boilRemain := (1 - progress) * boilDuration
inputStr := fmt.Sprintf("%.1f", math.Round(float64(boilRemain)*10)/10)
boilDurationInput.SetText(inputStr)
}
// Define insets ...
margins := layout.Inset{
Top: unit.Dp(0),
Right: unit.Dp(170),
Bottom: unit.Dp(40),
Left: unit.Dp(179),
}
// ... and borders ...
border := widget.Border{
Color: color.NRGBA{R: 204, G: 204, B: 204, A: 255},
CornerRadius: unit.Dp(3),
Width: unit.Dp(2),
}
// ... before laying it out, one inside the other
return margins.Layout(gtx,
func(gtx C) D {
return border.Layout(gtx, ed.Layout)
},
)
},
),
// The progressbar
layout.Rigid(
func(gtx C) D {
bar := material.ProgressBar(th, progress)
return bar.Layout(gtx)
},
),
// The button
layout.Rigid(
func(gtx C) D {
// We start by defining a set of margins
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
// Then we lay out within those margins
return margins.Layout(gtx,
func(gtx C) D {
// The text on the button depends on program state
var text string
if !boiling {
text = "Start"
}
if boiling && progress < 1 {
text = "Stop"
}
if boiling && progress >= 1 {
text = "Finished"
}
newbutton := material.Button(th, &startButton, text)
return newbutton.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
// this is sent when the application is closed.
case app.DestroyEvent:
return e.Err
}
}
}
Bonus - Improved animation
TODO: the Finished state of progress need to be corrected with the general pattern.
package main
import (
"fmt"
"image"
"image/color"
"log"
"math"
"os"
"strconv"
"strings"
"time"
"gioui.org/app"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
)
var progress float32
func main() {
go func() {
// create new window
w := app.NewWindow(
app.Title("Egg Timer"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
if err := draw(w); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}
type C = layout.Context
type D = layout.Dimensions
// animation tracks the progress of a linear animation across multiple frames.
type animation struct {
start time.Time
duration time.Duration
}
var anim animation
// animate starts an animation at the current frame which will last for the provided duration.
func (a *animation) animate(gtx layout.Context, duration time.Duration) {
a.start = gtx.Now
a.duration = duration
// op.InvalidateOp{}.Add(gtx.Ops)
// ref: https://github.com/gioui/gio/commit/c515b7804e13fdab7b0cdb96f51def5f6c966730
gtx.Execute(op.InvalidateCmd{})
}
// stop ends the animation immediately.
func (a *animation) stop() {
a.duration = time.Duration(0)
}
// progress returns whether the animation is currently running and (if so) how far through the animation it is.
func (a animation) progress(gtx layout.Context) (animating bool, progress float32) {
if gtx.Now.After(a.start.Add(a.duration)) {
return false, 0
}
// op.InvalidateOp{}.Add(gtx.Ops)
// https://github.com/gioui/gio/commit/c515b7804e13fdab7b0cdb96f51def5f6c966730
gtx.Execute(op.InvalidateCmd{})
return true, float32(gtx.Now.Sub(a.start)) / float32(a.duration)
}
func draw(w *app.Window) error {
// ops are the operations from the UI
var ops op.Ops
// startButton is a clickable widget
var startButton widget.Clickable
// boilDurationInput is a textfield to input boil duration
var boilDurationInput widget.Editor
// th defines the material design style
th := material.NewTheme()
for {
// listen for events in the window.
switch e := w.NextEvent().(type) {
// this is sent when the application should re-render.
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
boiling, progress := anim.progress(gtx)
// Let's try out the flexbox layout concept
if startButton.Clicked(gtx) {
// Start (or stop) the boil
if boiling {
anim.stop()
} else {
// Read from the input
inputString := boilDurationInput.Text()
inputString = strings.TrimSpace(inputString)
inputFloat, _ := strconv.ParseFloat(inputString, 32)
anim.animate(gtx, time.Duration(inputFloat)*time.Second)
}
}
layout.Flex{
// Vertical alignment, from top to bottom
Axis: layout.Vertical,
// Empty space is left at the start, i.e. at the top
Spacing: layout.SpaceStart,
}.Layout(
gtx,
// The egg
layout.Rigid(
func(gtx C) D {
// Draw a custom path, shaped like an egg
var eggPath clip.Path
op.Offset(image.Pt(gtx.Dp(200), gtx.Dp(150))).Add(gtx.Ops)
eggPath.Begin(gtx.Ops)
// rotate from zero to 360 deg
for deg := 0.0; deg <= 360; deg++ {
// covert degrees to radians
rad := deg / 360 * 2 * math.Pi
// trigger gives the distance in X and Y direction
cosT := math.Cos(rad)
sinT := math.Sin(rad)
// constants to define the eggshapes
a := 110.0
b := 150.0
d := 20.0
// the x/y coordinate
x := a * cosT
y := -(math.Sqrt(b*b-d*d*cosT*cosT) + d*sinT) * sinT
// finally, the point on the outline
p := f32.Pt(float32(x), float32(y))
// draw the line to this point
eggPath.LineTo(p)
}
// close the path
eggPath.Close()
// get the hold of the actual clip
eggArea := clip.Outline{Path: eggPath.End()}.Op()
// fill the shape
color := color.NRGBA{
R: 255,
G: uint8(239 * (1 - progress)),
B: uint8(174 * (1 - progress)),
A: 255,
}
paint.FillShape(gtx.Ops, color, eggArea)
d := image.Point{Y: 375}
return layout.Dimensions{Size: d}
},
),
// The inputbox
layout.Rigid(
func(gtx C) D {
// Wrap the editor in material design
ed := material.Editor(th, &boilDurationInput, "sec")
// Define characteristics of the input box
boilDurationInput.SingleLine = true
boilDurationInput.Alignment = text.Middle
// Count down the text when boiling
if boiling && progress < 1 {
boilRemain := (1 - progress) * float32(anim.duration.Seconds())
inputStr := fmt.Sprintf("%.1f", math.Round(float64(boilRemain)*10)/10)
boilDurationInput.SetText(inputStr)
}
// Define insets ...
margins := layout.Inset{
Top: unit.Dp(0),
Right: unit.Dp(170),
Bottom: unit.Dp(40),
Left: unit.Dp(179),
}
// ... and borders ...
border := widget.Border{
Color: color.NRGBA{R: 204, G: 204, B: 204, A: 255},
CornerRadius: unit.Dp(3),
Width: unit.Dp(2),
}
// ... before laying it out, one inside the other
return margins.Layout(gtx,
func(gtx C) D {
return border.Layout(gtx, ed.Layout)
},
)
},
),
// The progressbar
layout.Rigid(
func(gtx C) D {
bar := material.ProgressBar(th, progress)
return bar.Layout(gtx)
},
),
// The button
layout.Rigid(
func(gtx C) D {
// We start by defining a set of margins
margins := layout.Inset{
Top: unit.Dp(25),
Bottom: unit.Dp(25),
Right: unit.Dp(35),
Left: unit.Dp(35),
}
// Then we lay out within those margins
return margins.Layout(gtx,
func(gtx C) D {
// The text on the button depends on program state
var text string
if !boiling {
text = "Start"
}
if boiling && progress < 1 {
text = "Stop"
}
if boiling && progress >= 1 {
text = "Finished"
}
newbutton := material.Button(th, &startButton, text)
return newbutton.Layout(gtx)
},
)
},
),
)
e.Frame(gtx.Ops)
// this is sent when the application is closed.
case app.DestroyEvent:
return e.Err
}
}
}
After finishing this wonderful tutorial, we may move further: what if …:
- I want the app to remember the boil duration time we last set
- (other requirements)
Teleprompter#
Next we will move on to Jon Strand’s » Teleprompter