2 Egg Timer

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