~eliasnaur/gio-example

c4d96091d2a543cc24c28c7bff73e05361f5dcbc — Egon Elbre 7 months ago 5eab227
7gui/timer: add timer example

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
4 files changed, 314 insertions(+), 3 deletions(-)

M 7gui/README.md
A 7gui/timer/main.go
A 7gui/timer/timer.go
M go.mod
M 7gui/README.md => 7gui/README.md +16 -3
@@ 1,6 1,8 @@
# 7 GUIs

This demonstrates several classic GUI problems based on [7GUIs](https://eugenkiss.github.io/7guis/).
This demonstrates several classic GUI problems based on [7GUIs](https://eugenkiss.github.io/7guis/). They show different ways of using Gio framework.

The examples show one way of implementing of things, of course, there are many more.

The examples are over-commented to help understand the structure better, in practice, you don't need that many comments.



@@ 10,7 12,7 @@ Counter shows basic usage of Gio and how to write interactions.

It displays a count value that increases when you press a button.

[Source](./counter/main.go)
[UI](./counter/main.go)

## Temperature Converter



@@ 18,4 20,15 @@ Temperature conversion shows bidirectional data flow between two editable fields

It implements a bordered field that can be used to propagate values back to another field without causing update loops.

[Source](./temperature/main.go)
\ No newline at end of file
[UI](./temperature/main.go)


## Timer

Timer shows how to react to external signals.

It implements a timer that is running in a separate goroutine and the UI interacts with it. The same effect can be implemented in shorter ways without goroutines, however it nicely demonstrates how you would interact with information that comes in asynchronously.

The UI shows a slider to change the duration of the timer and there is a button to reset the counter.

[UI](./timer/main.go), [Timer](./timer/timer.go)
\ No newline at end of file

A 7gui/timer/main.go => 7gui/timer/main.go +155 -0
@@ 0,0 1,155 @@
package main

import (
	"log"
	"os"
	"time"

	"gioui.org/app"             // app contains Window handling.
	"gioui.org/font/gofont"     // gofont is used for loading the default font.
	"gioui.org/io/key"          // key is used for keyboard events.
	"gioui.org/io/system"       // system is used for system events (e.g. closing the window).
	"gioui.org/layout"          // layout is used for layouting widgets.
	"gioui.org/op"              // op is used for recording different operations.
	"gioui.org/unit"            // unit is used to define pixel-independent sizes
	"gioui.org/widget"          // widget contains state handling for widgets.
	"gioui.org/widget/material" // material contains material design widgets.
)

func main() {
	// The ui loop is separated from the application window creation
	// such that it can be used for testing.
	ui := NewUI()

	// This creates a new application window and starts the UI.
	go func() {
		w := app.NewWindow(
			app.Title("Timer"),
			app.Size(unit.Dp(360), unit.Dp(360)),
		)
		if err := ui.Run(w); err != nil {
			log.Println(err)
			os.Exit(1)
		}
		os.Exit(0)
	}()

	// This starts Gio main.
	app.Main()
}

// defaultMargin is a margin applied in multiple places to give
// widgets room to breathe.
var defaultMargin = unit.Dp(10)

// UI holds all of the application state.
type UI struct {
	// Theme is used to hold the fonts used throughout the application.
	Theme *material.Theme

	Timer *Timer

	duration widget.Float
	reset    widget.Clickable
}

// NewUI creates a new UI using the Go Fonts.
func NewUI() *UI {
	ui := &UI{}
	ui.Theme = material.NewTheme(gofont.Collection())

	// start with reasonable defaults.
	ui.Timer = NewTimer(5 * time.Second)
	ui.duration.Value = 5

	return ui
}

// Run handles window events and renders the application.
func (ui *UI) Run(w *app.Window) error {

	// start the timer goroutine and ensure it's closed
	// when the application closes.
	closeTimer := ui.Timer.Start()
	defer closeTimer()

	var ops op.Ops
	for {
		select {
		// when the timer is updated we should update the screen.
		case <-ui.Timer.Updated:
			w.Invalidate()

		case e := <-w.Events():
			// detect the type of the event.
			switch e := e.(type) {
			// this is sent when the application should re-render.
			case system.FrameEvent:
				// gtx is used to pass around rendering and event information.
				gtx := layout.NewContext(&ops, e)
				// render and handle UI.
				ui.Layout(gtx)
				// render and handle the operations from the UI.
				e.Frame(gtx.Ops)

			// handle a global key press.
			case key.Event:
				switch e.Name {
				// when we click escape, let's close the window.
				case key.NameEscape:
					return nil
				}

			// this is sent when the application is closed.
			case system.DestroyEvent:
				return e.Err
			}
		}
	}

	return nil
}

// Layout displays the main program layout.
func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
	th := ui.Theme

	// check whether the reset button was clicked.
	if ui.reset.Clicked() {
		ui.Timer.Reset()
	}
	// check whether the slider value has changed.
	if ui.duration.Changed() {
		ui.Timer.SetDuration(secondsToDuration(float64(ui.duration.Value)))
	}

	// get the latest information about the timer.
	info := ui.Timer.Info()
	progress := 0
	if info.Duration == 0 {
		progress = 100
	} else {
		progress = int(info.Progress * 100 / info.Duration)
	}

	// inset is used to add padding around the window border.
	inset := layout.UniformInset(defaultMargin)
	return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
			layout.Rigid(material.Body1(th, "Elapsed Time").Layout),
			layout.Rigid(material.ProgressBar(th, progress).Layout),
			layout.Rigid(material.Body1(th, info.ProgressString()).Layout),

			layout.Rigid(layout.Spacer{Height: th.TextSize}.Layout),
			layout.Rigid(material.Body1(th, "Duration").Layout),
			layout.Rigid(material.Slider(th, &ui.duration, 0, 15).Layout),

			layout.Rigid(layout.Spacer{Height: th.TextSize}.Layout),
			layout.Rigid(material.Button(th, &ui.reset, "Reset").Layout),
		)
	})
}

func secondsToDuration(s float64) time.Duration {
	return time.Duration(s * float64(time.Second))
}

A 7gui/timer/timer.go => 7gui/timer/timer.go +142 -0
@@ 0,0 1,142 @@
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

// Timer implements an
type Timer struct {
	// Updated is used to notify UI about changes in the timer.
	Updated chan struct{}

	// mu locks the state such that it can be modified and accessed
	// from multiple goroutines.
	mu       sync.Mutex
	start    time.Time     // start corresponds to when the timer was started.
	now      time.Time     // now corresponds to the last updated time.
	duration time.Duration // duration is the maximum progress.
}

// NewTimer creates a new timer with the specified timer.
func NewTimer(initialDuration time.Duration) *Timer {
	return &Timer{
		Updated:  make(chan struct{}),
		duration: initialDuration,
	}
}

// Start the timer goroutine and return a cancel func that
// that can be used to stop it.
func (t *Timer) Start() context.CancelFunc {
	// initialize the timer state.
	now := time.Now()
	t.now = now
	t.start = now

	// we use done to signal stopping the goroutine.
	// a context.Context could be also used.
	done := make(chan struct{})
	go t.run(done)
	return func() { close(done) }
}

// run is the main loop for the timer.
func (t *Timer) run(done chan struct{}) {
	// we use a time.Ticker to update the state,
	// in many cases, this could be a network access instead.
	tick := time.NewTicker(50 * time.Millisecond)
	defer tick.Stop()

	for {
		select {
		case now := <-tick.C:
			t.update(now)
		case <-done:
			return
		}
	}
}

// invalidate sends a signal to the UI that
// the internal state has changed.
func (t *Timer) invalidate() {
	// we use a non-blocking send, that way the Timer
	// can continue updating internally.
	select {
	case t.Updated <- struct{}{}:
	default:
	}
}

func (t *Timer) update(now time.Time) {
	t.mu.Lock()
	defer t.mu.Unlock()

	previousNow := t.now
	t.now = now

	// first check whether we have not exceeded the duration.
	// in that case the progress advanced and we need to notify
	// about a change.
	progressAfter := t.now.Sub(t.start)
	if progressAfter <= t.duration {
		t.invalidate()
		return
	}

	// when we had progressed beyond the duration we also
	// need to update the first time it happens.
	progressBefore := previousNow.Sub(t.start)
	if progressBefore <= t.duration {
		t.invalidate()
		return
	}
}

// Reset resets timer to the last know time.
func (t *Timer) Reset() {
	t.mu.Lock()
	defer t.mu.Unlock()

	t.start = t.now
	t.invalidate()
}

// SetDuration changes the duration of the timer.
func (t *Timer) SetDuration(duration time.Duration) {
	t.mu.Lock()
	defer t.mu.Unlock()

	if t.duration == duration {
		return
	}
	t.duration = duration
	t.invalidate()
}

// Info returns the latest know info about the timer.
func (t *Timer) Info() (info Info) {
	t.mu.Lock()
	defer t.mu.Unlock()

	info.Progress = t.now.Sub(t.start)
	info.Duration = t.duration
	if info.Progress > info.Duration {
		info.Progress = info.Duration
	}
	return info
}

// Info is the information about the timer.
type Info struct {
	Progress time.Duration
	Duration time.Duration
}

// ProgressString returns the progress formatted as seconds.
func (info *Info) ProgressString() string {
	return fmt.Sprintf("%.1fs", info.Progress.Seconds())
}

M go.mod => go.mod +1 -0
@@ 10,4 10,5 @@ require (
	golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
	golang.org/x/image v0.0.0-20200618115811-c13761719519
	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
	golang.org/x/sync v0.0.0-20190423024810-112230192c58
)