~eliasnaur/gio-example

ref: 1ad8d13d0f99c508d3a2528dfe349f3313acc7c5 gio-example/7gui/temperature/main.go -rw-r--r-- 5.8 KiB
1ad8d13dChris Waldon windows,multiwindow: rename package for clarity 6 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package main

import (
	"image/color"
	"log"
	"os"
	"strconv"

	"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("Temperature Converter"),
			app.Size(unit.Dp(360), unit.Dp(47)),
		)
		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

	// Converter displays and modifies the state.
	Converter Converter
}

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

	ui.Converter.Init()
	return ui
}

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

	// listen for events happening on the window.
	for e := range 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 {
	// 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 ui.Converter.Layout(ui.Theme, gtx)
	})
}

// Converter is a component that keeps track of it's state and
// displays itself as two editors.
type Converter struct {
	Celsius    Field
	Fahrenheit Field
}

// Init is used to set the inital state.
func (conv *Converter) Init() {
	conv.Celsius.SingleLine = true
	conv.Fahrenheit.SingleLine = true
}

// Layout lays out the editors.
func (conv *Converter) Layout(th *material.Theme, gtx layout.Context) layout.Dimensions {
	// We use an empty widget to add spacing between widgets.
	spacer := layout.Rigid(layout.Spacer{Width: defaultMargin}.Layout)

	// check whether the celsius value has changed.
	if conv.Celsius.Changed() {
		// try to convert the value to an integer
		newValue, err := strconv.Atoi(conv.Celsius.Text())
		// update whether the editor is displaying a valid value
		conv.Celsius.Invalid = err != nil
		if !conv.Celsius.Invalid {
			// update the other editor when it's valid
			conv.Fahrenheit.Invalid = false
			conv.Fahrenheit.SetText(strconv.Itoa(newValue*9/5 + 32))
		}
	}

	// check whether the fahrenheit value has changed.
	if conv.Fahrenheit.Changed() {
		newValue, err := strconv.Atoi(conv.Fahrenheit.Text())
		conv.Fahrenheit.Invalid = err != nil
		if !conv.Fahrenheit.Invalid {
			conv.Celsius.Invalid = false
			conv.Celsius.SetText(strconv.Itoa((newValue - 32) * 5 / 9))
		}
	}

	// TODO: use proper baseline alignment.
	return layout.Flex{}.Layout(gtx,
		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
			return conv.Celsius.Layout(th, gtx)
		}),
		spacer,
		layout.Rigid(material.Body1(th, "Celsius").Layout),
		spacer,
		layout.Rigid(material.Body1(th, "=").Layout),
		spacer,
		layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
			return conv.Fahrenheit.Layout(th, gtx)
		}),
		spacer,
		layout.Rigid(material.Body1(th, "Fahrenheit").Layout),
	)
}

// Field implements an editor that allows updating the state and detect
// changes to the field from other sources.
type Field struct {
	widget.Editor
	Invalid bool

	old string
}

// Changed checks once whether the editor context has changed.
func (ed *Field) Changed() bool {
	newText := ed.Editor.Text()
	changed := newText != ed.old
	ed.old = newText
	return changed
}

// SetText sets editor content without marking the editor changed.
func (ed *Field) SetText(s string) {
	ed.old = s
	ed.Editor.SetText(s)
}

// Layout handles the editor with the appropriate color and border.
func (ed *Field) Layout(th *material.Theme, gtx layout.Context) layout.Dimensions {
	// Determine colors based on the state of the editor.
	borderWidth := float32(0.5)
	borderColor := color.NRGBA{A: 107}
	switch {
	case ed.Editor.Focused():
		borderColor = th.Palette.ContrastBg
		borderWidth = 2
	case ed.Invalid:
		borderColor = color.NRGBA{R: 200, A: 0xFF}
	}

	// draw an editor with a border.
	return widget.Border{
		Color:        borderColor,
		CornerRadius: unit.Dp(4),
		Width:        unit.Dp(borderWidth),
	}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return layout.UniformInset(unit.Dp(4)).Layout(gtx,
			material.Editor(th, &ed.Editor, "").Layout)
	})
}