├── .builds
└── linux.yml
├── 7gui
├── README.md
├── counter
│ └── main.go
├── temperature
│ └── main.go
└── timer
│ ├── main.go
│ └── timer.go
├── LICENSE
├── README.md
├── bidi
└── bidi.go
├── color-grid
└── colorgrid.go
├── colorpicker
└── main.go
├── component
├── applayout
│ └── applayout.go
├── icon
│ └── icons.go
├── main.go
└── pages
│ ├── about
│ └── about.go
│ ├── appbar
│ └── appbar.go
│ ├── discloser
│ └── discloser.go
│ ├── menu
│ └── menu.go
│ ├── navdrawer
│ └── navdrawer.go
│ ├── page.go
│ └── textfield
│ └── textfield.go
├── cursor_position
└── cursor_position.go
├── customdeco
└── main.go
├── explorer
└── main.go
├── fps-table
└── table.go
├── galaxy
├── galaxy.go
└── main.go
├── glfw
└── main.go
├── go.mod
├── go.sum
├── gophers
├── main.go
├── main_test.go
└── ui.go
├── haptic
├── example.go
├── example_android.go
└── example_nonandroid.go
├── hello
└── hello.go
├── kitchen
├── kitchen.go
└── main_test.go
├── life
├── board.go
├── main.go
└── style.go
├── markdown
└── main.go
├── multiwindow
├── letters.go
├── log.go
└── main.go
├── notify
├── build_macos.go
├── example.app
│ └── Contents
│ │ ├── Info.plist
│ │ └── PkgInfo
└── main.go
├── opacity
└── main.go
├── opengl
├── display_angle.go
├── egl_linux.go
├── main.go
├── view_darwin.go
└── view_windows.go
├── outlay
├── fan
│ ├── cribbage
│ │ ├── cmd
│ │ │ └── crib.go
│ │ └── cribbage.go
│ ├── main.go
│ ├── playing
│ │ └── card.go
│ └── widget
│ │ ├── boring
│ │ ├── cards.go
│ │ └── rect.go
│ │ └── state.go
└── grid
│ └── main.go
├── tabs
├── slider.go
└── tabs.go
├── textfeatures
└── main.go
└── tools.go
/.builds/linux.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Unlicense OR MIT
2 | image: debian/bookworm
3 | packages:
4 | - curl
5 | - pkg-config
6 | - libwayland-dev
7 | - libx11-dev
8 | - libx11-xcb-dev
9 | - libxkbcommon-dev
10 | - libxkbcommon-x11-dev
11 | - libgles2-mesa-dev
12 | - libegl1-mesa-dev
13 | - libffi-dev
14 | - libxcursor-dev
15 | - libxrandr-dev
16 | - libxinerama-dev
17 | - libxi-dev
18 | - libxxf86vm-dev
19 | - libvulkan-dev
20 | sources:
21 | - https://git.sr.ht/~eliasnaur/gio-example
22 | environment:
23 | PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin
24 | github_mirror: git@github.com:gioui/gio-example
25 | secrets:
26 | - c9ef426e-0c06-42b1-9ee8-d7b9aad02363
27 | tasks:
28 | - install_go: |
29 | mkdir -p /home/build/sdk
30 | cd /home/build/sdk
31 | curl -Lso go.tar.gz https://go.dev/dl/go1.24.2.linux-amd64.tar.gz
32 | echo "68097bd680839cbc9d464a0edce4f7c333975e27a90246890e9f1078c7e702ad go.tar.gz" | sha256sum -c -
33 | tar xzf go.tar.gz
34 | - test_example: |
35 | cd gio-example
36 | go test -race ./...
37 | - check_gofmt: |
38 | cd gio-example
39 | test -z "$(gofmt -s -l .)"
40 | - check_sign_off: |
41 | set +x -e
42 | cd gio-example
43 | for hash in $(git log -n 20 --format="%H"); do
44 | message=$(git log -1 --format=%B $hash)
45 | if [[ ! "$message" =~ "Signed-off-by: " ]]; then
46 | echo "Missing 'Signed-off-by' in commit $hash"
47 | exit 1
48 | fi
49 | done
50 | - mirror: |
51 | # mirror to github
52 | ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio-example && git push --mirror "$github_mirror" || echo "failed mirroring"
53 |
54 |
55 |
--------------------------------------------------------------------------------
/7gui/README.md:
--------------------------------------------------------------------------------
1 | # 7 GUIs
2 |
3 | This demonstrates several classic GUI problems based on [7GUIs](https://eugenkiss.github.io/7guis/). They show different ways of using Gio framework.
4 |
5 | The examples show one way of implementing of things, of course, there are many more.
6 |
7 | The examples are over-commented to help understand the structure better, in practice, you don't need that many comments.
8 |
9 | ## Counter
10 |
11 | Counter shows basic usage of Gio and how to write interactions.
12 |
13 | It displays a count value that increases when you press a button.
14 |
15 | [UI](./counter/main.go)
16 |
17 | ## Temperature Converter
18 |
19 | Temperature conversion shows bidirectional data flow between two editable fields.
20 |
21 | It implements a bordered field that can be used to propagate values back to another field without causing update loops.
22 |
23 | [UI](./temperature/main.go)
24 |
25 |
26 | ## Timer
27 |
28 | Timer shows how to react to external signals.
29 |
30 | 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.
31 |
32 | The UI shows a slider to change the duration of the timer and there is a button to reset the counter.
33 |
34 | [UI](./timer/main.go), [Timer](./timer/timer.go)
--------------------------------------------------------------------------------
/7gui/counter/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 |
8 | "gioui.org/app" // app contains Window handling.
9 | "gioui.org/font/gofont" // gofont is used for loading the default font.
10 | "gioui.org/io/event"
11 | "gioui.org/io/key" // key is used for keyboard events.
12 |
13 | // system is used for system events (e.g. closing the window).
14 | "gioui.org/layout" // layout is used for layouting widgets.
15 | "gioui.org/op" // op is used for recording different operations.
16 | "gioui.org/op/clip"
17 | "gioui.org/text"
18 | "gioui.org/unit" // unit is used to define pixel-independent sizes
19 | "gioui.org/widget" // widget contains state handling for widgets.
20 | "gioui.org/widget/material" // material contains material design widgets.
21 | )
22 |
23 | func main() {
24 | // The ui loop is separated from the application window creation
25 | // such that it can be used for testing.
26 | ui := NewUI()
27 |
28 | // This creates a new application window and starts the UI.
29 | go func() {
30 | w := new(app.Window)
31 | w.Option(
32 | app.Title("Counter"),
33 | app.Size(unit.Dp(240), unit.Dp(70)),
34 | )
35 | if err := ui.Run(w); err != nil {
36 | log.Println(err)
37 | os.Exit(1)
38 | }
39 | os.Exit(0)
40 | }()
41 |
42 | // This starts Gio main.
43 | app.Main()
44 | }
45 |
46 | // defaultMargin is a margin applied in multiple places to give
47 | // widgets room to breathe.
48 | var defaultMargin = unit.Dp(10)
49 |
50 | // UI holds all of the application state.
51 | type UI struct {
52 | // Theme is used to hold the fonts used throughout the application.
53 | Theme *material.Theme
54 |
55 | // Counter displays and keeps the state of the counter.
56 | Counter Counter
57 | }
58 |
59 | // NewUI creates a new UI using the Go Fonts.
60 | func NewUI() *UI {
61 | ui := &UI{}
62 | ui.Theme = material.NewTheme()
63 | ui.Theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
64 | return ui
65 | }
66 |
67 | // Run handles window events and renders the application.
68 | func (ui *UI) Run(w *app.Window) error {
69 | var ops op.Ops
70 |
71 | // listen for events happening on the window.
72 | for {
73 | // detect the type of the event.
74 | switch e := w.Event().(type) {
75 | // this is sent when the application should re-render.
76 | case app.FrameEvent:
77 | // gtx is used to pass around rendering and event information.
78 | gtx := app.NewContext(&ops, e)
79 |
80 | // register a global key listener for the escape key wrapping our entire UI.
81 | area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
82 | event.Op(gtx.Ops, w)
83 |
84 | // check for presses of the escape key and close the window if we find them.
85 | for {
86 | event, ok := gtx.Event(key.Filter{
87 | Name: key.NameEscape,
88 | })
89 | if !ok {
90 | break
91 | }
92 | switch event := event.(type) {
93 | case key.Event:
94 | if event.Name == key.NameEscape {
95 | return nil
96 | }
97 | }
98 | }
99 | // render and handle UI.
100 | ui.Layout(gtx)
101 | area.Pop()
102 | // render and handle the operations from the UI.
103 | e.Frame(gtx.Ops)
104 |
105 | // this is sent when the application is closed.
106 | case app.DestroyEvent:
107 | return e.Err
108 | }
109 | }
110 | }
111 |
112 | // Layout displays the main program layout.
113 | func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
114 | // inset is used to add padding around the window border.
115 | inset := layout.UniformInset(defaultMargin)
116 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
117 | return ui.Counter.Layout(ui.Theme, gtx)
118 | })
119 | }
120 |
121 | // Counter is a component that keeps track of it's state and
122 | // displays itself as a label and a button.
123 | type Counter struct {
124 | // Count is the current value.
125 | Count int
126 |
127 | // increase is used to track button clicks.
128 | increase widget.Clickable
129 | }
130 |
131 | // Layout lays out the counter and handles input.
132 | func (counter *Counter) Layout(th *material.Theme, gtx layout.Context) layout.Dimensions {
133 | // first, update the count so that once it renders it has the correct value
134 | counter.UpdateCount(gtx)
135 |
136 | // Flex layout lays out widgets from left to right by default.
137 | return layout.Flex{}.Layout(gtx,
138 | // We use weight 1 for both text and count to make them the same size.
139 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
140 | // We center align the text to the area available.
141 | return layout.Center.Layout(gtx,
142 | // Body1 is the default text size for reading.
143 | material.Body1(th, strconv.Itoa(counter.Count)).Layout)
144 | }),
145 | // We use an empty widget to add spacing between the text
146 | // and the button.
147 | layout.Rigid(layout.Spacer{Height: defaultMargin}.Layout),
148 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
149 | // display the button.
150 | return material.Button(th, &counter.increase, "Count").Layout(gtx)
151 | }),
152 | )
153 | }
154 |
155 | func (counter *Counter) UpdateCount(gtx layout.Context) {
156 | // For every click on the button increment the count.
157 | for counter.increase.Clicked(gtx) {
158 | counter.Count++
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/7gui/temperature/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "image"
5 | "image/color"
6 | "log"
7 | "os"
8 | "strconv"
9 |
10 | "gioui.org/app" // app contains Window handling.
11 | "gioui.org/font/gofont" // gofont is used for loading the default font.
12 | "gioui.org/io/event"
13 | "gioui.org/io/key" // key is used for keyboard events.
14 |
15 | // system is used for system events (e.g. closing the window).
16 | "gioui.org/layout" // layout is used for layouting widgets.
17 | "gioui.org/op" // op is used for recording different operations.
18 | "gioui.org/op/clip"
19 | "gioui.org/text"
20 | "gioui.org/unit" // unit is used to define pixel-independent sizes
21 | "gioui.org/widget" // widget contains state handling for widgets.
22 | "gioui.org/widget/material" // material contains material design widgets.
23 | )
24 |
25 | func main() {
26 | // The ui loop is separated from the application window creation
27 | // such that it can be used for testing.
28 | ui := NewUI()
29 |
30 | // This creates a new application window and starts the UI.
31 | go func() {
32 | w := new(app.Window)
33 | w.Option(
34 | app.Title("Temperature Converter"),
35 | app.Size(unit.Dp(360), unit.Dp(47)),
36 | )
37 | if err := ui.Run(w); err != nil {
38 | log.Println(err)
39 | os.Exit(1)
40 | }
41 | os.Exit(0)
42 | }()
43 |
44 | // This starts Gio main.
45 | app.Main()
46 | }
47 |
48 | // defaultMargin is a margin applied in multiple places to give
49 | // widgets room to breathe.
50 | var defaultMargin = unit.Dp(10)
51 |
52 | // UI holds all of the application state.
53 | type UI struct {
54 | // Theme is used to hold the fonts used throughout the application.
55 | Theme *material.Theme
56 |
57 | // Converter displays and modifies the state.
58 | Converter Converter
59 | }
60 |
61 | // NewUI creates a new UI using the Go Fonts.
62 | func NewUI() *UI {
63 | ui := &UI{}
64 | ui.Theme = material.NewTheme()
65 | ui.Theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
66 |
67 | ui.Converter.Init()
68 | return ui
69 | }
70 |
71 | // Run handles window events and renders the application.
72 | func (ui *UI) Run(w *app.Window) error {
73 | var ops op.Ops
74 |
75 | // listen for events happening on the window.
76 | for {
77 | // detect the type of the event.
78 | switch e := w.Event().(type) {
79 | // this is sent when the application should re-render.
80 | case app.FrameEvent:
81 | // gtx is used to pass around rendering and event information.
82 | gtx := app.NewContext(&ops, e)
83 | // register a global key listener for the escape key wrapping our entire UI.
84 | area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
85 | event.Op(gtx.Ops, w)
86 |
87 | // check for presses of the escape key and close the window if we find them.
88 | for {
89 | event, ok := gtx.Event(key.Filter{Name: key.NameEscape})
90 | if !ok {
91 | break
92 | }
93 | switch event := event.(type) {
94 | case key.Event:
95 | if event.Name == key.NameEscape {
96 | return nil
97 | }
98 | }
99 | }
100 | // render and handle UI.
101 | ui.Layout(gtx)
102 | area.Pop()
103 | // render and handle the operations from the UI.
104 | e.Frame(gtx.Ops)
105 |
106 | // this is sent when the application is closed.
107 | case app.DestroyEvent:
108 | return e.Err
109 | }
110 | }
111 | }
112 |
113 | // Layout displays the main program layout.
114 | func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
115 | // inset is used to add padding around the window border.
116 | inset := layout.UniformInset(defaultMargin)
117 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
118 | return ui.Converter.Layout(ui.Theme, gtx)
119 | })
120 | }
121 |
122 | // Converter is a component that keeps track of it's state and
123 | // displays itself as two editors.
124 | type Converter struct {
125 | Celsius Field
126 | Fahrenheit Field
127 | }
128 |
129 | // Init is used to set the inital state.
130 | func (conv *Converter) Init() {
131 | conv.Celsius.SingleLine = true
132 | conv.Fahrenheit.SingleLine = true
133 | }
134 |
135 | // Layout lays out the editors.
136 | func (conv *Converter) Layout(th *material.Theme, gtx layout.Context) layout.Dimensions {
137 | // We use an empty widget to add spacing between widgets.
138 | spacer := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
139 | return layout.Dimensions{Size: image.Pt(gtx.Dp(defaultMargin), 0)}
140 | })
141 |
142 | // check whether the celsius value has changed.
143 | if conv.Celsius.Changed() {
144 | // try to convert the value to an integer
145 | newValue, err := strconv.Atoi(conv.Celsius.Text())
146 | // update whether the editor is displaying a valid value
147 | conv.Celsius.Invalid = err != nil
148 | if !conv.Celsius.Invalid {
149 | // update the other editor when it's valid
150 | conv.Fahrenheit.Invalid = false
151 | conv.Fahrenheit.SetText(strconv.Itoa(newValue*9/5 + 32))
152 | }
153 | }
154 |
155 | // check whether the fahrenheit value has changed.
156 | if conv.Fahrenheit.Changed() {
157 | newValue, err := strconv.Atoi(conv.Fahrenheit.Text())
158 | conv.Fahrenheit.Invalid = err != nil
159 | if !conv.Fahrenheit.Invalid {
160 | conv.Celsius.Invalid = false
161 | conv.Celsius.SetText(strconv.Itoa((newValue - 32) * 5 / 9))
162 | }
163 | }
164 |
165 | return layout.Flex{Alignment: layout.Baseline}.Layout(gtx,
166 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
167 | return conv.Celsius.Layout(th, gtx)
168 | }),
169 | spacer,
170 | layout.Rigid(material.Body1(th, "Celsius").Layout),
171 | spacer,
172 | layout.Rigid(material.Body1(th, "=").Layout),
173 | spacer,
174 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
175 | return conv.Fahrenheit.Layout(th, gtx)
176 | }),
177 | spacer,
178 | layout.Rigid(material.Body1(th, "Fahrenheit").Layout),
179 | )
180 | }
181 |
182 | // Field implements an editor that allows updating the state and detect
183 | // changes to the field from other sources.
184 | type Field struct {
185 | widget.Editor
186 | Invalid bool
187 |
188 | old string
189 | }
190 |
191 | // Changed checks once whether the editor context has changed.
192 | func (ed *Field) Changed() bool {
193 | newText := ed.Editor.Text()
194 | changed := newText != ed.old
195 | ed.old = newText
196 | return changed
197 | }
198 |
199 | // SetText sets editor content without marking the editor changed.
200 | func (ed *Field) SetText(s string) {
201 | ed.old = s
202 | ed.Editor.SetText(s)
203 | }
204 |
205 | // Layout handles the editor with the appropriate color and border.
206 | func (ed *Field) Layout(th *material.Theme, gtx layout.Context) layout.Dimensions {
207 | // Determine colors based on the state of the editor.
208 | borderWidth := float32(0.5)
209 | borderColor := color.NRGBA{A: 107}
210 | switch {
211 | case gtx.Source.Focused(&ed.Editor):
212 | borderColor = th.Palette.ContrastBg
213 | borderWidth = 2
214 | case ed.Invalid:
215 | borderColor = color.NRGBA{R: 200, A: 0xFF}
216 | }
217 |
218 | // draw an editor with a border.
219 | return widget.Border{
220 | Color: borderColor,
221 | CornerRadius: unit.Dp(4),
222 | Width: unit.Dp(borderWidth),
223 | }.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
224 | return layout.UniformInset(unit.Dp(4)).Layout(gtx,
225 | material.Editor(th, &ed.Editor, "").Layout)
226 | })
227 | }
228 |
--------------------------------------------------------------------------------
/7gui/timer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "time"
7 |
8 | "gioui.org/app" // app contains Window handling.
9 | "gioui.org/font/gofont" // gofont is used for loading the default font.
10 | "gioui.org/io/event"
11 | "gioui.org/io/key" // key is used for keyboard events.
12 |
13 | // system is used for system events (e.g. closing the window).
14 | "gioui.org/layout" // layout is used for layouting widgets.
15 | "gioui.org/op" // op is used for recording different operations.
16 | "gioui.org/op/clip"
17 | "gioui.org/text"
18 | "gioui.org/unit" // unit is used to define pixel-independent sizes
19 | "gioui.org/widget" // widget contains state handling for widgets.
20 | "gioui.org/widget/material" // material contains material design widgets.
21 | )
22 |
23 | func main() {
24 | // The ui loop is separated from the application window creation
25 | // such that it can be used for testing.
26 | ui := NewUI()
27 |
28 | // This creates a new application window and starts the UI.
29 | go func() {
30 | w := new(app.Window)
31 | w.Option(
32 | app.Title("Timer"),
33 | app.Size(unit.Dp(360), unit.Dp(360)),
34 | )
35 | if err := ui.Run(w); err != nil {
36 | log.Println(err)
37 | os.Exit(1)
38 | }
39 | os.Exit(0)
40 | }()
41 |
42 | // This starts Gio main.
43 | app.Main()
44 | }
45 |
46 | // defaultMargin is a margin applied in multiple places to give
47 | // widgets room to breathe.
48 | var defaultMargin = unit.Dp(10)
49 |
50 | // UI holds all of the application state.
51 | type UI struct {
52 | // Theme is used to hold the fonts used throughout the application.
53 | Theme *material.Theme
54 |
55 | Timer *Timer
56 |
57 | duration widget.Float
58 | reset widget.Clickable
59 | }
60 |
61 | // NewUI creates a new UI using the Go Fonts.
62 | func NewUI() *UI {
63 | ui := &UI{}
64 | ui.Theme = material.NewTheme()
65 | ui.Theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
66 |
67 | // start with reasonable defaults.
68 | ui.Timer = NewTimer(5 * time.Second)
69 | ui.duration.Value = .5
70 |
71 | return ui
72 | }
73 |
74 | // Run handles window events and renders the application.
75 | func (ui *UI) Run(w *app.Window) error {
76 | // start the timer goroutine and ensure it's closed
77 | // when the application closes.
78 | closeTimer := ui.Timer.Start()
79 | defer closeTimer()
80 |
81 | go func() {
82 | for range ui.Timer.Updated {
83 | // when the timer is updated we should update the screen.
84 | w.Invalidate()
85 | }
86 | }()
87 |
88 | var ops op.Ops
89 | for {
90 | // detect the type of the event.
91 | switch e := w.Event().(type) {
92 | // this is sent when the application should re-render.
93 | case app.FrameEvent:
94 | // gtx is used to pass around rendering and event information.
95 | gtx := app.NewContext(&ops, e)
96 | // register a global key listener for the escape key wrapping our entire UI.
97 | area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
98 | event.Op(gtx.Ops, w)
99 |
100 | // check for presses of the escape key and close the window if we find them.
101 | for {
102 | event, ok := gtx.Event(key.Filter{Name: key.NameEscape})
103 | if !ok {
104 | break
105 | }
106 | switch event := event.(type) {
107 | case key.Event:
108 | if event.Name == key.NameEscape {
109 | return nil
110 | }
111 | }
112 | }
113 | // render and handle UI.
114 | ui.Layout(gtx)
115 | area.Pop()
116 | // render and handle the operations from the UI.
117 | e.Frame(gtx.Ops)
118 |
119 | // this is sent when the application is closed.
120 | case app.DestroyEvent:
121 | return e.Err
122 | }
123 | }
124 | }
125 |
126 | // Layout displays the main program layout.
127 | func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
128 | th := ui.Theme
129 |
130 | // check whether the reset button was clicked.
131 | if ui.reset.Clicked(gtx) {
132 | ui.Timer.Reset()
133 | }
134 | // check whether the slider value has changed.
135 | if ui.duration.Update(gtx) {
136 | ui.Timer.SetDuration(secondsToDuration(float64(ui.duration.Value) * 15))
137 | }
138 |
139 | // get the latest information about the timer.
140 | info := ui.Timer.Info()
141 | progress := float32(0)
142 | if info.Duration == 0 {
143 | progress = 1
144 | } else {
145 | progress = float32(info.Progress.Seconds() / info.Duration.Seconds())
146 | }
147 |
148 | // inset is used to add padding around the window border.
149 | inset := layout.UniformInset(defaultMargin)
150 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
151 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
152 | layout.Rigid(material.Body1(th, "Elapsed Time").Layout),
153 | layout.Rigid(material.ProgressBar(th, progress).Layout),
154 | layout.Rigid(material.Body1(th, info.ProgressString()).Layout),
155 |
156 | layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
157 | layout.Rigid(material.Body1(th, "Duration").Layout),
158 | layout.Rigid(material.Slider(th, &ui.duration).Layout),
159 |
160 | layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
161 | layout.Rigid(material.Button(th, &ui.reset, "Reset").Layout),
162 | )
163 | })
164 | }
165 |
166 | func secondsToDuration(s float64) time.Duration {
167 | return time.Duration(s * float64(time.Second))
168 | }
169 |
--------------------------------------------------------------------------------
/7gui/timer/timer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 | "time"
8 | )
9 |
10 | // Timer implements an
11 | type Timer struct {
12 | // Updated is used to notify UI about changes in the timer.
13 | Updated chan struct{}
14 |
15 | // mu locks the state such that it can be modified and accessed
16 | // from multiple goroutines.
17 | mu sync.Mutex
18 | start time.Time // start corresponds to when the timer was started.
19 | now time.Time // now corresponds to the last updated time.
20 | duration time.Duration // duration is the maximum progress.
21 | }
22 |
23 | // NewTimer creates a new timer with the specified timer.
24 | func NewTimer(initialDuration time.Duration) *Timer {
25 | return &Timer{
26 | Updated: make(chan struct{}),
27 | duration: initialDuration,
28 | }
29 | }
30 |
31 | // Start the timer goroutine and return a cancel func that
32 | // that can be used to stop it.
33 | func (t *Timer) Start() context.CancelFunc {
34 | // initialize the timer state.
35 | now := time.Now()
36 | t.now = now
37 | t.start = now
38 |
39 | // we use done to signal stopping the goroutine.
40 | // a context.Context could be also used.
41 | done := make(chan struct{})
42 | go t.run(done)
43 | return func() { close(done) }
44 | }
45 |
46 | // run is the main loop for the timer.
47 | func (t *Timer) run(done chan struct{}) {
48 | // we use a time.Ticker to update the state,
49 | // in many cases, this could be a network access instead.
50 | tick := time.NewTicker(50 * time.Millisecond)
51 | defer tick.Stop()
52 |
53 | for {
54 | select {
55 | case now := <-tick.C:
56 | t.update(now)
57 | case <-done:
58 | return
59 | }
60 | }
61 | }
62 |
63 | // invalidate sends a signal to the UI that
64 | // the internal state has changed.
65 | func (t *Timer) invalidate() {
66 | // we use a non-blocking send, that way the Timer
67 | // can continue updating internally.
68 | select {
69 | case t.Updated <- struct{}{}:
70 | default:
71 | }
72 | }
73 |
74 | func (t *Timer) update(now time.Time) {
75 | t.mu.Lock()
76 | defer t.mu.Unlock()
77 |
78 | previousNow := t.now
79 | t.now = now
80 |
81 | // first check whether we have not exceeded the duration.
82 | // in that case the progress advanced and we need to notify
83 | // about a change.
84 | progressAfter := t.now.Sub(t.start)
85 | if progressAfter <= t.duration {
86 | t.invalidate()
87 | return
88 | }
89 |
90 | // when we had progressed beyond the duration we also
91 | // need to update the first time it happens.
92 | progressBefore := previousNow.Sub(t.start)
93 | if progressBefore <= t.duration {
94 | t.invalidate()
95 | return
96 | }
97 | }
98 |
99 | // Reset resets timer to the last know time.
100 | func (t *Timer) Reset() {
101 | t.mu.Lock()
102 | defer t.mu.Unlock()
103 |
104 | t.start = t.now
105 | t.invalidate()
106 | }
107 |
108 | // SetDuration changes the duration of the timer.
109 | func (t *Timer) SetDuration(duration time.Duration) {
110 | t.mu.Lock()
111 | defer t.mu.Unlock()
112 |
113 | if t.duration == duration {
114 | return
115 | }
116 | t.duration = duration
117 | t.invalidate()
118 | }
119 |
120 | // Info returns the latest know info about the timer.
121 | func (t *Timer) Info() (info Info) {
122 | t.mu.Lock()
123 | defer t.mu.Unlock()
124 |
125 | info.Progress = t.now.Sub(t.start)
126 | info.Duration = t.duration
127 | if info.Progress > info.Duration {
128 | info.Progress = info.Duration
129 | }
130 | return info
131 | }
132 |
133 | // Info is the information about the timer.
134 | type Info struct {
135 | Progress time.Duration
136 | Duration time.Duration
137 | }
138 |
139 | // ProgressString returns the progress formatted as seconds.
140 | func (info *Info) ProgressString() string {
141 | return fmt.Sprintf("%.1fs", info.Progress.Seconds())
142 | }
143 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This project is provided under the terms of the UNLICENSE or
2 | the MIT license denoted by the following SPDX identifier:
3 |
4 | SPDX-License-Identifier: Unlicense OR MIT
5 |
6 | You may use the project under the terms of either license.
7 |
8 | Both licenses are reproduced below.
9 |
10 | ----
11 | The MIT License (MIT)
12 |
13 | Copyright (c) 2019 The Gio authors
14 |
15 | Permission is hereby granted, free of charge, to any person obtaining a copy
16 | of this software and associated documentation files (the "Software"), to deal
17 | in the Software without restriction, including without limitation the rights
18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 | copies of the Software, and to permit persons to whom the Software is
20 | furnished to do so, subject to the following conditions:
21 |
22 | The above copyright notice and this permission notice shall be included in
23 | all copies or substantial portions of the Software.
24 |
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31 | THE SOFTWARE.
32 | ---
33 |
34 |
35 |
36 | ---
37 | The UNLICENSE
38 |
39 | This is free and unencumbered software released into the public domain.
40 |
41 | Anyone is free to copy, modify, publish, use, compile, sell, or
42 | distribute this software, either in source code form or as a compiled
43 | binary, for any purpose, commercial or non-commercial, and by any
44 | means.
45 |
46 | In jurisdictions that recognize copyright laws, the author or authors
47 | of this software dedicate any and all copyright interest in the
48 | software to the public domain. We make this dedication for the benefit
49 | of the public at large and to the detriment of our heirs and
50 | successors. We intend this dedication to be an overt act of
51 | relinquishment in perpetuity of all present and future rights to this
52 | software under copyright law.
53 |
54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
55 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
56 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
57 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
58 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
59 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
60 | OTHER DEALINGS IN THE SOFTWARE.
61 |
62 | For more information, please refer to
63 | ---
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gio Examples
2 |
3 | Example programs for the [Gio project](https://gioui.org).
4 |
5 | [](https://builds.sr.ht/~eliasnaur/gio-example)
6 |
7 | ## Issues
8 |
9 | File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email
10 | to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the
11 | mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht).
12 |
13 | ## Contributing
14 |
15 | Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to
16 | [gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut
17 | account is required and you can post without being subscribed.
18 |
19 | See the [contribution guide](https://gioui.org/doc/contribute) for more details.
20 |
21 | An [official GitHub mirror](https://github.com/gioui/gio-example) is available.
22 |
23 | ## Tags
24 |
25 | Pre-1.0 tags are provided for reference only, and do not designate releases with ongoing support. Bugfixes will not be backported to older tags.
26 |
27 | Tags follow semantic versioning. In particular, as the major version is zero:
28 |
29 | - breaking API or behavior changes will increment the *minor* version component.
30 | - non-breaking changes will increment the *patch* version component.
31 |
--------------------------------------------------------------------------------
/bidi/bidi.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | _ "embed"
9 | "fmt"
10 | "log"
11 | "os"
12 |
13 | "gioui.org/app"
14 | "gioui.org/io/key"
15 | "gioui.org/layout"
16 | "gioui.org/op"
17 | "gioui.org/text"
18 | "gioui.org/widget"
19 | "gioui.org/widget/material"
20 | )
21 |
22 | func main() {
23 | go func() {
24 | w := new(app.Window)
25 | if err := loop(w); err != nil {
26 | log.Fatal(err)
27 | }
28 | os.Exit(0)
29 | }()
30 | app.Main()
31 | }
32 |
33 | type (
34 | C = layout.Context
35 | D = layout.Dimensions
36 | )
37 |
38 | func loop(w *app.Window) error {
39 | th := material.NewTheme()
40 | var ed widget.Editor
41 | txt := "Hello أهلا my good friend صديقي الجيد bidirectional text نص ثنائي الاتجاه."
42 | ed.SetText(txt)
43 | init := false
44 | ed.Alignment = text.Middle
45 | var ops op.Ops
46 | for {
47 | switch e := w.Event().(type) {
48 | case app.DestroyEvent:
49 | return e.Err
50 | case app.FrameEvent:
51 | gtx := app.NewContext(&ops, e)
52 | if !init {
53 | init = true
54 | gtx.Execute(key.FocusCmd{Tag: &ed})
55 | }
56 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
57 | layout.Rigid(func(gtx C) D {
58 | med := material.Editor(th, &ed, "")
59 | med.TextSize = material.H3(th, "").TextSize
60 | return med.Layout(gtx)
61 | }),
62 | layout.Rigid(func(gtx C) D {
63 | start, end := ed.Selection()
64 | return material.Body1(th, fmt.Sprintf("Selection start %d, end %d", start, end)).Layout(gtx)
65 | }),
66 | )
67 | e.Frame(gtx.Ops)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/color-grid/colorgrid.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "image/color"
9 | "log"
10 | "os"
11 |
12 | "gioui.org/app"
13 | "gioui.org/layout"
14 | "gioui.org/op"
15 | "gioui.org/op/clip"
16 | "gioui.org/op/paint"
17 | "gioui.org/unit"
18 | "gioui.org/widget/material"
19 | "gioui.org/x/component"
20 | )
21 |
22 | func main() {
23 | go func() {
24 | w := new(app.Window)
25 | if err := loop(w); err != nil {
26 | log.Fatal(err)
27 | }
28 | os.Exit(0)
29 | }()
30 | app.Main()
31 | }
32 |
33 | type (
34 | C = layout.Context
35 | D = layout.Dimensions
36 | )
37 |
38 | func loop(w *app.Window) error {
39 | th := material.NewTheme()
40 | var (
41 | ops op.Ops
42 | grid component.GridState
43 | )
44 | sideLength := 1000
45 | cellSize := unit.Dp(10)
46 | for {
47 | switch e := w.Event().(type) {
48 | case app.DestroyEvent:
49 | return e.Err
50 | case app.FrameEvent:
51 | gtx := app.NewContext(&ops, e)
52 | component.Grid(th, &grid).Layout(gtx, sideLength, sideLength,
53 | func(axis layout.Axis, index, constraint int) int {
54 | return gtx.Dp(cellSize)
55 | },
56 | func(gtx C, row, col int) D {
57 | c := color.NRGBA{R: uint8(3 * row), G: uint8(5 * col), B: uint8(row * col), A: 255}
58 | paint.FillShape(gtx.Ops, c, clip.Rect{Max: gtx.Constraints.Max}.Op())
59 | return D{Size: gtx.Constraints.Max}
60 | })
61 | e.Frame(gtx.Ops)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/colorpicker/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "image"
5 | "image/color"
6 | "log"
7 | "os"
8 |
9 | "gioui.org/app"
10 | "gioui.org/font/gofont"
11 | "gioui.org/layout"
12 | "gioui.org/op"
13 | "gioui.org/op/clip"
14 | "gioui.org/op/paint"
15 | "gioui.org/text"
16 | "gioui.org/widget/material"
17 | "gioui.org/x/colorpicker"
18 | )
19 |
20 | func main() {
21 | go func() {
22 | w := new(app.Window)
23 | if err := loop(w); err != nil {
24 | log.Fatal(err)
25 | }
26 | os.Exit(0)
27 | }()
28 | app.Main()
29 | }
30 |
31 | type (
32 | C = layout.Context
33 | D = layout.Dimensions
34 | )
35 |
36 | var white = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
37 |
38 | func loop(w *app.Window) error {
39 | th := material.NewTheme()
40 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
41 | background := white
42 | current := color.NRGBA{R: 255, G: 128, B: 75, A: 255}
43 | picker := colorpicker.State{}
44 | picker.SetColor(current)
45 | muxState := colorpicker.NewMuxState(
46 | []colorpicker.MuxOption{
47 | {
48 | Label: "current",
49 | Value: ¤t,
50 | },
51 | {
52 | Label: "background",
53 | Value: &th.Palette.Bg,
54 | },
55 | {
56 | Label: "foreground",
57 | Value: &th.Palette.Fg,
58 | },
59 | }...)
60 | background = *muxState.Color()
61 | var ops op.Ops
62 | for {
63 | switch e := w.Event().(type) {
64 | case app.DestroyEvent:
65 | return e.Err
66 | case app.FrameEvent:
67 | gtx := app.NewContext(&ops, e)
68 | if muxState.Update(gtx) {
69 | background = *muxState.Color()
70 | }
71 | if picker.Update(gtx) {
72 | current = picker.Color()
73 | background = *muxState.Color()
74 | }
75 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
76 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
77 | return colorpicker.PickerStyle{
78 | Label: "Current",
79 | Theme: th,
80 | State: &picker,
81 | MonospaceFace: "Go Mono",
82 | }.Layout(gtx)
83 | }),
84 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
85 | return layout.Flex{}.Layout(gtx,
86 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
87 | return colorpicker.Mux(th, &muxState, "Display Right:").Layout(gtx)
88 | }),
89 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
90 | size := gtx.Constraints.Max
91 | paint.FillShape(gtx.Ops, background, clip.Rect(image.Rectangle{Max: size}).Op())
92 | return D{Size: size}
93 | }),
94 | )
95 | }),
96 | )
97 | e.Frame(gtx.Ops)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/component/applayout/applayout.go:
--------------------------------------------------------------------------------
1 | package applayout
2 |
3 | import (
4 | "gioui.org/layout"
5 | "gioui.org/unit"
6 | )
7 |
8 | type (
9 | C = layout.Context
10 | D = layout.Dimensions
11 | )
12 |
13 | // DetailRow lays out two widgets in a horizontal row, with the left
14 | // widget considered the "Primary" widget.
15 | type DetailRow struct {
16 | // PrimaryWidth is the fraction of the available width that should
17 | // be allocated to the primary widget. It should be in the range
18 | // (0,1.0]. Defaults to 0.3 if not set.
19 | PrimaryWidth float32
20 | // Inset is automatically applied to both widgets. This inset is
21 | // required, and will default to a uniform 8DP inset if not set.
22 | layout.Inset
23 | }
24 |
25 | var DefaultInset = layout.UniformInset(unit.Dp(8))
26 |
27 | // Layout the DetailRow with the provided widgets.
28 | func (d DetailRow) Layout(gtx C, primary, detail layout.Widget) D {
29 | if d.PrimaryWidth == 0 {
30 | d.PrimaryWidth = 0.3
31 | }
32 | if d.Inset == (layout.Inset{}) {
33 | d.Inset = DefaultInset
34 | }
35 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
36 | layout.Flexed(d.PrimaryWidth, func(gtx C) D {
37 | return d.Inset.Layout(gtx, primary)
38 | }),
39 | layout.Flexed(1-d.PrimaryWidth, func(gtx C) D {
40 | return d.Inset.Layout(gtx, detail)
41 | }),
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/component/icon/icons.go:
--------------------------------------------------------------------------------
1 | package icon
2 |
3 | import (
4 | "gioui.org/widget"
5 | "golang.org/x/exp/shiny/materialdesign/icons"
6 | )
7 |
8 | var MenuIcon *widget.Icon = func() *widget.Icon {
9 | icon, _ := widget.NewIcon(icons.NavigationMenu)
10 | return icon
11 | }()
12 |
13 | var RestaurantMenuIcon *widget.Icon = func() *widget.Icon {
14 | icon, _ := widget.NewIcon(icons.MapsRestaurantMenu)
15 | return icon
16 | }()
17 |
18 | var AccountBalanceIcon *widget.Icon = func() *widget.Icon {
19 | icon, _ := widget.NewIcon(icons.ActionAccountBalance)
20 | return icon
21 | }()
22 |
23 | var AccountBoxIcon *widget.Icon = func() *widget.Icon {
24 | icon, _ := widget.NewIcon(icons.ActionAccountBox)
25 | return icon
26 | }()
27 |
28 | var CartIcon *widget.Icon = func() *widget.Icon {
29 | icon, _ := widget.NewIcon(icons.ActionAddShoppingCart)
30 | return icon
31 | }()
32 |
33 | var HomeIcon *widget.Icon = func() *widget.Icon {
34 | icon, _ := widget.NewIcon(icons.ActionHome)
35 | return icon
36 | }()
37 |
38 | var SettingsIcon *widget.Icon = func() *widget.Icon {
39 | icon, _ := widget.NewIcon(icons.ActionSettings)
40 | return icon
41 | }()
42 |
43 | var OtherIcon *widget.Icon = func() *widget.Icon {
44 | icon, _ := widget.NewIcon(icons.ActionHelp)
45 | return icon
46 | }()
47 |
48 | var HeartIcon *widget.Icon = func() *widget.Icon {
49 | icon, _ := widget.NewIcon(icons.ActionFavorite)
50 | return icon
51 | }()
52 |
53 | var PlusIcon *widget.Icon = func() *widget.Icon {
54 | icon, _ := widget.NewIcon(icons.ContentAdd)
55 | return icon
56 | }()
57 |
58 | var EditIcon *widget.Icon = func() *widget.Icon {
59 | icon, _ := widget.NewIcon(icons.ContentCreate)
60 | return icon
61 | }()
62 |
63 | var VisibilityIcon *widget.Icon = func() *widget.Icon {
64 | icon, _ := widget.NewIcon(icons.ActionVisibility)
65 | return icon
66 | }()
67 |
--------------------------------------------------------------------------------
/component/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 |
8 | "gioui.org/app"
9 | page "gioui.org/example/component/pages"
10 | "gioui.org/example/component/pages/about"
11 | "gioui.org/example/component/pages/appbar"
12 | "gioui.org/example/component/pages/discloser"
13 | "gioui.org/example/component/pages/menu"
14 | "gioui.org/example/component/pages/navdrawer"
15 | "gioui.org/example/component/pages/textfield"
16 | "gioui.org/font/gofont"
17 | "gioui.org/layout"
18 | "gioui.org/op"
19 | "gioui.org/text"
20 | "gioui.org/widget/material"
21 | )
22 |
23 | type (
24 | C = layout.Context
25 | D = layout.Dimensions
26 | )
27 |
28 | func main() {
29 | flag.Parse()
30 | go func() {
31 | w := new(app.Window)
32 | if err := loop(w); err != nil {
33 | log.Fatal(err)
34 | }
35 | os.Exit(0)
36 | }()
37 | app.Main()
38 | }
39 |
40 | func loop(w *app.Window) error {
41 | th := material.NewTheme()
42 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
43 | var ops op.Ops
44 |
45 | router := page.NewRouter()
46 | router.Register(0, appbar.New(&router))
47 | router.Register(1, navdrawer.New(&router))
48 | router.Register(2, textfield.New(&router))
49 | router.Register(3, menu.New(&router))
50 | router.Register(4, discloser.New(&router))
51 | router.Register(5, about.New(&router))
52 |
53 | for {
54 | switch e := w.Event().(type) {
55 | case app.DestroyEvent:
56 | return e.Err
57 | case app.FrameEvent:
58 | gtx := app.NewContext(&ops, e)
59 | router.Layout(gtx, th)
60 | e.Frame(gtx.Ops)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/component/pages/about/about.go:
--------------------------------------------------------------------------------
1 | package about
2 |
3 | import (
4 | "io"
5 | "strings"
6 |
7 | "gioui.org/io/clipboard"
8 | "gioui.org/layout"
9 | "gioui.org/widget"
10 | "gioui.org/widget/material"
11 | "gioui.org/x/component"
12 |
13 | alo "gioui.org/example/component/applayout"
14 | "gioui.org/example/component/icon"
15 | page "gioui.org/example/component/pages"
16 | )
17 |
18 | type (
19 | C = layout.Context
20 | D = layout.Dimensions
21 | )
22 |
23 | // Page holds the state for a page demonstrating the features of
24 | // the AppBar component.
25 | type Page struct {
26 | eliasCopyButton, chrisCopyButtonGH, chrisCopyButtonLP widget.Clickable
27 | widget.List
28 | *page.Router
29 | }
30 |
31 | // New constructs a Page with the provided router.
32 | func New(router *page.Router) *Page {
33 | return &Page{
34 | Router: router,
35 | }
36 | }
37 |
38 | var _ page.Page = &Page{}
39 |
40 | func (p *Page) Actions() []component.AppBarAction {
41 | return []component.AppBarAction{}
42 | }
43 |
44 | func (p *Page) Overflow() []component.OverflowAction {
45 | return []component.OverflowAction{}
46 | }
47 |
48 | func (p *Page) NavItem() component.NavItem {
49 | return component.NavItem{
50 | Name: "About this library",
51 | Icon: icon.OtherIcon,
52 | }
53 | }
54 |
55 | const (
56 | sponsorEliasURL = "https://github.com/sponsors/eliasnaur"
57 | sponsorChrisURLGitHub = "https://github.com/sponsors/whereswaldon"
58 | sponsorChrisURLLiberapay = "https://liberapay.com/whereswaldon/"
59 | )
60 |
61 | func (p *Page) Layout(gtx C, th *material.Theme) D {
62 | p.List.Axis = layout.Vertical
63 | return material.List(th, &p.List).Layout(gtx, 1, func(gtx C, _ int) D {
64 | return layout.Flex{
65 | Alignment: layout.Middle,
66 | Axis: layout.Vertical,
67 | }.Layout(gtx,
68 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
69 | return alo.DefaultInset.Layout(gtx, material.Body1(th, `This library implements material design components from https://material.io using https://gioui.org.
70 |
71 | If you like this library and work like it, please consider sponsoring Elias and/or Chris!`).Layout)
72 | }),
73 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
74 | return alo.DetailRow{}.Layout(gtx,
75 | material.Body1(th, "Elias Naur can be sponsored on GitHub at "+sponsorEliasURL).Layout,
76 | func(gtx C) D {
77 | if p.eliasCopyButton.Clicked(gtx) {
78 | gtx.Execute(clipboard.WriteCmd{
79 | Data: io.NopCloser(strings.NewReader(sponsorEliasURL)),
80 | })
81 | }
82 | return material.Button(th, &p.eliasCopyButton, "Copy Sponsorship URL").Layout(gtx)
83 | })
84 | }),
85 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
86 | return alo.DetailRow{}.Layout(gtx,
87 | material.Body1(th, "Chris Waldon can be sponsored on GitHub at "+sponsorChrisURLGitHub+" and on Liberapay at "+sponsorChrisURLLiberapay).Layout,
88 |
89 | func(gtx C) D {
90 | if p.chrisCopyButtonGH.Clicked(gtx) {
91 | gtx.Execute(clipboard.WriteCmd{
92 | Data: io.NopCloser(strings.NewReader(sponsorChrisURLGitHub)),
93 | })
94 | }
95 | if p.chrisCopyButtonLP.Clicked(gtx) {
96 | gtx.Execute(clipboard.WriteCmd{
97 | Data: io.NopCloser(strings.NewReader(sponsorChrisURLLiberapay)),
98 | })
99 | }
100 | return alo.DefaultInset.Layout(gtx, func(gtx C) D {
101 | return layout.Flex{}.Layout(gtx,
102 | layout.Flexed(.5, material.Button(th, &p.chrisCopyButtonGH, "Copy GitHub URL").Layout),
103 | layout.Flexed(.5, material.Button(th, &p.chrisCopyButtonLP, "Copy Liberapay URL").Layout),
104 | )
105 | })
106 | })
107 | }),
108 | )
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/component/pages/appbar/appbar.go:
--------------------------------------------------------------------------------
1 | package appbar
2 |
3 | import (
4 | "image/color"
5 |
6 | "gioui.org/layout"
7 | "gioui.org/widget"
8 | "gioui.org/widget/material"
9 | "gioui.org/x/component"
10 |
11 | alo "gioui.org/example/component/applayout"
12 | "gioui.org/example/component/icon"
13 | page "gioui.org/example/component/pages"
14 | )
15 |
16 | type (
17 | C = layout.Context
18 | D = layout.Dimensions
19 | )
20 |
21 | // Page holds the state for a page demonstrating the features of
22 | // the AppBar component.
23 | type Page struct {
24 | heartBtn, plusBtn, contextBtn widget.Clickable
25 | exampleOverflowState, red, green, blue widget.Clickable
26 | bottomBar, customNavIcon widget.Bool
27 | favorited bool
28 | widget.List
29 | *page.Router
30 | }
31 |
32 | // New constructs a Page with the provided router.
33 | func New(router *page.Router) *Page {
34 | return &Page{
35 | Router: router,
36 | }
37 | }
38 |
39 | var _ page.Page = &Page{}
40 |
41 | func (p *Page) Actions() []component.AppBarAction {
42 | return []component.AppBarAction{
43 | {
44 | OverflowAction: component.OverflowAction{
45 | Name: "Favorite",
46 | Tag: &p.heartBtn,
47 | },
48 | Layout: func(gtx layout.Context, bg, fg color.NRGBA) layout.Dimensions {
49 | if p.heartBtn.Clicked(gtx) {
50 | p.favorited = !p.favorited
51 | }
52 | btn := component.SimpleIconButton(bg, fg, &p.heartBtn, icon.HeartIcon)
53 | btn.Background = bg
54 | if p.favorited {
55 | btn.Color = color.NRGBA{R: 200, A: 255}
56 | } else {
57 | btn.Color = fg
58 | }
59 | return btn.Layout(gtx)
60 | },
61 | },
62 | component.SimpleIconAction(&p.plusBtn, icon.PlusIcon,
63 | component.OverflowAction{
64 | Name: "Create",
65 | Tag: &p.plusBtn,
66 | },
67 | ),
68 | }
69 | }
70 |
71 | func (p *Page) Overflow() []component.OverflowAction {
72 | return []component.OverflowAction{
73 | {
74 | Name: "Example 1",
75 | Tag: &p.exampleOverflowState,
76 | },
77 | {
78 | Name: "Example 2",
79 | Tag: &p.exampleOverflowState,
80 | },
81 | }
82 | }
83 |
84 | func (p *Page) NavItem() component.NavItem {
85 | return component.NavItem{
86 | Name: "App Bar Features",
87 | Icon: icon.HomeIcon,
88 | }
89 | }
90 |
91 | const (
92 | settingNameColumnWidth = .3
93 | settingDetailsColumnWidth = 1 - settingNameColumnWidth
94 | )
95 |
96 | func (p *Page) Layout(gtx C, th *material.Theme) D {
97 | p.List.Axis = layout.Vertical
98 | return material.List(th, &p.List).Layout(gtx, 1, func(gtx C, _ int) D {
99 | return layout.Flex{
100 | Alignment: layout.Middle,
101 | Axis: layout.Vertical,
102 | }.Layout(gtx,
103 | layout.Rigid(func(gtx C) D {
104 | return alo.DefaultInset.Layout(gtx, material.Body1(th, `The app bar widget provides a consistent interface element for triggering navigation and page-specific actions.
105 |
106 | The controls below allow you to see the various features available in our App Bar implementation.`).Layout)
107 | }),
108 | layout.Rigid(func(gtx C) D {
109 | return alo.DetailRow{}.Layout(gtx, material.Body1(th, "Contextual App Bar").Layout, func(gtx C) D {
110 | if p.contextBtn.Clicked(gtx) {
111 | p.Router.AppBar.SetContextualActions(
112 | []component.AppBarAction{
113 | component.SimpleIconAction(&p.red, icon.HeartIcon,
114 | component.OverflowAction{
115 | Name: "House",
116 | Tag: &p.red,
117 | },
118 | ),
119 | },
120 | []component.OverflowAction{
121 | {
122 | Name: "foo",
123 | Tag: &p.blue,
124 | },
125 | {
126 | Name: "bar",
127 | Tag: &p.green,
128 | },
129 | },
130 | )
131 | p.Router.AppBar.ToggleContextual(gtx.Now, "Contextual Title")
132 | }
133 | return material.Button(th, &p.contextBtn, "Trigger").Layout(gtx)
134 | })
135 | }),
136 | layout.Rigid(func(gtx C) D {
137 | return alo.DetailRow{}.Layout(gtx,
138 | material.Body1(th, "Bottom App Bar").Layout,
139 | func(gtx C) D {
140 | if p.bottomBar.Update(gtx) {
141 | if p.bottomBar.Value {
142 | p.Router.ModalNavDrawer.Anchor = component.Bottom
143 | p.Router.AppBar.Anchor = component.Bottom
144 | } else {
145 | p.Router.ModalNavDrawer.Anchor = component.Top
146 | p.Router.AppBar.Anchor = component.Top
147 | }
148 | p.Router.BottomBar = p.bottomBar.Value
149 | }
150 |
151 | return material.Switch(th, &p.bottomBar, "Use Bottom App Bar").Layout(gtx)
152 | })
153 | }),
154 | layout.Rigid(func(gtx C) D {
155 | return alo.DetailRow{}.Layout(gtx,
156 | material.Body1(th, "Custom Navigation Icon").Layout,
157 | func(gtx C) D {
158 | if p.customNavIcon.Update(gtx) {
159 | if p.customNavIcon.Value {
160 | p.Router.AppBar.NavigationIcon = icon.HomeIcon
161 | } else {
162 | p.Router.AppBar.NavigationIcon = icon.MenuIcon
163 | }
164 | }
165 | return material.Switch(th, &p.customNavIcon, "Use Custom Navigation Icon").Layout(gtx)
166 | })
167 | }),
168 | layout.Rigid(func(gtx C) D {
169 | return alo.DetailRow{}.Layout(gtx,
170 | material.Body1(th, "Animated Resize").Layout,
171 | material.Body2(th, "Resize the width of your screen to see app bar actions collapse into or emerge from the overflow menu (as size permits).").Layout,
172 | )
173 | }),
174 | layout.Rigid(func(gtx C) D {
175 | return alo.DetailRow{}.Layout(gtx,
176 | material.Body1(th, "Custom Action Buttons").Layout,
177 | material.Body2(th, "Click the heart action to see custom button behavior.").Layout)
178 | }),
179 | )
180 | })
181 | }
182 |
--------------------------------------------------------------------------------
/component/pages/discloser/discloser.go:
--------------------------------------------------------------------------------
1 | package discloser
2 |
3 | import (
4 | "gioui.org/layout"
5 | "gioui.org/unit"
6 | "gioui.org/widget"
7 | "gioui.org/widget/material"
8 | "gioui.org/x/component"
9 |
10 | "gioui.org/example/component/icon"
11 | page "gioui.org/example/component/pages"
12 | )
13 |
14 | // TreeNode is a simple tree implementation that holds both
15 | // display data and the state for Discloser widgets. In
16 | // practice, you'll often want to separate the state from
17 | // the data being presented.
18 | type TreeNode struct {
19 | Text string
20 | Children []TreeNode
21 | component.DiscloserState
22 | }
23 |
24 | type (
25 | C = layout.Context
26 | D = layout.Dimensions
27 | )
28 |
29 | // Page holds the state for a page demonstrating the features of
30 | // the AppBar component.
31 | type Page struct {
32 | TreeNode
33 | widget.List
34 | *page.Router
35 | CustomDiscloserState component.DiscloserState
36 | }
37 |
38 | // New constructs a Page with the provided router.
39 | func New(router *page.Router) *Page {
40 | return &Page{
41 | Router: router,
42 | TreeNode: TreeNode{
43 | Text: "Expand Me",
44 | Children: []TreeNode{
45 | {
46 | Text: "Disclosers can be (expand me)...",
47 | Children: []TreeNode{
48 | {
49 | Text: "...nested to arbitrary depths.",
50 | },
51 | {
52 | Text: "There are also types available to customize the look and feel of the discloser:",
53 | Children: []TreeNode{
54 | {
55 | Text: "• DiscloserStyle lets you provide your own control instead of the default triangle used here.",
56 | },
57 | {
58 | Text: "• DiscloserArrowStyle lets you alter the presentation of the triangle used here, like changing its color, size, left/right anchoring, or margin.",
59 | },
60 | },
61 | },
62 | },
63 | },
64 | },
65 | },
66 | }
67 | }
68 |
69 | var _ page.Page = &Page{}
70 |
71 | func (p *Page) Actions() []component.AppBarAction {
72 | return []component.AppBarAction{}
73 | }
74 |
75 | func (p *Page) Overflow() []component.OverflowAction {
76 | return []component.OverflowAction{}
77 | }
78 |
79 | func (p *Page) NavItem() component.NavItem {
80 | return component.NavItem{
81 | Name: "Disclosers",
82 | Icon: icon.VisibilityIcon,
83 | }
84 | }
85 |
86 | // LayoutTreeNode recursively lays out a tree of widgets described by
87 | // TreeNodes.
88 | func (p *Page) LayoutTreeNode(gtx C, th *material.Theme, tn *TreeNode) D {
89 | if len(tn.Children) == 0 {
90 | return layout.UniformInset(unit.Dp(2)).Layout(gtx,
91 | material.Body1(th, tn.Text).Layout)
92 | }
93 | children := make([]layout.FlexChild, 0, len(tn.Children))
94 | for i := range tn.Children {
95 | child := &tn.Children[i]
96 | children = append(children, layout.Rigid(
97 | func(gtx C) D {
98 | return p.LayoutTreeNode(gtx, th, child)
99 | }))
100 | }
101 | return component.SimpleDiscloser(th, &tn.DiscloserState).Layout(gtx,
102 | material.Body1(th, tn.Text).Layout,
103 | func(gtx C) D {
104 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
105 | })
106 | }
107 |
108 | // LayoutCustomDiscloser demonstrates how to create a custom control for
109 | // a discloser.
110 | func (p *Page) LayoutCustomDiscloser(gtx C, th *material.Theme) D {
111 | return component.Discloser(th, &p.CustomDiscloserState).Layout(gtx,
112 | func(gtx C) D {
113 | var l material.LabelStyle
114 | l = material.Body1(th, "+")
115 | if p.CustomDiscloserState.Visible() {
116 | l.Text = "-"
117 | }
118 | l.Font.Typeface = "Go Mono"
119 | return layout.UniformInset(unit.Dp(2)).Layout(gtx, l.Layout)
120 | },
121 | material.Body1(th, "Custom Control").Layout,
122 | material.Body2(th, "This control only took 9 lines of code.").Layout,
123 | )
124 | }
125 |
126 | func (p *Page) Layout(gtx C, th *material.Theme) D {
127 | p.List.Axis = layout.Vertical
128 | return material.List(th, &p.List).Layout(gtx, 2, func(gtx C, index int) D {
129 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
130 | if index == 0 {
131 | return p.LayoutTreeNode(gtx, th, &p.TreeNode)
132 | }
133 | return p.LayoutCustomDiscloser(gtx, th)
134 | })
135 | })
136 | }
137 |
--------------------------------------------------------------------------------
/component/pages/menu/menu.go:
--------------------------------------------------------------------------------
1 | package menu
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 |
8 | "gioui.org/layout"
9 | "gioui.org/op/clip"
10 | "gioui.org/op/paint"
11 | "gioui.org/unit"
12 | "gioui.org/widget"
13 | "gioui.org/widget/material"
14 | "gioui.org/x/component"
15 |
16 | "gioui.org/example/component/icon"
17 | page "gioui.org/example/component/pages"
18 | )
19 |
20 | type (
21 | C = layout.Context
22 | D = layout.Dimensions
23 | )
24 |
25 | // Page holds the state for a page demonstrating the features of
26 | // the Menu component.
27 | type Page struct {
28 | redButton, greenButton, blueButton widget.Clickable
29 | balanceButton, accountButton, cartButton widget.Clickable
30 | leftFillColor color.NRGBA
31 | leftContextArea component.ContextArea
32 | leftMenu, rightMenu component.MenuState
33 | menuInit bool
34 | menuDemoList widget.List
35 | menuDemoListStates []component.ContextArea
36 | widget.List
37 |
38 | *page.Router
39 | }
40 |
41 | // New constructs a Page with the provided router.
42 | func New(router *page.Router) *Page {
43 | return &Page{
44 | Router: router,
45 | }
46 | }
47 |
48 | var _ page.Page = &Page{}
49 |
50 | func (p *Page) Actions() []component.AppBarAction {
51 | return []component.AppBarAction{}
52 | }
53 |
54 | func (p *Page) Overflow() []component.OverflowAction {
55 | return []component.OverflowAction{}
56 | }
57 |
58 | func (p *Page) NavItem() component.NavItem {
59 | return component.NavItem{
60 | Name: "Menu Features",
61 | Icon: icon.RestaurantMenuIcon,
62 | }
63 | }
64 |
65 | func (p *Page) Layout(gtx C, th *material.Theme) D {
66 | if !p.menuInit {
67 | p.leftMenu = component.MenuState{
68 | Options: []func(gtx C) D{
69 | func(gtx C) D {
70 | return layout.Inset{
71 | Left: unit.Dp(16),
72 | Right: unit.Dp(16),
73 | }.Layout(gtx, material.Body1(th, "Menus support arbitrary widgets.\nThis is just a label!\nHere's a loader:").Layout)
74 | },
75 | component.Divider(th).Layout,
76 | func(gtx C) D {
77 | return layout.Inset{
78 | Top: unit.Dp(4),
79 | Bottom: unit.Dp(4),
80 | Left: unit.Dp(16),
81 | Right: unit.Dp(16),
82 | }.Layout(gtx, func(gtx C) D {
83 | gtx.Constraints.Max.X = gtx.Dp(unit.Dp(24))
84 | gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(24))
85 | return material.Loader(th).Layout(gtx)
86 | })
87 | },
88 | component.SubheadingDivider(th, "Colors").Layout,
89 | component.MenuItem(th, &p.redButton, "Red").Layout,
90 | component.MenuItem(th, &p.greenButton, "Green").Layout,
91 | component.MenuItem(th, &p.blueButton, "Blue").Layout,
92 | },
93 | }
94 | p.rightMenu = component.MenuState{
95 | Options: []func(gtx C) D{
96 | func(gtx C) D {
97 | item := component.MenuItem(th, &p.balanceButton, "Balance")
98 | item.Icon = icon.AccountBalanceIcon
99 | item.Hint = component.MenuHintText(th, "Hint")
100 | return item.Layout(gtx)
101 | },
102 | func(gtx C) D {
103 | item := component.MenuItem(th, &p.accountButton, "Account")
104 | item.Icon = icon.AccountBoxIcon
105 | item.Hint = component.MenuHintText(th, "Hint")
106 | return item.Layout(gtx)
107 | },
108 | func(gtx C) D {
109 | item := component.MenuItem(th, &p.cartButton, "Cart")
110 | item.Icon = icon.CartIcon
111 | item.Hint = component.MenuHintText(th, "Hint")
112 | return item.Layout(gtx)
113 | },
114 | },
115 | }
116 | }
117 | if p.redButton.Clicked(gtx) {
118 | p.leftFillColor = color.NRGBA{R: 200, A: 255}
119 | }
120 | if p.greenButton.Clicked(gtx) {
121 | p.leftFillColor = color.NRGBA{G: 200, A: 255}
122 | }
123 | if p.blueButton.Clicked(gtx) {
124 | p.leftFillColor = color.NRGBA{B: 200, A: 255}
125 | }
126 | return layout.Flex{}.Layout(gtx,
127 | layout.Flexed(.5, func(gtx C) D {
128 | return widget.Border{
129 | Color: color.NRGBA{A: 255},
130 | Width: unit.Dp(2),
131 | }.Layout(gtx, func(gtx C) D {
132 | return layout.Stack{}.Layout(gtx,
133 | layout.Stacked(func(gtx C) D {
134 | max := image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
135 | rect := image.Rectangle{
136 | Max: max,
137 | }
138 | paint.FillShape(gtx.Ops, p.leftFillColor, clip.Rect(rect).Op())
139 | return D{Size: max}
140 | }),
141 | layout.Stacked(func(gtx C) D {
142 | return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx C) D {
143 | return component.Surface(th).Layout(gtx, func(gtx C) D {
144 | return layout.UniformInset(unit.Dp(12)).Layout(gtx, material.Body1(th, "Right-click anywhere in this region").Layout)
145 | })
146 | })
147 | }),
148 | layout.Expanded(func(gtx C) D {
149 | return p.leftContextArea.Layout(gtx, func(gtx C) D {
150 | gtx.Constraints.Min = image.Point{}
151 | return component.Menu(th, &p.leftMenu).Layout(gtx)
152 | })
153 | }),
154 | )
155 | })
156 | }),
157 | layout.Flexed(.5, func(gtx C) D {
158 | p.menuDemoList.Axis = layout.Vertical
159 | return material.List(th, &p.menuDemoList).Layout(gtx, 30, func(gtx C, index int) D {
160 | if len(p.menuDemoListStates) < index+1 {
161 | p.menuDemoListStates = append(p.menuDemoListStates, component.ContextArea{})
162 | }
163 | state := &p.menuDemoListStates[index]
164 | return layout.Stack{}.Layout(gtx,
165 | layout.Stacked(func(gtx C) D {
166 | gtx.Constraints.Min.X = gtx.Constraints.Max.X
167 | return layout.UniformInset(unit.Dp(8)).Layout(gtx, material.Body1(th, fmt.Sprintf("Item %d", index)).Layout)
168 | }),
169 | layout.Expanded(func(gtx C) D {
170 | return state.Layout(gtx, func(gtx C) D {
171 | gtx.Constraints.Min.X = 0
172 | return component.Menu(th, &p.rightMenu).Layout(gtx)
173 | })
174 | }),
175 | )
176 | })
177 | }),
178 | )
179 | }
180 |
--------------------------------------------------------------------------------
/component/pages/navdrawer/navdrawer.go:
--------------------------------------------------------------------------------
1 | package navdrawer
2 |
3 | import (
4 | "gioui.org/layout"
5 | "gioui.org/widget"
6 | "gioui.org/widget/material"
7 | "gioui.org/x/component"
8 |
9 | alo "gioui.org/example/component/applayout"
10 | "gioui.org/example/component/icon"
11 | page "gioui.org/example/component/pages"
12 | )
13 |
14 | type (
15 | C = layout.Context
16 | D = layout.Dimensions
17 | )
18 |
19 | // Page holds the state for a page demonstrating the features of
20 | // the NavDrawer component.
21 | type Page struct {
22 | nonModalDrawer widget.Bool
23 | widget.List
24 | *page.Router
25 | }
26 |
27 | // New constructs a Page with the provided router.
28 | func New(router *page.Router) *Page {
29 | return &Page{
30 | Router: router,
31 | }
32 | }
33 |
34 | var _ page.Page = &Page{}
35 |
36 | func (p *Page) Actions() []component.AppBarAction {
37 | return []component.AppBarAction{}
38 | }
39 |
40 | func (p *Page) Overflow() []component.OverflowAction {
41 | return []component.OverflowAction{}
42 | }
43 |
44 | func (p *Page) NavItem() component.NavItem {
45 | return component.NavItem{
46 | Name: "Nav Drawer Features",
47 | Icon: icon.SettingsIcon,
48 | }
49 | }
50 |
51 | func (p *Page) Layout(gtx C, th *material.Theme) D {
52 | p.List.Axis = layout.Vertical
53 | return material.List(th, &p.List).Layout(gtx, 1, func(gtx C, _ int) D {
54 | return layout.Flex{
55 | Alignment: layout.Middle,
56 | Axis: layout.Vertical,
57 | }.Layout(gtx,
58 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
59 | return alo.DefaultInset.Layout(gtx, material.Body1(th, `The nav drawer widget provides a consistent interface element for navigation.
60 |
61 | The controls below allow you to see the various features available in our Navigation Drawer implementation.`).Layout)
62 | }),
63 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
64 | return alo.DetailRow{}.Layout(gtx,
65 | material.Body1(th, "Use non-modal drawer").Layout,
66 | func(gtx C) D {
67 | if p.nonModalDrawer.Update(gtx) {
68 | p.Router.NonModalDrawer = p.nonModalDrawer.Value
69 | if p.nonModalDrawer.Value {
70 | p.Router.NavAnim.Appear(gtx.Now)
71 | } else {
72 | p.Router.NavAnim.Disappear(gtx.Now)
73 | }
74 | }
75 | return material.Switch(th, &p.nonModalDrawer, "Use Non-Modal Navigation Drawer").Layout(gtx)
76 | })
77 | }),
78 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
79 | return alo.DetailRow{}.Layout(gtx,
80 | material.Body1(th, "Drag to Close").Layout,
81 | material.Body2(th, "You can close the modal nav drawer by dragging it to the left.").Layout)
82 | }),
83 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
84 | return alo.DetailRow{}.Layout(gtx,
85 | material.Body1(th, "Touch Scrim to Close").Layout,
86 | material.Body2(th, "You can close the modal nav drawer touching anywhere in the translucent scrim to the right.").Layout)
87 | }),
88 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
89 | return alo.DetailRow{}.Layout(gtx,
90 | material.Body1(th, "Bottom content anchoring").Layout,
91 | material.Body2(th, "If you toggle support for the bottom app bar in the App Bar settings, nav drawer content will anchor to the bottom of the drawer area instead of the top.").Layout)
92 | }),
93 | )
94 | })
95 | }
96 |
--------------------------------------------------------------------------------
/component/pages/page.go:
--------------------------------------------------------------------------------
1 | package page
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "gioui.org/example/component/icon"
8 | "gioui.org/layout"
9 | "gioui.org/op/paint"
10 | "gioui.org/widget/material"
11 | "gioui.org/x/component"
12 | )
13 |
14 | type Page interface {
15 | Actions() []component.AppBarAction
16 | Overflow() []component.OverflowAction
17 | Layout(gtx layout.Context, th *material.Theme) layout.Dimensions
18 | NavItem() component.NavItem
19 | }
20 |
21 | type Router struct {
22 | pages map[any]Page
23 | current any
24 | *component.ModalNavDrawer
25 | NavAnim component.VisibilityAnimation
26 | *component.AppBar
27 | *component.ModalLayer
28 | NonModalDrawer, BottomBar bool
29 | }
30 |
31 | func NewRouter() Router {
32 | modal := component.NewModal()
33 |
34 | nav := component.NewNav("Navigation Drawer", "This is an example.")
35 | modalNav := component.ModalNavFrom(&nav, modal)
36 |
37 | bar := component.NewAppBar(modal)
38 | bar.NavigationIcon = icon.MenuIcon
39 |
40 | na := component.VisibilityAnimation{
41 | State: component.Invisible,
42 | Duration: time.Millisecond * 250,
43 | }
44 | return Router{
45 | pages: make(map[any]Page),
46 | ModalLayer: modal,
47 | ModalNavDrawer: modalNav,
48 | AppBar: bar,
49 | NavAnim: na,
50 | }
51 | }
52 |
53 | func (r *Router) Register(tag any, p Page) {
54 | r.pages[tag] = p
55 | navItem := p.NavItem()
56 | navItem.Tag = tag
57 | if r.current == any(nil) {
58 | r.current = tag
59 | r.AppBar.Title = navItem.Name
60 | r.AppBar.SetActions(p.Actions(), p.Overflow())
61 | }
62 | r.ModalNavDrawer.AddNavItem(navItem)
63 | }
64 |
65 | func (r *Router) SwitchTo(tag any) {
66 | p, ok := r.pages[tag]
67 | if !ok {
68 | return
69 | }
70 | navItem := p.NavItem()
71 | r.current = tag
72 | r.AppBar.Title = navItem.Name
73 | r.AppBar.SetActions(p.Actions(), p.Overflow())
74 | }
75 |
76 | func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
77 | for _, event := range r.AppBar.Events(gtx) {
78 | switch event := event.(type) {
79 | case component.AppBarNavigationClicked:
80 | if r.NonModalDrawer {
81 | r.NavAnim.ToggleVisibility(gtx.Now)
82 | } else {
83 | r.ModalNavDrawer.Appear(gtx.Now)
84 | r.NavAnim.Disappear(gtx.Now)
85 | }
86 | case component.AppBarContextMenuDismissed:
87 | log.Printf("Context menu dismissed: %v", event)
88 | case component.AppBarOverflowActionClicked:
89 | log.Printf("Overflow action selected: %v", event)
90 | }
91 | }
92 | if r.ModalNavDrawer.NavDestinationChanged() {
93 | r.SwitchTo(r.ModalNavDrawer.CurrentNavDestination())
94 | }
95 | paint.Fill(gtx.Ops, th.Palette.Bg)
96 | content := layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
97 | return layout.Flex{}.Layout(gtx,
98 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
99 | gtx.Constraints.Max.X /= 3
100 | return r.NavDrawer.Layout(gtx, th, &r.NavAnim)
101 | }),
102 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
103 | return r.pages[r.current].Layout(gtx, th)
104 | }),
105 | )
106 | })
107 | bar := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
108 | return r.AppBar.Layout(gtx, th, "Menu", "Actions")
109 | })
110 | flex := layout.Flex{Axis: layout.Vertical}
111 | if r.BottomBar {
112 | flex.Layout(gtx, content, bar)
113 | } else {
114 | flex.Layout(gtx, bar, content)
115 | }
116 | r.ModalLayer.Layout(gtx, th)
117 | return layout.Dimensions{Size: gtx.Constraints.Max}
118 | }
119 |
--------------------------------------------------------------------------------
/component/pages/textfield/textfield.go:
--------------------------------------------------------------------------------
1 | package textfield
2 |
3 | import (
4 | "image/color"
5 | "unicode"
6 |
7 | "gioui.org/layout"
8 | "gioui.org/op"
9 | "gioui.org/text"
10 | "gioui.org/widget"
11 | "gioui.org/widget/material"
12 | "gioui.org/x/component"
13 |
14 | alo "gioui.org/example/component/applayout"
15 | "gioui.org/example/component/icon"
16 | page "gioui.org/example/component/pages"
17 | )
18 |
19 | type (
20 | C = layout.Context
21 | D = layout.Dimensions
22 | )
23 |
24 | // Page holds the state for a page demonstrating the features of
25 | // the TextField component.
26 | type Page struct {
27 | inputAlignment text.Alignment
28 | inputAlignmentEnum widget.Enum
29 | nameInput, addressInput, priceInput, tweetInput, numberInput component.TextField
30 | widget.List
31 | *page.Router
32 | }
33 |
34 | // New constructs a Page with the provided router.
35 | func New(router *page.Router) *Page {
36 | return &Page{
37 | Router: router,
38 | }
39 | }
40 |
41 | var _ page.Page = &Page{}
42 |
43 | func (p *Page) Actions() []component.AppBarAction {
44 | return []component.AppBarAction{}
45 | }
46 |
47 | func (p *Page) Overflow() []component.OverflowAction {
48 | return []component.OverflowAction{}
49 | }
50 |
51 | func (p *Page) NavItem() component.NavItem {
52 | return component.NavItem{
53 | Name: "Text Field Features",
54 | Icon: icon.EditIcon,
55 | }
56 | }
57 |
58 | func (p *Page) Layout(gtx C, th *material.Theme) D {
59 | p.List.Axis = layout.Vertical
60 | return material.List(th, &p.List).Layout(gtx, 1, func(gtx C, _ int) D {
61 | return layout.Flex{
62 | Axis: layout.Vertical,
63 | }.Layout(
64 | gtx,
65 | layout.Rigid(func(gtx C) D {
66 | p.nameInput.Alignment = p.inputAlignment
67 | return p.nameInput.Layout(gtx, th, "Name")
68 | }),
69 | layout.Rigid(func(gtx C) D {
70 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "Responds to hover events.").Layout)
71 | }),
72 | layout.Rigid(func(gtx C) D {
73 | p.addressInput.Alignment = p.inputAlignment
74 | return p.addressInput.Layout(gtx, th, "Address")
75 | }),
76 | layout.Rigid(func(gtx C) D {
77 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "Label animates properly when you click to select the text field.").Layout)
78 | }),
79 | layout.Rigid(func(gtx C) D {
80 | p.priceInput.Prefix = func(gtx C) D {
81 | th := *th
82 | th.Palette.Fg = color.NRGBA{R: 100, G: 100, B: 100, A: 255}
83 | return material.Label(&th, th.TextSize, "$").Layout(gtx)
84 | }
85 | p.priceInput.Suffix = func(gtx C) D {
86 | th := *th
87 | th.Palette.Fg = color.NRGBA{R: 100, G: 100, B: 100, A: 255}
88 | return material.Label(&th, th.TextSize, ".00").Layout(gtx)
89 | }
90 | p.priceInput.SingleLine = true
91 | p.priceInput.Alignment = p.inputAlignment
92 | return p.priceInput.Layout(gtx, th, "Price")
93 | }),
94 | layout.Rigid(func(gtx C) D {
95 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "Can have prefix and suffix elements.").Layout)
96 | }),
97 | layout.Rigid(func(gtx C) D {
98 | if err := func() string {
99 | for _, r := range p.numberInput.Text() {
100 | if !unicode.IsDigit(r) {
101 | return "Must contain only digits"
102 | }
103 | }
104 | return ""
105 | }(); err != "" {
106 | p.numberInput.SetError(err)
107 | } else {
108 | p.numberInput.ClearError()
109 | }
110 | p.numberInput.SingleLine = true
111 | p.numberInput.Alignment = p.inputAlignment
112 | return p.numberInput.Layout(gtx, th, "Number")
113 | }),
114 | layout.Rigid(func(gtx C) D {
115 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "Can be validated.").Layout)
116 | }),
117 | layout.Rigid(func(gtx C) D {
118 | if p.tweetInput.TextTooLong() {
119 | p.tweetInput.SetError("Too many characters")
120 | } else {
121 | p.tweetInput.ClearError()
122 | }
123 | p.tweetInput.CharLimit = 128
124 | p.tweetInput.Helper = "Tweets have a limited character count"
125 | p.tweetInput.Alignment = p.inputAlignment
126 | return p.tweetInput.Layout(gtx, th, "Tweet")
127 | }),
128 | layout.Rigid(func(gtx C) D {
129 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "Can have a character counter and help text.").Layout)
130 | }),
131 | layout.Rigid(func(gtx C) D {
132 | if p.inputAlignmentEnum.Update(gtx) {
133 | switch p.inputAlignmentEnum.Value {
134 | case layout.Start.String():
135 | p.inputAlignment = text.Start
136 | case layout.Middle.String():
137 | p.inputAlignment = text.Middle
138 | case layout.End.String():
139 | p.inputAlignment = text.End
140 | default:
141 | p.inputAlignment = text.Start
142 | }
143 | gtx.Execute(op.InvalidateCmd{})
144 | }
145 | return alo.DefaultInset.Layout(
146 | gtx,
147 | func(gtx C) D {
148 | return layout.Flex{
149 | Axis: layout.Vertical,
150 | }.Layout(
151 | gtx,
152 | layout.Rigid(func(gtx C) D {
153 | return material.Body2(th, "Text Alignment").Layout(gtx)
154 | }),
155 | layout.Rigid(func(gtx C) D {
156 | return layout.Flex{
157 | Axis: layout.Vertical,
158 | }.Layout(
159 | gtx,
160 | layout.Rigid(func(gtx C) D {
161 | return material.RadioButton(
162 | th,
163 | &p.inputAlignmentEnum,
164 | layout.Start.String(),
165 | "Start",
166 | ).Layout(gtx)
167 | }),
168 | layout.Rigid(func(gtx C) D {
169 | return material.RadioButton(
170 | th,
171 | &p.inputAlignmentEnum,
172 | layout.Middle.String(),
173 | "Middle",
174 | ).Layout(gtx)
175 | }),
176 | layout.Rigid(func(gtx C) D {
177 | return material.RadioButton(
178 | th,
179 | &p.inputAlignmentEnum,
180 | layout.End.String(),
181 | "End",
182 | ).Layout(gtx)
183 | }),
184 | )
185 | }),
186 | )
187 | },
188 | )
189 | }),
190 | layout.Rigid(func(gtx C) D {
191 | return alo.DefaultInset.Layout(gtx, material.Body2(th, "This text field implementation was contributed by Jack Mordaunt. Thanks Jack!").Layout)
192 | }),
193 | )
194 | })
195 | }
196 |
--------------------------------------------------------------------------------
/cursor_position/cursor_position.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 | package main
3 |
4 | import (
5 | "fmt"
6 | "image"
7 | "log"
8 | "os"
9 |
10 | "gioui.org/app"
11 | "gioui.org/f32"
12 | "gioui.org/io/event"
13 | "gioui.org/io/pointer"
14 | "gioui.org/layout"
15 | "gioui.org/op"
16 | "gioui.org/op/clip"
17 | "gioui.org/unit"
18 | "gioui.org/widget/material"
19 | )
20 |
21 | func main() {
22 | // Create a new window.
23 | go func() {
24 | w := new(app.Window)
25 | w.Option(app.Size(800, 600))
26 | if err := loop(w); err != nil {
27 | log.Fatal(err)
28 | }
29 | os.Exit(0)
30 | }()
31 | app.Main()
32 | }
33 |
34 | func loop(w *app.Window) error {
35 | var ops op.Ops
36 | // Initialize the mouse position.
37 | var mousePos f32.Point
38 | mousePresent := false
39 | // Create a material theme.
40 | th := material.NewTheme()
41 | for {
42 | switch e := w.Event().(type) {
43 | case app.DestroyEvent:
44 | return e.Err
45 | case app.FrameEvent:
46 | gtx := app.NewContext(&ops, e)
47 | // Register for pointer move events over the entire window.
48 | r := image.Rectangle{Max: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Constraints.Max.Y}}
49 | area := clip.Rect(r).Push(&ops)
50 | event.Op(&ops, &mousePos)
51 | area.Pop()
52 | for {
53 | ev, ok := gtx.Event(pointer.Filter{
54 | Target: &mousePos,
55 | Kinds: pointer.Move | pointer.Enter | pointer.Leave,
56 | })
57 | if !ok {
58 | break
59 | }
60 | switch ev := ev.(type) {
61 | case pointer.Event:
62 | switch ev.Kind {
63 | case pointer.Enter:
64 | mousePresent = true
65 | case pointer.Leave:
66 | mousePresent = false
67 | }
68 | mousePos = ev.Position
69 | }
70 | }
71 |
72 | // Display the mouse coordinates.
73 | layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
74 | coords := "Mouse is outside window"
75 | if mousePresent {
76 | coords = fmt.Sprintf("Mouse Position: (%.2f, %.2f)", mousePos.X, mousePos.Y)
77 | }
78 | lbl := material.Label(th, unit.Sp(24), coords)
79 | return lbl.Layout(gtx)
80 | })
81 |
82 | e.Frame(gtx.Ops)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/customdeco/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | // The customdeco program demonstrates custom decorations
4 | // in Gio.
5 | package main
6 |
7 | import (
8 | "fmt"
9 | "image/color"
10 | "log"
11 | "os"
12 |
13 | "gioui.org/app"
14 | "gioui.org/io/system"
15 | "gioui.org/layout"
16 | "gioui.org/op"
17 | "gioui.org/op/clip"
18 | "gioui.org/op/paint"
19 | "gioui.org/text"
20 | "gioui.org/widget"
21 | "gioui.org/widget/material"
22 |
23 | "gioui.org/font/gofont"
24 | )
25 |
26 | func main() {
27 | go func() {
28 | w := new(app.Window)
29 | w.Option(app.Decorated(false))
30 | if err := loop(w); err != nil {
31 | log.Fatal(err)
32 | }
33 | os.Exit(0)
34 | }()
35 | app.Main()
36 | }
37 |
38 | func loop(w *app.Window) error {
39 | var (
40 | b widget.Clickable
41 | deco widget.Decorations
42 | )
43 | var (
44 | toggle bool
45 | decorated bool
46 | title string
47 | )
48 | th := material.NewTheme()
49 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
50 | var ops op.Ops
51 | for {
52 | switch e := w.Event().(type) {
53 | case app.DestroyEvent:
54 | return e.Err
55 | case app.ConfigEvent:
56 | decorated = e.Config.Decorated
57 | deco.Maximized = e.Config.Mode == app.Maximized
58 | title = e.Config.Title
59 | case app.FrameEvent:
60 | gtx := app.NewContext(&ops, e)
61 | for b.Clicked(gtx) {
62 | toggle = !toggle
63 | w.Option(app.Decorated(toggle))
64 | }
65 | cl := clip.Rect{Max: e.Size}.Push(gtx.Ops)
66 | paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(gtx.Ops)
67 | paint.PaintOp{}.Add(gtx.Ops)
68 | layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
69 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
70 | layout.Rigid(material.Button(th, &b, "Toggle decorations").Layout),
71 | layout.Rigid(material.Body1(th, fmt.Sprintf("Decorated: %v", decorated)).Layout),
72 | )
73 | })
74 | cl.Pop()
75 | if !decorated {
76 | w.Perform(deco.Update(gtx))
77 | material.Decorations(th, &deco, ^system.Action(0), title).Layout(gtx)
78 | }
79 | e.Frame(gtx.Ops)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/explorer/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "fmt"
9 | "image"
10 | "image/jpeg"
11 | _ "image/jpeg"
12 | "image/png"
13 | _ "image/png"
14 | "log"
15 | "os"
16 |
17 | "gioui.org/app"
18 | "gioui.org/io/event"
19 | "gioui.org/layout"
20 | "gioui.org/op"
21 | "gioui.org/op/paint"
22 | "gioui.org/text"
23 | "gioui.org/widget"
24 | "gioui.org/widget/material"
25 |
26 | "gioui.org/font/gofont"
27 | "gioui.org/x/explorer"
28 | )
29 |
30 | func main() {
31 | go func() {
32 | w := new(app.Window)
33 | if err := loop(w); err != nil {
34 | log.Fatal(err)
35 | }
36 | os.Exit(0)
37 | }()
38 | app.Main()
39 | }
40 |
41 | type (
42 | C = layout.Context
43 | D = layout.Dimensions
44 | )
45 |
46 | // ImageResult is the results of trying to open an image. It may
47 | // contain either an error or an image, but not both. The error
48 | // should always be checked first.
49 | type ImageResult struct {
50 | Error error
51 | Format string
52 | Image image.Image
53 | }
54 |
55 | func loop(w *app.Window) error {
56 | expl := explorer.NewExplorer(w)
57 | var openBtn, saveBtn widget.Clickable
58 | th := material.NewTheme()
59 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
60 | imgChan := make(chan ImageResult)
61 | saveChan := make(chan error)
62 |
63 | events := make(chan event.Event)
64 | acks := make(chan struct{})
65 |
66 | go func() {
67 | for {
68 | ev := w.Event()
69 | events <- ev
70 | <-acks
71 | if _, ok := ev.(app.DestroyEvent); ok {
72 | return
73 | }
74 | }
75 | }()
76 | var img ImageResult
77 | var saveErr error
78 | var ops op.Ops
79 | for {
80 | select {
81 | case img = <-imgChan:
82 | w.Invalidate()
83 | case saveErr = <-saveChan:
84 | w.Invalidate()
85 | case e := <-events:
86 | expl.ListenEvents(e)
87 | switch e := e.(type) {
88 | case app.DestroyEvent:
89 | acks <- struct{}{}
90 | return e.Err
91 | case app.FrameEvent:
92 | gtx := app.NewContext(&ops, e)
93 | if openBtn.Clicked(gtx) {
94 | go func() {
95 | file, err := expl.ChooseFile("png", "jpeg", "jpg")
96 | if err != nil {
97 | err = fmt.Errorf("failed opening image file: %w", err)
98 | imgChan <- ImageResult{Error: err}
99 | return
100 | }
101 | defer file.Close()
102 | imgData, format, err := image.Decode(file)
103 | if err != nil {
104 | err = fmt.Errorf("failed decoding image data: %w", err)
105 | imgChan <- ImageResult{Error: err}
106 | return
107 | }
108 | imgChan <- ImageResult{Image: imgData, Format: format}
109 | }()
110 | }
111 | if saveBtn.Clicked(gtx) {
112 | go func(img ImageResult) {
113 | if img.Error != nil {
114 | saveChan <- fmt.Errorf("no image loaded, cannot save")
115 | return
116 | }
117 | extension := "jpg"
118 | switch img.Format {
119 | case "png":
120 | extension = "png"
121 | }
122 | file, err := expl.CreateFile("file." + extension)
123 | if err != nil {
124 | saveChan <- fmt.Errorf("failed exporting image file: %w", err)
125 | return
126 | }
127 | defer func() {
128 | saveChan <- file.Close()
129 | }()
130 | switch extension {
131 | case "jpg":
132 | if err := jpeg.Encode(file, img.Image, nil); err != nil {
133 | saveChan <- fmt.Errorf("failed encoding image file: %w", err)
134 | return
135 | }
136 | case "png":
137 | if err := png.Encode(file, img.Image); err != nil {
138 | saveChan <- fmt.Errorf("failed encoding image file: %w", err)
139 | return
140 | }
141 | }
142 | }(img)
143 | }
144 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
145 | layout.Rigid(material.Button(th, &openBtn, "Open Image").Layout),
146 | layout.Flexed(1, func(gtx C) D {
147 | if img.Error == nil && img.Image == nil {
148 | return D{}
149 | } else if img.Error != nil {
150 | return material.H6(th, img.Error.Error()).Layout(gtx)
151 | }
152 |
153 | return widget.Image{
154 | Src: paint.NewImageOp(img.Image),
155 | Fit: widget.Contain,
156 | }.Layout(gtx)
157 | }),
158 | layout.Rigid(func(gtx C) D {
159 | if img.Image == nil {
160 | gtx = gtx.Disabled()
161 | }
162 | return material.Button(th, &saveBtn, "Save Image").Layout(gtx)
163 | }),
164 | layout.Rigid(func(gtx C) D {
165 | if saveErr == nil {
166 | return D{}
167 | }
168 | return material.H6(th, saveErr.Error()).Layout(gtx)
169 | }),
170 | )
171 | e.Frame(gtx.Ops)
172 | }
173 | acks <- struct{}{}
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/fps-table/table.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "image"
9 | "image/color"
10 | "log"
11 | "os"
12 | "strconv"
13 | "time"
14 |
15 | "gioui.org/app"
16 | "gioui.org/font"
17 | "gioui.org/font/gofont"
18 | "gioui.org/layout"
19 | "gioui.org/op"
20 | "gioui.org/text"
21 | "gioui.org/unit"
22 | "gioui.org/widget"
23 | "gioui.org/widget/material"
24 | "gioui.org/x/component"
25 | )
26 |
27 | func main() {
28 | go func() {
29 | w := new(app.Window)
30 | if err := loop(w); err != nil {
31 | log.Fatal(err)
32 | }
33 | os.Exit(0)
34 | }()
35 | app.Main()
36 | }
37 |
38 | type (
39 | C = layout.Context
40 | D = layout.Dimensions
41 | )
42 |
43 | type FrameTiming struct {
44 | Start, End time.Time
45 | FrameCount int
46 | FramesPerSecond float64
47 | }
48 |
49 | func max(a, b int) int {
50 | if a > b {
51 | return a
52 | }
53 | return b
54 | }
55 |
56 | func loop(w *app.Window) error {
57 | th := material.NewTheme()
58 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
59 | var (
60 | ops op.Ops
61 | grid component.GridState
62 | )
63 | timingWindow := time.Second
64 | timings := []FrameTiming{}
65 | frameCounter := 0
66 | timingStart := time.Time{}
67 | for {
68 | switch e := w.Event().(type) {
69 | case app.DestroyEvent:
70 | return e.Err
71 | case app.FrameEvent:
72 | gtx := app.NewContext(&ops, e)
73 | gtx.Execute(op.InvalidateCmd{})
74 | if timingStart == (time.Time{}) {
75 | timingStart = gtx.Now
76 | }
77 | if interval := gtx.Now.Sub(timingStart); interval >= timingWindow {
78 | timings = append(timings, FrameTiming{
79 | Start: timingStart,
80 | End: gtx.Now,
81 | FrameCount: frameCounter,
82 | FramesPerSecond: float64(frameCounter) / interval.Seconds(),
83 | })
84 | frameCounter = 0
85 | timingStart = gtx.Now
86 | }
87 | layoutTable(th, gtx, timings, &grid)
88 | e.Frame(gtx.Ops)
89 | frameCounter++
90 | }
91 | }
92 | }
93 |
94 | var headingText = []string{"Start", "End", "Frames", "FPS"}
95 |
96 | func layoutTable(th *material.Theme, gtx C, timings []FrameTiming, grid *component.GridState) D {
97 | // Configure width based on available space and a minimum size.
98 | minSize := gtx.Dp(unit.Dp(200))
99 | border := widget.Border{
100 | Color: color.NRGBA{A: 255},
101 | Width: unit.Dp(1),
102 | }
103 |
104 | inset := layout.UniformInset(unit.Dp(2))
105 |
106 | // Configure a label styled to be a heading.
107 | headingLabel := material.Body1(th, "")
108 | headingLabel.Font.Weight = font.Bold
109 | headingLabel.Alignment = text.Middle
110 | headingLabel.MaxLines = 1
111 |
112 | // Configure a label styled to be a data element.
113 | dataLabel := material.Body1(th, "")
114 | dataLabel.Font.Typeface = "Go Mono"
115 | dataLabel.MaxLines = 1
116 | dataLabel.Alignment = text.End
117 |
118 | // Measure the height of a heading row.
119 | orig := gtx.Constraints
120 | gtx.Constraints.Min = image.Point{}
121 | macro := op.Record(gtx.Ops)
122 | dims := inset.Layout(gtx, headingLabel.Layout)
123 | _ = macro.Stop()
124 | gtx.Constraints = orig
125 |
126 | return component.Table(th, grid).Layout(gtx, len(timings), 4,
127 | func(axis layout.Axis, index, constraint int) int {
128 | widthUnit := max(int(float32(constraint)/3), minSize)
129 | switch axis {
130 | case layout.Horizontal:
131 | switch index {
132 | case 0, 1:
133 | return int(widthUnit)
134 | case 2, 3:
135 | return int(widthUnit / 2)
136 | default:
137 | return 0
138 | }
139 | default:
140 | return dims.Size.Y
141 | }
142 | },
143 | func(gtx C, col int) D {
144 | return border.Layout(gtx, func(gtx C) D {
145 | return inset.Layout(gtx, func(gtx C) D {
146 | headingLabel.Text = headingText[col]
147 | return headingLabel.Layout(gtx)
148 | })
149 | })
150 | },
151 | func(gtx C, row, col int) D {
152 | return inset.Layout(gtx, func(gtx C) D {
153 | timing := timings[row]
154 | switch col {
155 | case 0:
156 | dataLabel.Text = timing.Start.Format("15:04:05.000000")
157 | case 1:
158 | dataLabel.Text = timing.End.Format("15:04:05.000000")
159 | case 2:
160 | dataLabel.Text = strconv.Itoa(timing.FrameCount)
161 | case 3:
162 | dataLabel.Text = strconv.FormatFloat(timing.FramesPerSecond, 'f', 2, 64)
163 | }
164 | return dataLabel.Layout(gtx)
165 | })
166 | },
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/galaxy/galaxy.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 |
8 | "golang.org/x/exp/rand"
9 |
10 | "gonum.org/v1/gonum/spatial/barneshut"
11 | "gonum.org/v1/gonum/spatial/r2"
12 | )
13 |
14 | type mass struct {
15 | d r2.Vec // position
16 | v r2.Vec // velocity
17 | m float64 // mass
18 | }
19 |
20 | func (m *mass) Coord2() r2.Vec { return m.d }
21 | func (m *mass) Mass() float64 { return m.m }
22 | func (m *mass) move(f r2.Vec) {
23 | // F = ma
24 | f.X /= m.m
25 | f.Y /= m.m
26 | m.v = m.v.Add(f)
27 |
28 | // Update position.
29 | m.d = m.d.Add(m.v)
30 | }
31 |
32 | func galaxy(numStars int, rnd *rand.Rand) ([]*mass, barneshut.Plane) {
33 | // Make 50 stars in random locations and velocities.
34 | stars := make([]*mass, numStars)
35 | p := make([]barneshut.Particle2, len(stars))
36 | for i := range stars {
37 | s := &mass{
38 | d: r2.Vec{
39 | X: 100*rnd.Float64() - 50,
40 | Y: 100*rnd.Float64() - 50,
41 | },
42 | m: rnd.Float64(),
43 | }
44 | // Aim at the ground and miss.
45 | s.d = s.d.Scale(-1).Add(r2.Vec{
46 | X: 10 * rnd.NormFloat64(),
47 | Y: 10 * rnd.NormFloat64(),
48 | })
49 |
50 | stars[i] = s
51 | p[i] = s
52 | }
53 | // Make a plane to calculate approximate forces
54 | plane := barneshut.Plane{Particles: p}
55 |
56 | return stars, plane
57 | }
58 |
59 | func simulate(stars []*mass, plane barneshut.Plane, dist *distribution) {
60 | vectors := make([]r2.Vec, len(stars))
61 | // Build the data structure. For small systems
62 | // this step may be omitted and ForceOn will
63 | // perform the naive quadratic calculation
64 | // without building the data structure.
65 | err := plane.Reset()
66 | if err != nil {
67 | log.Fatal(err)
68 | }
69 |
70 | // Calculate the force vectors using the theta
71 | // parameter.
72 | const theta = 0.1
73 | // and an imaginary gravitational constant.
74 | const G = 10
75 | for j, s := range stars {
76 | vectors[j] = plane.ForceOn(s, theta, barneshut.Gravity2).Scale(G)
77 | }
78 |
79 | // Update positions.
80 | for j, s := range stars {
81 | s.move(vectors[j])
82 | }
83 |
84 | // Recompute the distribution of stars
85 | dist.Update(stars)
86 | dist.EnsureSquare()
87 | }
88 |
--------------------------------------------------------------------------------
/galaxy/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "image"
8 | "image/color"
9 | "log"
10 | "math"
11 | "strconv"
12 | "time"
13 |
14 | "golang.org/x/exp/rand"
15 | "golang.org/x/exp/shiny/materialdesign/icons"
16 |
17 | "gonum.org/v1/gonum/spatial/r2"
18 |
19 | "gioui.org/app"
20 | "gioui.org/f32"
21 | "gioui.org/font/gofont"
22 | "gioui.org/io/event"
23 | "gioui.org/io/pointer"
24 | "gioui.org/layout"
25 | "gioui.org/op"
26 | "gioui.org/op/clip"
27 | "gioui.org/op/paint"
28 | "gioui.org/text"
29 | "gioui.org/unit"
30 | "gioui.org/widget"
31 | "gioui.org/widget/material"
32 | )
33 |
34 | // distribution tracks useful minimum and maximum information about
35 | // the stars.
36 | type distribution struct {
37 | min, max r2.Vec
38 | maxSpeed float64
39 | meanSpeed float64
40 | minMass, maxMass float64
41 |
42 | speedSum float64
43 | speedSamples int
44 | }
45 |
46 | // Update ensures that the distribution contains accurate min/max
47 | // data for the slice of stars provided.
48 | func (d *distribution) Update(stars []*mass) {
49 | var (
50 | speedSum float64
51 | speedSamples int
52 | )
53 | for i, s := range stars {
54 | speed := distance(s.v, s.d)
55 | if i == 0 {
56 | d.minMass = s.m
57 | }
58 | if s.d.X < d.min.X {
59 | d.min.X = s.d.X
60 | }
61 | if s.d.Y < d.min.Y {
62 | d.min.Y = s.d.Y
63 | }
64 | if s.d.X > d.max.X {
65 | d.max.X = s.d.X
66 | }
67 | if s.d.Y > d.max.Y {
68 | d.max.Y = s.d.Y
69 | }
70 | if s.m > d.maxMass {
71 | d.maxMass = s.m
72 | }
73 | if s.m < d.minMass {
74 | d.minMass = s.m
75 | }
76 | if speed > d.maxSpeed {
77 | d.maxSpeed = speed
78 | }
79 | speedSamples++
80 | speedSum += speed
81 | }
82 | d.meanSpeed = speedSum / float64(speedSamples)
83 | }
84 |
85 | // EnsureSquare adjusts the distribution so that the min and max
86 | // coordinates are the corners of a square (by padding one axis
87 | // equally across the top and bottom). This helps to prevent visual
88 | // distortion during the visualization, though it does not stop it
89 | // completely.
90 | func (d *distribution) EnsureSquare() {
91 | diff := d.max.Sub(d.min)
92 | if diff.X > diff.Y {
93 | padding := (diff.X - diff.Y) / 2
94 | d.max.Y += padding
95 | d.min.Y -= padding
96 | } else if diff.Y > diff.X {
97 | padding := (diff.Y - diff.X) / 2
98 | d.max.X += padding
99 | d.min.X -= padding
100 | }
101 | }
102 |
103 | // String describes the distribution in text form.
104 | func (d distribution) String() string {
105 | return fmt.Sprintf("distance: (min: %v max: %v), mass: (min: %v, max: %v)", d.min, d.max, d.minMass, d.maxMass)
106 | }
107 |
108 | // Scale uses the min/max data within the distribution to compute the
109 | // position, speed, and size of the star.
110 | func (d distribution) Scale(star *mass) Star {
111 | s := Star{}
112 | s.X = float32((star.d.X - d.min.X) / (d.max.X - d.min.X))
113 | s.Y = float32((star.d.Y - d.min.Y) / (d.max.Y - d.min.Y))
114 | speed := math.Log(distance(star.v, star.d)) / math.Log(d.maxSpeed)
115 | s.Speed = float32(speed)
116 | s.Size = unit.Dp(float32(1 + ((star.m / (d.maxMass - d.minMass)) * 10)))
117 | return s
118 | }
119 |
120 | // distance implements the simple two-dimensional euclidean distance function.
121 | func distance(a, b r2.Vec) float64 {
122 | return math.Sqrt((b.X-a.X)*(b.X-a.X) + (b.Y-a.Y)*(b.Y-a.Y))
123 | }
124 |
125 | var PlayIcon = func() *widget.Icon {
126 | ic, _ := widget.NewIcon(icons.AVPlayArrow)
127 | return ic
128 | }()
129 |
130 | var PauseIcon = func() *widget.Icon {
131 | ic, _ := widget.NewIcon(icons.AVPause)
132 | return ic
133 | }()
134 |
135 | var ClearIcon = func() *widget.Icon {
136 | ic, _ := widget.NewIcon(icons.ContentClear)
137 | return ic
138 | }()
139 |
140 | // viewport models a region of a larger space. Offset is the location
141 | // of the upper-left corner of the view within the larger space. size
142 | // is the dimensions of the viewport within the larger space.
143 | type viewport struct {
144 | offset f32.Point
145 | size f32.Point
146 | }
147 |
148 | // subview modifies v to describe a smaller region by zooming into the
149 | // space described by v using other.
150 | func (v *viewport) subview(other *viewport) {
151 | v.offset.X += other.offset.X * v.size.X
152 | v.offset.Y += other.offset.Y * v.size.Y
153 | v.size.X *= other.size.X
154 | v.size.Y *= other.size.Y
155 | }
156 |
157 | // ensureSquare returns a copy of the rectangle that has been padded to
158 | // be square by increasing the maximum coordinate.
159 | func ensureSquare(r image.Rectangle) image.Rectangle {
160 | dx := r.Dx()
161 | dy := r.Dy()
162 | if dx > dy {
163 | r.Max.Y = r.Min.Y + dx
164 | } else if dy > dx {
165 | r.Max.X = r.Min.X + dy
166 | }
167 | return r
168 | }
169 |
170 | var (
171 | ops op.Ops
172 | play, clear widget.Clickable
173 | playing = false
174 | th = material.NewTheme()
175 | selected image.Rectangle
176 | selecting = false
177 | view *viewport
178 | )
179 |
180 | func main() {
181 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
182 | th.Palette.Fg, th.Palette.Bg = th.Palette.Bg, th.Palette.Fg
183 | dist := distribution{}
184 |
185 | seed := time.Now().UnixNano()
186 | rnd := rand.New(rand.NewSource(uint64(seed)))
187 |
188 | // Make 1000 stars in random locations.
189 | stars, plane := galaxy(1000, rnd)
190 | dist.Update(stars)
191 |
192 | desiredSize := unit.Dp(800)
193 | window := new(app.Window)
194 | window.Option(
195 | app.Size(desiredSize, desiredSize),
196 | app.Title("Seed: "+strconv.Itoa(int(seed))),
197 | )
198 |
199 | iterateSim := func() {
200 | if !playing {
201 | return
202 | }
203 | simulate(stars, plane, &dist)
204 | window.Invalidate()
205 | }
206 | for {
207 | switch ev := window.Event().(type) {
208 | case app.DestroyEvent:
209 | if ev.Err != nil {
210 | log.Fatal(ev.Err)
211 | }
212 | return
213 | case app.FrameEvent:
214 | gtx := app.NewContext(&ops, ev)
215 | paint.Fill(gtx.Ops, th.Palette.Bg)
216 |
217 | layout.Center.Layout(gtx, func(gtx C) D {
218 | return widget.Border{
219 | Color: th.Fg,
220 | Width: unit.Dp(1),
221 | }.Layout(gtx, func(gtx C) D {
222 | if gtx.Constraints.Max.X > gtx.Constraints.Max.Y {
223 | gtx.Constraints.Max.X = gtx.Constraints.Max.Y
224 | } else {
225 | gtx.Constraints.Max.Y = gtx.Constraints.Max.X
226 | }
227 | gtx.Constraints.Min = gtx.Constraints.Max
228 |
229 | if clear.Clicked(gtx) {
230 | view = nil
231 | }
232 | if play.Clicked(gtx) {
233 | playing = !playing
234 | }
235 |
236 | layoutSelectionLayer(gtx)
237 |
238 | for _, s := range stars {
239 | dist.Scale(s).Layout(gtx, view)
240 | }
241 | layoutControls(gtx)
242 | return D{Size: gtx.Constraints.Max}
243 | })
244 | })
245 |
246 | ev.Frame(gtx.Ops)
247 | iterateSim()
248 | }
249 | }
250 | }
251 |
252 | func layoutControls(gtx C) D {
253 | layout.N.Layout(gtx, func(gtx C) D {
254 | return material.Body1(th, "Click and drag to zoom in on a region").Layout(gtx)
255 | })
256 | layout.S.Layout(gtx, func(gtx C) D {
257 | gtx.Constraints.Min.X = gtx.Constraints.Max.X
258 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
259 | return layout.Flex{
260 | Spacing: layout.SpaceEvenly,
261 | }.Layout(gtx,
262 | layout.Rigid(func(gtx C) D {
263 | var btn material.IconButtonStyle
264 | if playing {
265 | btn = material.IconButton(th, &play, PauseIcon, "Pause Simulation")
266 | } else {
267 | btn = material.IconButton(th, &play, PlayIcon, "Play Simulation")
268 | }
269 | return btn.Layout(gtx)
270 | }),
271 | layout.Rigid(func(gtx C) D {
272 | if view == nil {
273 | gtx = gtx.Disabled()
274 | }
275 | return material.IconButton(th, &clear, ClearIcon, "Reset Viewport").Layout(gtx)
276 | }),
277 | )
278 | })
279 | })
280 | return D{}
281 | }
282 |
283 | func layoutSelectionLayer(gtx C) D {
284 | for {
285 | event, ok := gtx.Event(pointer.Filter{
286 | Target: &selected,
287 | Kinds: pointer.Press | pointer.Release | pointer.Drag,
288 | })
289 | if !ok {
290 | break
291 | }
292 | switch event := event.(type) {
293 | case pointer.Event:
294 | var intPt image.Point
295 | intPt.X = int(event.Position.X)
296 | intPt.Y = int(event.Position.Y)
297 | switch event.Kind {
298 | case pointer.Press:
299 | selecting = true
300 | selected.Min = intPt
301 | selected.Max = intPt
302 | case pointer.Drag:
303 | if intPt.X >= selected.Min.X && intPt.Y >= selected.Min.Y {
304 | selected.Max = intPt
305 | } else {
306 | selected.Min = intPt
307 | }
308 | selected = ensureSquare(selected)
309 | case pointer.Release:
310 | selecting = false
311 | newView := &viewport{
312 | offset: f32.Point{
313 | X: float32(selected.Min.X) / float32(gtx.Constraints.Max.X),
314 | Y: float32(selected.Min.Y) / float32(gtx.Constraints.Max.Y),
315 | },
316 | size: f32.Point{
317 | X: float32(selected.Dx()) / float32(gtx.Constraints.Max.X),
318 | Y: float32(selected.Dy()) / float32(gtx.Constraints.Max.Y),
319 | },
320 | }
321 | if view == nil {
322 | view = newView
323 | } else {
324 | view.subview(newView)
325 | }
326 | case pointer.Cancel:
327 | selecting = false
328 | selected = image.Rectangle{}
329 | }
330 | }
331 | }
332 | if selecting {
333 | paint.FillShape(gtx.Ops, color.NRGBA{R: 255, A: 100}, clip.Rect(selected).Op())
334 | }
335 | pr := clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops)
336 | pointer.CursorCrosshair.Add(gtx.Ops)
337 | event.Op(gtx.Ops, &selected)
338 | pr.Pop()
339 |
340 | return D{Size: gtx.Constraints.Max}
341 | }
342 |
343 | // Star represents a point of mass rendered within a specific region of a canvas.
344 | type Star struct {
345 | X, Y float32
346 | Speed float32
347 | Size unit.Dp
348 | }
349 |
350 | type (
351 | C = layout.Context
352 | D = layout.Dimensions
353 | )
354 |
355 | // Layout renders the star into the gtx assuming that it is visible within the
356 | // provided viewport. Stars outside of the viewport will be skipped.
357 | func (s Star) Layout(gtx layout.Context, view *viewport) layout.Dimensions {
358 | px := gtx.Dp(s.Size)
359 | if view != nil {
360 | if s.X < view.offset.X || s.X > view.offset.X+view.size.X {
361 | return D{}
362 | }
363 | if s.Y < view.offset.Y || s.Y > view.offset.Y+view.size.Y {
364 | return D{}
365 | }
366 | s.X = (s.X - view.offset.X) / view.size.X
367 | s.Y = (s.Y - view.offset.Y) / view.size.Y
368 | }
369 | rr := px / 2
370 | x := s.X*float32(gtx.Constraints.Max.X) - float32(rr)
371 | y := s.Y*float32(gtx.Constraints.Max.Y) - float32(rr)
372 | defer op.Affine(f32.Affine2D{}.Offset(f32.Pt(x, y))).Push(gtx.Ops).Pop()
373 |
374 | rect := image.Rectangle{
375 | Max: image.Pt(px, px),
376 | }
377 | fill := color.NRGBA{R: 0xff, G: 128, B: 0xff, A: 50}
378 | fill.R = 255 - uint8(255*s.Speed)
379 | fill.B = uint8(255 * s.Speed)
380 | paint.FillShape(gtx.Ops, fill, clip.UniformRRect(rect, rr).Op(gtx.Ops))
381 | return D{}
382 | }
383 |
--------------------------------------------------------------------------------
/glfw/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | // GLFW doesn't build on OpenBSD and FreeBSD.
4 | //go:build !openbsd && !freebsd && !android && !ios && !js
5 | // +build !openbsd,!freebsd,!android,!ios,!js
6 |
7 | // The glfw example demonstrates integration of Gio into a foreign
8 | // windowing and rendering library, in this case GLFW
9 | // (https://www.glfw.org).
10 | //
11 | // See the go-glfw package for installation of the native
12 | // dependencies:
13 | //
14 | // https://github.com/go-gl/glfw
15 | package main
16 |
17 | import (
18 | "image"
19 | "log"
20 | "math"
21 | "runtime"
22 | "time"
23 |
24 | "gioui.org/f32"
25 | "gioui.org/font/gofont"
26 | "gioui.org/gpu"
27 | "gioui.org/io/input"
28 | "gioui.org/io/pointer"
29 | "gioui.org/layout"
30 | "gioui.org/op"
31 | "gioui.org/text"
32 | "gioui.org/unit"
33 | "gioui.org/widget"
34 | "gioui.org/widget/material"
35 | "github.com/go-gl/gl/v3.1/gles2"
36 | "github.com/go-gl/gl/v3.3-core/gl"
37 | "github.com/go-gl/glfw/v3.3/glfw"
38 | )
39 |
40 | // desktopGL is true when the (core, desktop) OpenGL should
41 | // be used, false for OpenGL ES.
42 | const desktopGL = runtime.GOOS == "darwin"
43 |
44 | func main() {
45 | // Required by the OpenGL threading model.
46 | runtime.LockOSThread()
47 |
48 | err := glfw.Init()
49 | if err != nil {
50 | log.Fatal(err)
51 | }
52 | defer glfw.Terminate()
53 | // Gio assumes a sRGB backbuffer.
54 | glfw.WindowHint(glfw.SRGBCapable, glfw.True)
55 | glfw.WindowHint(glfw.ScaleToMonitor, glfw.True)
56 | glfw.WindowHint(glfw.CocoaRetinaFramebuffer, glfw.True)
57 | if desktopGL {
58 | glfw.WindowHint(glfw.ContextVersionMajor, 3)
59 | glfw.WindowHint(glfw.ContextVersionMinor, 3)
60 | glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
61 | glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
62 | } else {
63 | glfw.WindowHint(glfw.ContextCreationAPI, glfw.EGLContextAPI)
64 | glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLESAPI)
65 | glfw.WindowHint(glfw.ContextVersionMajor, 3)
66 | glfw.WindowHint(glfw.ContextVersionMinor, 0)
67 | }
68 |
69 | window, err := glfw.CreateWindow(800, 600, "Gio + GLFW", nil, nil)
70 | if err != nil {
71 | log.Fatal(err)
72 | }
73 |
74 | window.MakeContextCurrent()
75 |
76 | if desktopGL {
77 | err = gl.Init()
78 | } else {
79 | err = gles2.Init()
80 | }
81 | if err != nil {
82 | log.Fatalf("gl.Init failed: %v", err)
83 | }
84 | if desktopGL {
85 | // Enable sRGB.
86 | gl.Enable(gl.FRAMEBUFFER_SRGB)
87 | // Set up default VBA, required for the forward-compatible core profile.
88 | var defVBA uint32
89 | gl.GenVertexArrays(1, &defVBA)
90 | gl.BindVertexArray(defVBA)
91 | }
92 |
93 | var queue input.Router
94 | var ops op.Ops
95 | th := material.NewTheme()
96 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
97 | gpuCtx, err := gpu.New(gpu.OpenGL{ES: !desktopGL, Shared: true})
98 | if err != nil {
99 | log.Fatal(err)
100 | }
101 | defer gpuCtx.Release()
102 |
103 | registerCallbacks(window, &queue)
104 | for !window.ShouldClose() {
105 | glfw.PollEvents()
106 | scale, _ := window.GetContentScale()
107 | width, height := window.GetFramebufferSize()
108 | sz := image.Point{X: width, Y: height}
109 | ops.Reset()
110 | gtx := layout.Context{
111 | Ops: &ops,
112 | Now: time.Now(),
113 | Source: queue.Source(),
114 | Metric: unit.Metric{
115 | PxPerDp: scale,
116 | PxPerSp: scale,
117 | },
118 | Constraints: layout.Exact(sz),
119 | }
120 | drawOpenGL()
121 | draw(gtx, th)
122 | gpuCtx.Frame(gtx.Ops, gpu.OpenGLRenderTarget{}, sz)
123 | queue.Frame(gtx.Ops)
124 | window.SwapBuffers()
125 | }
126 | }
127 |
128 | var (
129 | button widget.Clickable
130 | green float64 = 0.2
131 | )
132 |
133 | // drawOpenGL demonstrates the direct use of OpenGL commands
134 | // to draw non-Gio content below the Gio UI.
135 | func drawOpenGL() {
136 | if desktopGL {
137 | gl.ClearColor(0, float32(green), 0, 1)
138 | gl.Clear(gl.COLOR_BUFFER_BIT)
139 | } else {
140 | gles2.ClearColor(0, float32(green), 0, 1)
141 | gles2.Clear(gl.COLOR_BUFFER_BIT)
142 | }
143 | }
144 |
145 | // handleCursorEvent handles cursor events not processed by Gio.
146 | func handleCursorEvent(xpos, ypos float64) {
147 | log.Printf("mouse cursor: (%f,%f)", xpos, ypos)
148 | }
149 |
150 | // handleMouseButtonEvent handles mouse button events not processed by Gio.
151 | func handleMouseButtonEvent(button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
152 | if action == glfw.Press {
153 | green += 0.1
154 | green, _ = math.Frexp(green)
155 | }
156 | log.Printf("mouse button: %v action %v mods %v", button, action, mods)
157 | }
158 |
159 | func draw(gtx layout.Context, th *material.Theme) layout.Dimensions {
160 | return layout.Center.Layout(gtx,
161 | material.Button(th, &button, "Button").Layout,
162 | )
163 | }
164 |
165 | func registerCallbacks(window *glfw.Window, q *input.Router) {
166 | var btns pointer.Buttons
167 | beginning := time.Now()
168 | var lastPos f32.Point
169 | window.SetCursorPosCallback(func(w *glfw.Window, xpos float64, ypos float64) {
170 | scale := float32(1)
171 | if runtime.GOOS == "darwin" {
172 | // macOS cursor positions are not scaled to the underlying framebuffer
173 | // size when CocoaRetinaFramebuffer is true.
174 | scale, _ = w.GetContentScale()
175 | }
176 | lastPos = f32.Point{X: float32(xpos) * scale, Y: float32(ypos) * scale}
177 | e := pointer.Event{
178 | Kind: pointer.Move,
179 | Position: lastPos,
180 | Source: pointer.Mouse,
181 | Time: time.Since(beginning),
182 | Buttons: btns,
183 | }
184 | q.Queue(e)
185 | if _, ok := q.WakeupTime(); !ok {
186 | handleCursorEvent(xpos, ypos)
187 | }
188 | })
189 | window.SetMouseButtonCallback(func(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
190 | var btn pointer.Buttons
191 | switch button {
192 | case glfw.MouseButton1:
193 | btn = pointer.ButtonPrimary
194 | case glfw.MouseButton2:
195 | btn = pointer.ButtonSecondary
196 | case glfw.MouseButton3:
197 | btn = pointer.ButtonTertiary
198 | }
199 | var typ pointer.Kind
200 | switch action {
201 | case glfw.Release:
202 | typ = pointer.Release
203 | btns &^= btn
204 | case glfw.Press:
205 | typ = pointer.Press
206 | btns |= btn
207 | }
208 | e := pointer.Event{
209 | Kind: typ,
210 | Source: pointer.Mouse,
211 | Time: time.Since(beginning),
212 | Position: lastPos,
213 | Buttons: btns,
214 | }
215 | q.Queue(e)
216 | if _, ok := q.WakeupTime(); !ok {
217 | handleMouseButtonEvent(button, action, mods)
218 | }
219 | })
220 | }
221 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gioui.org/example
2 |
3 | go 1.24.1
4 |
5 | require (
6 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
7 | gioui.org v0.8.1-0.20250424183133-e18db649912a
8 | gioui.org/cmd v0.8.1-0.20250321120711-01ffdf788daa
9 | gioui.org/x v0.8.2-0.20250115181849-c005f2ad1592
10 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
11 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
12 | github.com/google/go-github/v24 v24.0.1
13 | github.com/inkeliz/giohyperlink v0.0.0-20220903215451-2ac5d54abdce
14 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
15 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
16 | golang.org/x/image v0.25.0
17 | golang.org/x/oauth2 v0.30.0
18 | gonum.org/v1/gonum v0.8.2
19 | )
20 |
21 | require (
22 | gioui.org/shader v1.0.8 // indirect
23 | git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
24 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
25 | github.com/akavel/rsrc v0.10.1 // indirect
26 | github.com/esiqveland/notify v0.13.3 // indirect
27 | github.com/go-ole/go-ole v1.3.0 // indirect
28 | github.com/go-text/typesetting v0.3.0 // indirect
29 | github.com/godbus/dbus/v5 v5.1.0 // indirect
30 | github.com/google/go-querystring v1.1.0 // indirect
31 | github.com/yuin/goldmark v1.7.11 // indirect
32 | golang.org/x/mod v0.24.0 // indirect
33 | golang.org/x/sync v0.13.0 // indirect
34 | golang.org/x/sys v0.32.0 // indirect
35 | golang.org/x/text v0.24.0 // indirect
36 | golang.org/x/tools v0.31.0 // indirect
37 | )
38 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
2 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
3 | gioui.org v0.8.1-0.20250424183133-e18db649912a h1:hqcxAFkm5lKJlYvi9hkUvK0s0XcN2xGP5cRGZfbUVKU=
4 | gioui.org v0.8.1-0.20250424183133-e18db649912a/go.mod h1:JnoLsqpYezue9ZRMG7E2hOXar1/oAE9ZFkiFfF4oULs=
5 | gioui.org/cmd v0.8.1-0.20250321120711-01ffdf788daa h1:KF5RNvVkslrCCAvxNNKldwKPUi4EXfBiONjm64Hj+kM=
6 | gioui.org/cmd v0.8.1-0.20250321120711-01ffdf788daa/go.mod h1:leSxAyTne/BCsyZp7zLrjxzS0trQ3wQ0YWqIPDuKJmQ=
7 | gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
8 | gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
9 | gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
10 | gioui.org/x v0.8.2-0.20250115181849-c005f2ad1592 h1:n7iW0im2lkdwiykN/WkQYDaJbRxxQ8ojag3AvyOcv/o=
11 | gioui.org/x v0.8.2-0.20250115181849-c005f2ad1592/go.mod h1:v2g60aiZtIVR7lNFXZ123+U0kijJeOChODSuqr7MFSI=
12 | git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
13 | git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
14 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
15 | git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
16 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
17 | github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
18 | github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
19 | github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
20 | github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
21 | github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
22 | github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
27 | github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
28 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
29 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
30 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
31 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
32 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
33 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
34 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
35 | github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
36 | github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
37 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
38 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
39 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
40 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
41 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
42 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
43 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
44 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
45 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
46 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
47 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
49 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
50 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
51 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
52 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
53 | github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju57UR4Q4=
54 | github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M=
55 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
56 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
57 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
58 | github.com/inkeliz/giohyperlink v0.0.0-20220903215451-2ac5d54abdce h1:VY+88zGHu3up1GEdcSH9bFBrwF/0HJRLhaR7QGn+6II=
59 | github.com/inkeliz/giohyperlink v0.0.0-20220903215451-2ac5d54abdce/go.mod h1:aYfTeMhp1YaZo0welffItZD1SeIsccPQ7A4evWbHjmY=
60 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
61 | github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
62 | github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
63 | github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
64 | github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
70 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
71 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
72 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
73 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
74 | github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
75 | github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
76 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
77 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
78 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
79 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
80 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
81 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
82 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
83 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
84 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
85 | golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
86 | golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
87 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
88 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
89 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
90 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
91 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
92 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
93 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
94 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
95 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
96 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
98 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
99 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
100 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
101 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
102 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
103 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
104 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
105 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
106 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
107 | gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM=
108 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
109 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
110 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
111 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
113 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
114 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
115 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
116 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
117 |
--------------------------------------------------------------------------------
/gophers/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A Gio program that displays Go contributors from GitHub. See https://gioui.org for more information.
6 |
7 | import (
8 | "context"
9 | "flag"
10 | "fmt"
11 | "image"
12 | "log"
13 | "net/http"
14 | "os"
15 | "slices"
16 |
17 | "golang.org/x/oauth2"
18 |
19 | "gioui.org/app"
20 | "gioui.org/gesture"
21 | "gioui.org/io/event"
22 | "gioui.org/io/key"
23 | "gioui.org/op"
24 | "gioui.org/op/clip"
25 | "gioui.org/unit"
26 |
27 | "github.com/google/go-github/v24/github"
28 |
29 | _ "image/jpeg"
30 | _ "image/png"
31 |
32 | _ "net/http/pprof"
33 | )
34 |
35 | type App struct {
36 | w *app.Window
37 |
38 | ui *UI
39 |
40 | updateUsers chan []*user
41 | commitsResult chan []*github.Commit
42 | ctx context.Context
43 | ctxCancel context.CancelFunc
44 | }
45 |
46 | var (
47 | prof = flag.Bool("profile", false, "serve profiling data at http://localhost:6060")
48 | stats = flag.Bool("stats", false, "show rendering statistics")
49 | token = flag.String("token", "", "Github authentication token")
50 | )
51 |
52 | func main() {
53 | flag.Parse()
54 | initProfiling()
55 | if *token == "" {
56 | fmt.Println("The quota for anonymous GitHub API access is very low. Specify a token with -token to avoid quota errors.")
57 | fmt.Println("See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line.")
58 | }
59 | go func() {
60 | w := new(app.Window)
61 | w.Option(
62 | app.Size(unit.Dp(400), unit.Dp(800)),
63 | app.Title("Gophers"),
64 | )
65 | if err := newApp(w).run(); err != nil {
66 | log.Fatal(err)
67 | }
68 | os.Exit(0)
69 | }()
70 | app.Main()
71 | }
72 |
73 | func initProfiling() {
74 | if !*prof {
75 | return
76 | }
77 | go func() {
78 | log.Println(http.ListenAndServe("localhost:6060", nil))
79 | }()
80 | }
81 |
82 | func (a *App) run() error {
83 | a.ui.profiling = *stats
84 |
85 | events := make(chan event.Event)
86 | acks := make(chan struct{})
87 |
88 | go func() {
89 | for {
90 | ev := a.w.Event()
91 | events <- ev
92 | <-acks
93 | if _, ok := ev.(app.DestroyEvent); ok {
94 | return
95 | }
96 | }
97 | }()
98 | var ops op.Ops
99 | for {
100 | select {
101 | case users := <-a.updateUsers:
102 | a.ui.users = users
103 | a.ui.userClicks = make([]gesture.Click, len(users))
104 | a.w.Invalidate()
105 | case commits := <-a.commitsResult:
106 | a.ui.selectedUser.commits = commits
107 | a.w.Invalidate()
108 | case e := <-events:
109 | switch e := e.(type) {
110 | case app.DestroyEvent:
111 | if a.ctxCancel != nil {
112 | a.ctxCancel()
113 | a.ctxCancel = nil
114 | }
115 | acks <- struct{}{}
116 | return e.Err
117 | case app.FrameEvent:
118 | if a.ctxCancel == nil {
119 | a.ctx, a.ctxCancel = context.WithCancel(context.Background())
120 | }
121 | if a.ui.users == nil {
122 | a.ui.users = []*user{}
123 | go a.fetchContributors()
124 | }
125 | gtx := app.NewContext(&ops, e)
126 |
127 | // register a global key listener for the escape key wrapping our entire UI.
128 | area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
129 | event.Op(gtx.Ops, a.w)
130 |
131 | // check for presses of global keyboard shortcuts and process them.
132 | for {
133 | event, ok := gtx.Event(
134 | key.Filter{
135 | Required: key.ModShortcut,
136 | Name: "P",
137 | },
138 | key.Filter{
139 | Name: key.NameBack,
140 | },
141 | key.Filter{
142 | Name: key.NameEscape,
143 | },
144 | )
145 | if !ok {
146 | break
147 | }
148 | switch event := event.(type) {
149 | case key.Event:
150 | switch event.Name {
151 | case key.NameEscape:
152 | return nil
153 | case key.NameBack:
154 | if a.ui.selectedUser != nil {
155 | a.ui.selectedUser = nil
156 | a.w.Invalidate()
157 | }
158 | case "P":
159 | if event.Modifiers.Contain(key.ModShortcut) && event.State == key.Press {
160 | a.ui.profiling = !a.ui.profiling
161 | a.w.Invalidate()
162 | }
163 | }
164 | }
165 | }
166 | a.ui.Layout(gtx)
167 | area.Pop()
168 | e.Frame(gtx.Ops)
169 | }
170 | acks <- struct{}{}
171 | }
172 | }
173 | }
174 |
175 | func newApp(w *app.Window) *App {
176 | a := &App{
177 | w: w,
178 | updateUsers: make(chan []*user),
179 | commitsResult: make(chan []*github.Commit, 1),
180 | }
181 | fetch := func(u string) {
182 | a.fetchCommits(a.ctx, u)
183 | }
184 | a.ui = newUI(fetch)
185 | return a
186 | }
187 |
188 | func githubClient(ctx context.Context) *github.Client {
189 | var tc *http.Client
190 | if *token != "" {
191 | ts := oauth2.StaticTokenSource(
192 | &oauth2.Token{AccessToken: *token},
193 | )
194 | tc = oauth2.NewClient(ctx, ts)
195 | }
196 | return github.NewClient(tc)
197 | }
198 |
199 | func (a *App) fetchContributors() {
200 | client := githubClient(a.ctx)
201 | cons, _, err := client.Repositories.ListContributors(a.ctx, "golang", "go", nil)
202 | if err != nil {
203 | fmt.Fprintf(os.Stderr, "github: failed to fetch contributors: %v\n", err)
204 | return
205 | }
206 | var users []*user
207 | userErrs := make(chan error, len(cons))
208 | avatarErrs := make(chan error, len(cons))
209 | for _, con := range cons {
210 | con := con
211 | avatar := con.GetAvatarURL()
212 | if avatar == "" {
213 | continue
214 | }
215 | u := &user{
216 | login: con.GetLogin(),
217 | }
218 | users = append(users, u)
219 | go func() {
220 | guser, _, err := client.Users.Get(a.ctx, u.login)
221 | if err != nil {
222 | avatarErrs <- err
223 | return
224 | }
225 | u.name = guser.GetName()
226 | u.company = guser.GetCompany()
227 | avatarErrs <- nil
228 | }()
229 | go func() {
230 | a, err := fetchImage(avatar)
231 | if a != nil {
232 | u.avatar = a
233 | }
234 | userErrs <- err
235 | }()
236 | }
237 | for range cons {
238 | if err := <-userErrs; err != nil {
239 | fmt.Fprintf(os.Stderr, "github: failed to fetch user: %v\n", err)
240 | }
241 | if err := <-avatarErrs; err != nil {
242 | fmt.Fprintf(os.Stderr, "github: failed to fetch avatar: %v\n", err)
243 | }
244 | }
245 | // Drop users with no avatar or name.
246 | for i := len(users) - 1; i >= 0; i-- {
247 | if u := users[i]; u.name == "" || u.avatar.Bounds().Size() == (image.Point{}) {
248 | users = slices.Delete(users, i, i+1)
249 | }
250 | }
251 | a.updateUsers <- users
252 | }
253 |
254 | func fetchImage(url string) (image.Image, error) {
255 | resp, err := http.Get(url)
256 | if err != nil {
257 | return nil, fmt.Errorf("fetchImage: http.Get(%q): %v", url, err)
258 | }
259 | defer resp.Body.Close()
260 | img, _, err := image.Decode(resp.Body)
261 | if err != nil {
262 | return nil, fmt.Errorf("fetchImage: image decode failed: %v", err)
263 | }
264 | return img, nil
265 | }
266 |
267 | func (a *App) fetchCommits(ctx context.Context, user string) {
268 | go func() {
269 | gh := githubClient(ctx)
270 | repoCommits, _, err := gh.Repositories.ListCommits(ctx, "golang", "go", &github.CommitsListOptions{
271 | Author: user,
272 | })
273 | if err != nil {
274 | log.Printf("failed to fetch commits: %v", err)
275 | return
276 | }
277 | var commits []*github.Commit
278 | for _, commit := range repoCommits {
279 | if c := commit.GetCommit(); c != nil {
280 | commits = append(commits, c)
281 | }
282 | }
283 | a.commitsResult <- commits
284 | }()
285 | }
286 |
--------------------------------------------------------------------------------
/gophers/main_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "testing"
8 |
9 | "gioui.org/layout"
10 | "gioui.org/op"
11 | )
12 |
13 | func BenchmarkUI(b *testing.B) {
14 | fetch := func(_ string) {}
15 | u := newUI(fetch)
16 | var ops op.Ops
17 | for b.Loop() {
18 | gtx := layout.Context{
19 | Ops: &ops,
20 | Constraints: layout.Exact(image.Pt(800, 600)),
21 | }
22 | u.Layout(gtx)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/haptic/example.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "log"
9 | "os"
10 |
11 | "gioui.org/app"
12 | "gioui.org/layout"
13 | "gioui.org/op"
14 | "gioui.org/text"
15 | "gioui.org/widget"
16 | "gioui.org/widget/material"
17 |
18 | "gioui.org/font/gofont"
19 | "gioui.org/x/haptic"
20 | )
21 |
22 | var buzzer *haptic.Buzzer
23 |
24 | func main() {
25 | go func() {
26 | w := new(app.Window)
27 | if err := loop(w); err != nil {
28 | log.Fatal(err)
29 | }
30 | os.Exit(0)
31 | }()
32 | app.Main()
33 | }
34 |
35 | func loop(w *app.Window) error {
36 | th := material.NewTheme()
37 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
38 | btn := widget.Clickable{}
39 | buzzer = haptic.NewBuzzer(w)
40 | go func() {
41 | for err := range buzzer.Errors() {
42 | if err != nil {
43 | log.Printf("buzzer error: %v", err)
44 | }
45 | }
46 | }()
47 | var ops op.Ops
48 | for {
49 | switch e := w.Event().(type) {
50 | case app.DestroyEvent:
51 | return e.Err
52 | case app.FrameEvent:
53 | gtx := app.NewContext(&ops, e)
54 | if btn.Clicked(gtx) {
55 | buzzer.Buzz()
56 | }
57 | layout.Center.Layout(gtx, material.Button(th, &btn, "buzz").Layout)
58 | e.Frame(gtx.Ops)
59 | default:
60 | ProcessPlatformEvent(e)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/haptic/example_android.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "gioui.org/app"
5 | "gioui.org/io/event"
6 | )
7 |
8 | // ProcessPlatformEvent handles platform-specific event processing. If it
9 | // consumed the provided event, it returns true. In this case, no further
10 | // event processing should occur.
11 | func ProcessPlatformEvent(event event.Event) bool {
12 | if ve, ok := event.(app.ViewEvent); ok {
13 | buzzer.SetView(ve.View)
14 | return true
15 | }
16 | return false
17 | }
18 |
--------------------------------------------------------------------------------
/haptic/example_nonandroid.go:
--------------------------------------------------------------------------------
1 | //go:build !android
2 | // +build !android
3 |
4 | package main
5 |
6 | import "gioui.org/io/event"
7 |
8 | func ProcessPlatformEvent(event event.Event) bool {
9 | return false
10 | }
11 |
--------------------------------------------------------------------------------
/hello/hello.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "image/color"
9 | "log"
10 | "os"
11 |
12 | "gioui.org/app"
13 | "gioui.org/font/gofont"
14 | "gioui.org/op"
15 | "gioui.org/text"
16 | "gioui.org/widget/material"
17 | )
18 |
19 | func main() {
20 | go func() {
21 | w := new(app.Window)
22 | if err := loop(w); err != nil {
23 | log.Fatal(err)
24 | }
25 | os.Exit(0)
26 | }()
27 | app.Main()
28 | }
29 |
30 | func loop(w *app.Window) error {
31 | th := material.NewTheme()
32 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
33 | var ops op.Ops
34 | for {
35 | switch e := w.Event().(type) {
36 | case app.DestroyEvent:
37 | return e.Err
38 | case app.FrameEvent:
39 | gtx := app.NewContext(&ops, e)
40 | l := material.H1(th, "Hello, Gio")
41 | maroon := color.NRGBA{R: 127, G: 0, B: 0, A: 255}
42 | l.Color = maroon
43 | l.Alignment = text.Middle
44 | l.Layout(gtx)
45 | e.Frame(gtx.Ops)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/kitchen/main_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "testing"
8 | "time"
9 |
10 | "gioui.org/f32"
11 | "gioui.org/font/gofont"
12 | "gioui.org/gpu/headless"
13 | "gioui.org/layout"
14 | "gioui.org/op"
15 | "gioui.org/text"
16 | "gioui.org/widget/material"
17 | )
18 |
19 | func BenchmarkUI(b *testing.B) { benchmarkUI(b, transformation{}) }
20 | func BenchmarkUI_Offset(b *testing.B) { benchmarkUI(b, transformation{offset: true}) }
21 | func BenchmarkUI_Scale(b *testing.B) { benchmarkUI(b, transformation{scale: true}) }
22 | func BenchmarkUI_Rotate(b *testing.B) { benchmarkUI(b, transformation{rotate: true}) }
23 | func BenchmarkUI_All(b *testing.B) {
24 | benchmarkUI(b, transformation{offset: true, rotate: true, scale: true})
25 | }
26 |
27 | func benchmarkUI(b *testing.B, transform transformation) {
28 | th := material.NewTheme()
29 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
30 |
31 | w, err := headless.NewWindow(800, 600)
32 | if err != nil {
33 | b.Fatal(err)
34 | }
35 | defer w.Release()
36 |
37 | var layoutTime time.Duration
38 | var frameTime time.Duration
39 |
40 | var ops op.Ops
41 | for i := 0; b.Loop(); i++ {
42 | ops.Reset()
43 | gtx := layout.Context{
44 | Ops: &ops,
45 | Constraints: layout.Exact(image.Pt(800, 600)),
46 | }
47 | addTransform(i, transform, gtx.Ops)
48 | layoutTime += measure(func() { kitchen(gtx, th) })
49 | frameTime += measure(func() { w.Frame(&ops) })
50 | }
51 | b.StopTimer()
52 |
53 | b.ReportMetric(float64(layoutTime.Nanoseconds())/float64(b.N), "ns/layout")
54 | b.ReportMetric(float64(frameTime.Nanoseconds())/float64(b.N), "ns/frame")
55 | }
56 |
57 | type transformation struct {
58 | offset bool
59 | rotate bool
60 | scale bool
61 | }
62 |
63 | func addTransform(i int, transform transformation, ops *op.Ops) {
64 | if !(transform.offset || transform.rotate || transform.scale) {
65 | return
66 | }
67 | dt := float32(i)
68 | tr := f32.Affine2D{}
69 | if transform.rotate {
70 | angle := dt * .1
71 | tr = tr.Rotate(f32.Pt(300, 20), -angle)
72 | }
73 | if transform.scale {
74 | scale := max(1.0-dt*.5, 0.5)
75 | tr = tr.Scale(f32.Pt(300, 20), f32.Pt(scale, scale))
76 | }
77 | if transform.offset {
78 | offset := min(dt*50, 200)
79 | tr = tr.Offset(f32.Pt(0, offset))
80 | }
81 | op.Affine(tr).Add(ops)
82 | }
83 |
84 | func measure(fn func()) time.Duration {
85 | start := time.Now()
86 | fn()
87 | return time.Since(start)
88 | }
89 |
--------------------------------------------------------------------------------
/life/board.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "math/rand"
8 | )
9 |
10 | // Board implements game of life logic.
11 | type Board struct {
12 | // Size is the count of cells in a particular dimension.
13 | Size image.Point
14 | // Cells contains the alive or dead cells.
15 | Cells []byte
16 |
17 | // buffer is used to avoid reallocating a new cells
18 | // slice for every update.
19 | buffer []byte
20 | }
21 |
22 | // NewBoard returns a new game of life with the defined size.
23 | func NewBoard(size image.Point) *Board {
24 | return &Board{
25 | Size: size,
26 | Cells: make([]byte, size.X*size.Y),
27 | buffer: make([]byte, size.X*size.Y),
28 | }
29 | }
30 |
31 | // Randomize randomizes each cell state.
32 | func (b *Board) Randomize() {
33 | rand.Read(b.Cells)
34 | for i, v := range b.Cells {
35 | if v < 0x30 {
36 | b.Cells[i] = 1
37 | } else {
38 | b.Cells[i] = 0
39 | }
40 | }
41 | }
42 |
43 | // Pt returns the coordinate given a index in b.Cells.
44 | func (b *Board) Pt(i int) image.Point {
45 | x, y := i%b.Size.X, i/b.Size.Y
46 | return image.Point{X: x, Y: y}
47 | }
48 |
49 | // At returns the b.Cells index, given a wrapped coordinate.
50 | func (b *Board) At(c image.Point) int {
51 | if c.X < 0 {
52 | c.X += b.Size.X
53 | }
54 | if c.X >= b.Size.X {
55 | c.X -= b.Size.X
56 | }
57 | if c.Y < 0 {
58 | c.Y += b.Size.Y
59 | }
60 | if c.Y >= b.Size.Y {
61 | c.Y -= b.Size.Y
62 | }
63 | return b.Size.Y*c.Y + c.X
64 | }
65 |
66 | // SetWithoutWrap sets a cell to alive.
67 | func (b *Board) SetWithoutWrap(c image.Point) {
68 | if !c.In(image.Rectangle{Max: b.Size}) {
69 | return
70 | }
71 |
72 | b.Cells[b.At(c)] = 1
73 | }
74 |
75 | // Advance advances the board state by 1.
76 | func (b *Board) Advance() {
77 | next, cur := b.buffer, b.Cells
78 | defer func() { b.Cells, b.buffer = next, cur }()
79 |
80 | for i := range next {
81 | next[i] = 0
82 | }
83 |
84 | for y := range b.Size.Y {
85 | for x := range b.Size.X {
86 | var t byte
87 | t += cur[b.At(image.Pt(x-1, y-1))]
88 | t += cur[b.At(image.Pt(x+0, y-1))]
89 | t += cur[b.At(image.Pt(x+1, y-1))]
90 | t += cur[b.At(image.Pt(x-1, y+0))]
91 | t += cur[b.At(image.Pt(x+1, y+0))]
92 | t += cur[b.At(image.Pt(x-1, y+1))]
93 | t += cur[b.At(image.Pt(x+0, y+1))]
94 | t += cur[b.At(image.Pt(x+1, y+1))]
95 |
96 | // Any live cell with fewer than two live neighbours dies, as if by underpopulation.
97 | // Any live cell with two or three live neighbours lives on to the next generation.
98 | // Any live cell with more than three live neighbours dies, as if by overpopulation.
99 | // Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
100 |
101 | p := b.At(image.Pt(x, y))
102 | switch {
103 | case t < 2:
104 | t = 0
105 | case t == 2:
106 | t = cur[p]
107 | case t == 3:
108 | t = 1
109 | case t > 3:
110 | t = 0
111 | }
112 |
113 | next[p] = t
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/life/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "log"
8 | "os"
9 | "time"
10 |
11 | "gioui.org/app" // app contains Window handling.
12 | "gioui.org/io/event"
13 | "gioui.org/io/key" // key is used for keyboard events.
14 |
15 | // system is used for system events (e.g. closing the window).
16 | "gioui.org/layout" // layout is used for layouting widgets.
17 | "gioui.org/op" // op is used for recording different operations.
18 | "gioui.org/op/clip"
19 | "gioui.org/unit" // unit is used to define pixel-independent sizes
20 | )
21 |
22 | var (
23 | // cellSizePx is the cell size in pixels.
24 | cellSize = unit.Dp(5)
25 | // boardSize is the count of cells in a particular dimension.
26 | boardSize = image.Pt(50, 50)
27 | )
28 |
29 | func main() {
30 | // The ui loop is separated from the application window creation
31 | // such that it can be used for testing.
32 | ui := NewUI()
33 |
34 | windowWidth := cellSize * (unit.Dp(boardSize.X + 2))
35 | windowHeight := cellSize * (unit.Dp(boardSize.Y + 2))
36 | // This creates a new application window and starts the UI.
37 | go func() {
38 | w := new(app.Window)
39 | w.Option(
40 | app.Title("Game of Life"),
41 | app.Size(windowWidth, windowHeight),
42 | )
43 | if err := ui.Run(w); err != nil {
44 | log.Println(err)
45 | os.Exit(1)
46 | }
47 | os.Exit(0)
48 | }()
49 |
50 | // This starts Gio main.
51 | app.Main()
52 | }
53 |
54 | // UI holds all of the application state.
55 | type UI struct {
56 | // Board handles all game-of-life logic.
57 | Board *Board
58 | }
59 |
60 | // NewUI creates a new UI using the Go Fonts.
61 | func NewUI() *UI {
62 | // We start with a new random board.
63 | board := NewBoard(boardSize)
64 | board.Randomize()
65 |
66 | return &UI{
67 | Board: board,
68 | }
69 | }
70 |
71 | // Run handles window events and renders the application.
72 | func (ui *UI) Run(w *app.Window) error {
73 | var ops op.Ops
74 |
75 | // Update the board 3 times per second.
76 | advanceBoard := time.NewTicker(time.Second / 3)
77 | defer advanceBoard.Stop()
78 |
79 | events := make(chan event.Event)
80 | acks := make(chan struct{})
81 |
82 | go func() {
83 | for {
84 | ev := w.Event()
85 | events <- ev
86 | <-acks
87 | if _, ok := ev.(app.DestroyEvent); ok {
88 | return
89 | }
90 | }
91 | }()
92 |
93 | // listen for events happening on the window.
94 | for {
95 | select {
96 | case e := <-events:
97 | // detect the type of the event.
98 | switch e := e.(type) {
99 | // this is sent when the application should re-render.
100 | case app.FrameEvent:
101 | // gtx is used to pass around rendering and event information.
102 | gtx := app.NewContext(&ops, e)
103 | // register a global key listener for the escape key wrapping our entire UI.
104 | area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops)
105 | event.Op(gtx.Ops, w)
106 |
107 | // check for presses of the escape key and close the window if we find them.
108 | for {
109 | event, ok := gtx.Event(key.Filter{
110 | Name: key.NameEscape,
111 | })
112 | if !ok {
113 | break
114 | }
115 | switch event := event.(type) {
116 | case key.Event:
117 | if event.Name == key.NameEscape {
118 | return nil
119 | }
120 | }
121 | }
122 | // render and handle UI.
123 | ui.Layout(gtx)
124 | area.Pop()
125 | // render and handle the operations from the UI.
126 | e.Frame(gtx.Ops)
127 |
128 | // this is sent when the application is closed.
129 | case app.DestroyEvent:
130 | acks <- struct{}{}
131 | return e.Err
132 | }
133 | acks <- struct{}{}
134 |
135 | case <-advanceBoard.C:
136 | ui.Board.Advance()
137 | w.Invalidate()
138 | }
139 | }
140 | }
141 |
142 | // Layout displays the main program layout.
143 | func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
144 | return layout.Center.Layout(gtx,
145 | BoardStyle{
146 | CellSizePx: gtx.Dp(cellSize),
147 | Board: ui.Board,
148 | }.Layout,
149 | )
150 | }
151 |
--------------------------------------------------------------------------------
/life/style.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "image/color"
8 |
9 | "gioui.org/f32" // f32 is used for shape calculations.
10 | "gioui.org/io/event"
11 | "gioui.org/io/pointer" // system is used for system events (e.g. closing the window).
12 | "gioui.org/layout" // layout is used for layouting widgets.
13 |
14 | // op is used for recording different operations.
15 | "gioui.org/op/clip" // clip is used to draw the cell shape.
16 | "gioui.org/op/paint" // paint is used to paint the cells.
17 | )
18 |
19 | // BoardStyle draws Board with rectangles.
20 | type BoardStyle struct {
21 | CellSizePx int
22 | *Board
23 | }
24 |
25 | // Layout draws the Board and accepts input for adding alive cells.
26 | func (board BoardStyle) Layout(gtx layout.Context) layout.Dimensions {
27 | // Calculate the board size based on the cell size in pixels.
28 | size := board.Size.Mul(board.CellSizePx)
29 | gtx.Constraints = layout.Exact(size)
30 |
31 | // Handle any input from a pointer.
32 | for {
33 | ev, ok := gtx.Event(pointer.Filter{
34 | Target: board.Board,
35 | Kinds: pointer.Drag,
36 | })
37 | if !ok {
38 | break
39 | }
40 | if ev, ok := ev.(pointer.Event); ok {
41 | p := image.Pt(int(ev.Position.X), int(ev.Position.Y))
42 | // Calculate the board coordinate given a cursor position.
43 | p = p.Div(board.CellSizePx)
44 | board.SetWithoutWrap(p)
45 | }
46 | }
47 | // Register to listen for pointer Drag events.
48 | pr := clip.Rect(image.Rectangle{Max: size}).Push(gtx.Ops)
49 | event.Op(gtx.Ops, board.Board)
50 | pr.Pop()
51 |
52 | cellSize := float32(board.CellSizePx)
53 |
54 | // Draw a shape for each alive cell.
55 | var p clip.Path
56 | p.Begin(gtx.Ops)
57 | for i, v := range board.Cells {
58 | if v == 0 {
59 | continue
60 | }
61 |
62 | c := layout.FPt(board.Pt(i).Mul(board.CellSizePx))
63 | p.MoveTo(f32.Pt(c.X, c.Y))
64 | p.LineTo(f32.Pt(c.X+cellSize, c.Y))
65 | p.LineTo(f32.Pt(c.X+cellSize, c.Y+cellSize))
66 | p.LineTo(f32.Pt(c.X, c.Y+cellSize))
67 | p.Close()
68 | }
69 | defer clip.Outline{Path: p.End()}.Op().Push(gtx.Ops).Pop()
70 |
71 | // Paint the shape with a black color.
72 | paint.ColorOp{Color: color.NRGBA{A: 0xFF}}.Add(gtx.Ops)
73 | paint.PaintOp{}.Add(gtx.Ops)
74 |
75 | return layout.Dimensions{Size: size}
76 | }
77 |
--------------------------------------------------------------------------------
/markdown/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 | //
7 | // This program showcases markdown rendering.
8 | // The left pane contains a text editor for inputing raw text.
9 | // The right pane renders the resulting markdown document using richtext.
10 | //
11 | // Richtext is fully interactive, links can be clicked, hovered, and longpressed.
12 |
13 | import (
14 | "image"
15 | "image/color"
16 | "log"
17 | "os"
18 |
19 | "gioui.org/app"
20 | "gioui.org/font"
21 | "gioui.org/layout"
22 | "gioui.org/op"
23 | "gioui.org/op/clip"
24 | "gioui.org/op/paint"
25 | "gioui.org/text"
26 | "gioui.org/unit"
27 | "gioui.org/widget"
28 | "gioui.org/widget/material"
29 | "gioui.org/x/component"
30 | "gioui.org/x/markdown"
31 | "gioui.org/x/richtext"
32 |
33 | "gioui.org/font/gofont"
34 | "github.com/inkeliz/giohyperlink"
35 | )
36 |
37 | func main() {
38 | th := NewTheme(gofont.Collection())
39 | ui := UI{
40 | Window: new(app.Window),
41 | Renderer: markdown.NewRenderer(),
42 | Theme: th,
43 | Resize: component.Resize{Ratio: 0.5},
44 | }
45 | ui.Renderer.Config.MonospaceFont.Typeface = "Go Mono"
46 | go func() {
47 | if err := ui.Loop(); err != nil {
48 | log.Fatal(err)
49 | }
50 | os.Exit(0)
51 | }()
52 | app.Main()
53 | }
54 |
55 | type (
56 | C = layout.Context
57 | D = layout.Dimensions
58 | )
59 |
60 | // UI specifies the user interface.
61 | type UI struct {
62 | // External systems.
63 | // Window provides access to the OS window.
64 | Window *app.Window
65 | // Theme contains semantic style data. Extends `material.Theme`.
66 | Theme *Theme
67 | // Renderer tranforms raw text containing markdown into richtext.
68 | Renderer *markdown.Renderer
69 |
70 | // Core state.
71 | // Editor retains raw text in an edit buffer.
72 | Editor widget.Editor
73 | // TextState retains rich text interactions: clicks, hovers and longpresses.
74 | TextState richtext.InteractiveText
75 | // Resize state retains the split between the editor and the rendered text.
76 | component.Resize
77 | }
78 |
79 | // Theme contains semantic style data.
80 | type Theme struct {
81 | // Base theme to extend.
82 | Base *material.Theme
83 | // cache of processed markdown.
84 | cache []richtext.SpanStyle
85 | }
86 |
87 | // NewTheme instantiates a theme, extending material theme.
88 | func NewTheme(font []font.FontFace) *Theme {
89 | th := material.NewTheme()
90 | th.Shaper = text.NewShaper(text.WithCollection(font))
91 | return &Theme{
92 | Base: th,
93 | }
94 | }
95 |
96 | // Loop drives the UI until the window is destroyed.
97 | func (ui UI) Loop() error {
98 | var ops op.Ops
99 | for {
100 | e := ui.Window.Event()
101 | giohyperlink.ListenEvents(e)
102 | switch e := e.(type) {
103 | case app.DestroyEvent:
104 | return e.Err
105 | case app.FrameEvent:
106 | gtx := app.NewContext(&ops, e)
107 | ui.Layout(gtx)
108 | e.Frame(gtx.Ops)
109 | }
110 | }
111 | }
112 |
113 | // Update processes events from the previous frame, updating state accordingly.
114 | func (ui *UI) Update(gtx C) {
115 | for {
116 | o, event, ok := ui.TextState.Update(gtx)
117 | if !ok {
118 | break
119 | }
120 | switch event.Type {
121 | case richtext.Click:
122 | if url, ok := o.Get(markdown.MetadataURL).(string); ok && url != "" {
123 | if err := giohyperlink.Open(url); err != nil {
124 | // TODO(jfm): display UI element explaining the error to the user.
125 | log.Printf("error: opening hyperlink: %v", err)
126 | }
127 | }
128 | case richtext.Hover:
129 | case richtext.LongPress:
130 | log.Println("longpress")
131 | if url, ok := o.Get(markdown.MetadataURL).(string); ok && url != "" {
132 | ui.Window.Option(app.Title(url))
133 | }
134 | }
135 | }
136 | for {
137 | event, ok := ui.Editor.Update(gtx)
138 | if !ok {
139 | break
140 | }
141 | if _, ok := event.(widget.ChangeEvent); ok {
142 | var err error
143 | ui.Theme.cache, err = ui.Renderer.Render([]byte(ui.Editor.Text()))
144 | if err != nil {
145 | // TODO(jfm): display UI element explaining the error to the user.
146 | log.Printf("error: rendering markdown: %v", err)
147 | }
148 | }
149 | }
150 | }
151 |
152 | // Layout renders the current frame.
153 | func (ui *UI) Layout(gtx C) D {
154 | ui.Update(gtx)
155 | return ui.Resize.Layout(gtx,
156 | func(gtx C) D {
157 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
158 | return material.Editor(ui.Theme.Base, &ui.Editor, "markdown").Layout(gtx)
159 | })
160 | },
161 | func(gtx C) D {
162 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
163 | return richtext.Text(&ui.TextState, ui.Theme.Base.Shaper, ui.Theme.cache...).Layout(gtx)
164 | })
165 | },
166 | func(gtx C) D {
167 | rect := image.Rectangle{
168 | Max: image.Point{
169 | X: (gtx.Dp(unit.Dp(4))),
170 | Y: (gtx.Constraints.Max.Y),
171 | },
172 | }
173 | paint.FillShape(gtx.Ops, color.NRGBA{A: 200}, clip.Rect(rect).Op())
174 | return D{Size: rect.Max}
175 | },
176 | )
177 | }
178 |
--------------------------------------------------------------------------------
/multiwindow/letters.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "gioui.org/app"
7 | "gioui.org/layout"
8 | "gioui.org/widget"
9 | "gioui.org/widget/material"
10 | )
11 |
12 | // Letters displays a clickable list of text items that open a new window.
13 | type Letters struct {
14 | App *Application
15 | log *Log
16 |
17 | items []*LetterListItem
18 | list widget.List
19 | }
20 |
21 | // NewLetters creates a new letters view with the provided log.
22 | func NewLetters(log *Log) *Letters {
23 | view := &Letters{
24 | log: log,
25 | list: widget.List{List: layout.List{Axis: layout.Vertical}},
26 | }
27 | for text := 'a'; text <= 'z'; text++ {
28 | view.items = append(view.items, &LetterListItem{Text: string(text)})
29 | }
30 | return view
31 | }
32 |
33 | // Run implements Window.Run method.
34 | func (v *Letters) Run(w *Window) error {
35 | v.App = w.App
36 | return WidgetView(v.Layout).Run(w)
37 | }
38 |
39 | // Layout handles drawing the letters view.
40 | func (v *Letters) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
41 | return material.List(th, &v.list).Layout(gtx, len(v.items), func(gtx layout.Context, index int) layout.Dimensions {
42 | item := v.items[index]
43 | for item.Click.Clicked(gtx) {
44 | v.log.Printf("opening %s view", item.Text)
45 |
46 | bigText := material.H1(th, item.Text)
47 | size := 2 * gtx.Metric.SpToDp(bigText.TextSize)
48 | v.App.NewWindow(item.Text,
49 | WidgetView(func(gtx layout.Context, th *material.Theme) layout.Dimensions {
50 | return layout.Center.Layout(gtx, material.H1(th, item.Text).Layout)
51 | }),
52 | app.Size(size, size),
53 | )
54 | }
55 | return material.Button(th, &item.Click, item.Text).Layout(gtx)
56 | })
57 | }
58 |
59 | type LetterListItem struct {
60 | Text string
61 | Click widget.Clickable
62 | }
63 |
--------------------------------------------------------------------------------
/multiwindow/log.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 |
8 | "gioui.org/app"
9 | "gioui.org/font/gofont"
10 | "gioui.org/io/event"
11 | "gioui.org/io/system"
12 | "gioui.org/layout"
13 | "gioui.org/op"
14 | "gioui.org/text"
15 | "gioui.org/widget"
16 | "gioui.org/widget/material"
17 | )
18 |
19 | // Log shows a list of strings.
20 | type Log struct {
21 | addLine chan string
22 | lines []string
23 |
24 | close widget.Clickable
25 | list widget.List
26 | }
27 |
28 | // NewLog crates a new log view.
29 | func NewLog() *Log {
30 | return &Log{
31 | addLine: make(chan string, 100),
32 | list: widget.List{List: layout.List{Axis: layout.Vertical}},
33 | }
34 | }
35 |
36 | // Printf adds a new line to the log.
37 | func (log *Log) Printf(format string, args ...any) {
38 | s := fmt.Sprintf(format, args...)
39 |
40 | // ensure that this logging does not block.
41 | select {
42 | case log.addLine <- s:
43 | default:
44 | }
45 | }
46 |
47 | // Run handles window loop for the log.
48 | func (log *Log) Run(w *Window) error {
49 | var ops op.Ops
50 |
51 | th := material.NewTheme()
52 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
53 |
54 | go func() {
55 | <-w.App.Context.Done()
56 | w.Perform(system.ActionClose)
57 | }()
58 |
59 | events := make(chan event.Event)
60 | acks := make(chan struct{})
61 |
62 | go func() {
63 | for {
64 | ev := w.Event()
65 | events <- ev
66 | <-acks
67 | if _, ok := ev.(app.DestroyEvent); ok {
68 | return
69 | }
70 | }
71 | }()
72 | for {
73 | select {
74 | // listen to new lines from Printf and add them to our lines.
75 | case line := <-log.addLine:
76 | log.lines = append(log.lines, line)
77 | w.Invalidate()
78 | case e := <-events:
79 | switch e := e.(type) {
80 | case app.DestroyEvent:
81 | acks <- struct{}{}
82 | return e.Err
83 | case app.FrameEvent:
84 | gtx := app.NewContext(&ops, e)
85 | log.Layout(w, th, gtx)
86 | e.Frame(gtx.Ops)
87 | }
88 | acks <- struct{}{}
89 | }
90 | }
91 | }
92 |
93 | // Layout displays the log with a close button.
94 | func (log *Log) Layout(w *Window, th *material.Theme, gtx layout.Context) {
95 | // This is here to demonstrate programmatic closing of a window,
96 | // however it's probably better to use OS close button instead.
97 | for log.close.Clicked(gtx) {
98 | w.Window.Perform(system.ActionClose)
99 | }
100 |
101 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
102 | layout.Rigid(material.Button(th, &log.close, "Close").Layout),
103 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
104 | return material.List(th, &log.list).Layout(gtx, len(log.lines), func(gtx layout.Context, i int) layout.Dimensions {
105 | return material.Body1(th, log.lines[i]).Layout(gtx)
106 | })
107 | }),
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/multiwindow/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // This projects demonstrates one way to manage and use multiple windows.
6 | //
7 | // It shows:
8 | // * how to track multiple windows,
9 | // * how to communicate between windows,
10 | // * how to create new windows.
11 |
12 | import (
13 | "context"
14 | "os"
15 | "os/signal"
16 | "sync"
17 |
18 | "gioui.org/app"
19 | "gioui.org/font/gofont"
20 | "gioui.org/io/system"
21 | "gioui.org/layout"
22 | "gioui.org/op"
23 | "gioui.org/text"
24 | "gioui.org/widget/material"
25 | )
26 |
27 | func main() {
28 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
29 | defer stop()
30 |
31 | go func() {
32 | a := NewApplication(ctx)
33 |
34 | log := NewLog()
35 | log.Printf("[Application Started]")
36 | letters := NewLetters(log)
37 |
38 | a.NewWindow("Log", log)
39 | a.NewWindow("Letters", letters)
40 |
41 | a.Wait()
42 |
43 | os.Exit(0)
44 | }()
45 |
46 | app.Main()
47 | }
48 |
49 | // Application keeps track of all the windows and global state.
50 | type Application struct {
51 | // Context is used to broadcast application shutdown.
52 | Context context.Context
53 | // Shutdown shuts down all windows.
54 | Shutdown func()
55 | // active keeps track the open windows, such that application
56 | // can shut down, when all of them are closed.
57 | active sync.WaitGroup
58 | }
59 |
60 | func NewApplication(ctx context.Context) *Application {
61 | ctx, cancel := context.WithCancel(ctx)
62 | return &Application{
63 | Context: ctx,
64 | Shutdown: cancel,
65 | }
66 | }
67 |
68 | // Wait waits for all windows to close.
69 | func (a *Application) Wait() {
70 | a.active.Wait()
71 | }
72 |
73 | // NewWindow creates a new tracked window.
74 | func (a *Application) NewWindow(title string, view View, opts ...app.Option) {
75 | opts = append(opts, app.Title(title))
76 | a.active.Add(1)
77 | go func() {
78 | defer a.active.Done()
79 |
80 | w := &Window{
81 | App: a,
82 | Window: new(app.Window),
83 | }
84 | w.Window.Option(opts...)
85 | view.Run(w)
86 | }()
87 | }
88 |
89 | // Window holds window state.
90 | type Window struct {
91 | App *Application
92 | *app.Window
93 | }
94 |
95 | // View describes .
96 | type View interface {
97 | // Run handles the window event loop.
98 | Run(w *Window) error
99 | }
100 |
101 | // WidgetView allows to use layout.Widget as a view.
102 | type WidgetView func(gtx layout.Context, th *material.Theme) layout.Dimensions
103 |
104 | // Run displays the widget with default handling.
105 | func (view WidgetView) Run(w *Window) error {
106 | var ops op.Ops
107 |
108 | th := material.NewTheme()
109 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
110 |
111 | go func() {
112 | <-w.App.Context.Done()
113 | w.Perform(system.ActionClose)
114 | }()
115 | for {
116 | switch e := w.Event().(type) {
117 | case app.DestroyEvent:
118 | return e.Err
119 | case app.FrameEvent:
120 | gtx := app.NewContext(&ops, e)
121 | view(gtx, th)
122 | e.Frame(gtx.Ops)
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/notify/build_macos.go:
--------------------------------------------------------------------------------
1 | //go:build darwin
2 | // +build darwin
3 |
4 | //go:generate mkdir -p example.app/Contents/MacOS
5 | //go:generate go build -o example.app/Contents/MacOS/example
6 | //go:generate codesign -s - example.app
7 |
8 | package main
9 |
--------------------------------------------------------------------------------
/notify/example.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 18G103
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleExecutable
10 | example
11 | CFBundleIdentifier
12 | org.gioui
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | example
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSupportedPlatforms
22 |
23 | MacOSX
24 |
25 | CFBundleVersion
26 | 1
27 | DTCompiler
28 | com.apple.compilers.llvm.clang.1_0
29 | DTPlatformBuild
30 | 11C505
31 | DTPlatformVersion
32 | GM
33 | DTSDKBuild
34 | 19B90
35 | DTSDKName
36 | macosx10.15
37 | DTXcode
38 | 1130
39 | DTXcodeBuild
40 | 11C505
41 | LSMinimumSystemVersion
42 | 10.14
43 | NSPrincipalClass
44 | NSApplication
45 | NSSupportsAutomaticTermination
46 |
47 | NSSupportsSuddenTermination
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/notify/example.app/Contents/PkgInfo:
--------------------------------------------------------------------------------
1 | APPL????
--------------------------------------------------------------------------------
/notify/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "fmt"
9 | "os"
10 |
11 | "gioui.org/app"
12 | "gioui.org/layout"
13 | "gioui.org/op"
14 | "gioui.org/text"
15 | "gioui.org/unit"
16 |
17 | "gioui.org/widget"
18 | "gioui.org/widget/material"
19 | "gioui.org/x/component"
20 | "gioui.org/x/notify"
21 |
22 | "gioui.org/font/gofont"
23 | )
24 |
25 | type (
26 | // C quick alias for Context.
27 | C = layout.Context
28 | // D quick alias for Dimensions.
29 | D = layout.Dimensions
30 | )
31 |
32 | func main() {
33 | go func() {
34 | th := material.NewTheme()
35 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
36 | n, err := notify.NewNotifier()
37 | if err != nil {
38 | panic(fmt.Errorf("init notification manager: %w", err))
39 | }
40 | _, ongoingSupported := n.(notify.OngoingNotifier)
41 | notifier := n
42 | var editor component.TextField
43 | var notifyBtn widget.Clickable
44 | var setOngoing widget.Bool
45 | w := new(app.Window)
46 | w.Option(
47 | app.Title("notify"),
48 | app.Size(unit.Dp(800), unit.Dp(600)))
49 |
50 | var ops op.Ops
51 | for {
52 | switch event := w.Event().(type) {
53 | case app.DestroyEvent:
54 | os.Exit(0)
55 | case app.FrameEvent:
56 | gtx := app.NewContext(&ops, event)
57 | if notifyBtn.Clicked(gtx) {
58 | msg := "This is a notification send from gio."
59 | if txt := editor.Text(); txt != "" {
60 | msg = txt
61 | }
62 | if ongoingSupported && setOngoing.Value {
63 | go notifier.(notify.OngoingNotifier).CreateOngoingNotification("Hello Gio!", msg)
64 | } else {
65 | go notifier.CreateNotification("Hello Gio!", msg)
66 | }
67 | }
68 | layout.Center.Layout(gtx, func(gtx C) D {
69 | gtx.Constraints.Max.X = gtx.Dp(unit.Dp(300))
70 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
71 | layout.Rigid(func(gtx C) D {
72 | return editor.Layout(gtx, th, "enter a notification message")
73 | }),
74 | layout.Rigid(func(gtx C) D {
75 | return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
76 | }),
77 | layout.Rigid(func(gtx C) D {
78 | return material.Button(th, ¬ifyBtn, "notify").Layout(gtx)
79 | }),
80 | layout.Rigid(func(gtx C) D {
81 | return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx)
82 | }),
83 | layout.Rigid(func(gtx C) D {
84 | if !ongoingSupported {
85 | return D{}
86 | }
87 | return material.CheckBox(th, &setOngoing, "ongoing").Layout(gtx)
88 | }),
89 | )
90 | })
91 | event.Frame(gtx.Ops)
92 | }
93 | }
94 | }()
95 | app.Main()
96 | }
97 |
--------------------------------------------------------------------------------
/opacity/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "os"
9 |
10 | "gioui.org/app"
11 | "gioui.org/font/gofont"
12 | "gioui.org/layout"
13 | "gioui.org/op"
14 | "gioui.org/op/paint"
15 | "gioui.org/text"
16 | "gioui.org/widget"
17 | "gioui.org/widget/material"
18 | )
19 |
20 | func main() {
21 | go func() {
22 | w := new(app.Window)
23 | if err := loop(w); err != nil {
24 | log.Fatal(err)
25 | }
26 | os.Exit(0)
27 | }()
28 | app.Main()
29 | }
30 |
31 | type (
32 | C = layout.Context
33 | D = layout.Dimensions
34 | )
35 |
36 | type opacityViewStyle struct {
37 | control *widget.Float
38 | slider material.SliderStyle
39 | value material.LabelStyle
40 | padding layout.Inset
41 | }
42 |
43 | func opacityView(th *material.Theme, state *widget.Float) opacityViewStyle {
44 | return opacityViewStyle{
45 | slider: material.Slider(th, state),
46 | padding: layout.UniformInset(12),
47 | value: material.Body1(th, fmt.Sprintf("%.2f", state.Value)),
48 | }
49 | }
50 |
51 | func (o opacityViewStyle) Layout(gtx C, w layout.Widget) D {
52 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
53 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
54 | return o.padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
55 | return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
56 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
57 | return o.value.Layout(gtx)
58 | }),
59 | layout.Rigid(layout.Spacer{Width: o.padding.Left}.Layout),
60 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
61 | return o.slider.Layout(gtx)
62 | }),
63 | )
64 | })
65 | }),
66 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
67 | defer paint.PushOpacity(gtx.Ops, o.slider.Float.Value).Pop()
68 | return w(gtx)
69 | }),
70 | )
71 | }
72 |
73 | func loop(w *app.Window) error {
74 | th := material.NewTheme()
75 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
76 | var ops op.Ops
77 | var outer, inner widget.Float
78 | outer.Value = .75
79 | inner.Value = .5
80 | for {
81 | switch e := w.Event().(type) {
82 | case app.DestroyEvent:
83 | return e.Err
84 | case app.FrameEvent:
85 | gtx := app.NewContext(&ops, e)
86 | opacityView(th, &outer).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
87 | return opacityView(th, &inner).Layout(gtx,
88 | func(gtx layout.Context) layout.Dimensions {
89 | return material.Loader(th).Layout(gtx)
90 | })
91 | })
92 | e.Frame(gtx.Ops)
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/opengl/display_angle.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | //go:build windows || darwin
4 | // +build windows darwin
5 |
6 | package main
7 |
8 | import (
9 | "strings"
10 |
11 | "gioui.org/app"
12 | )
13 |
14 | /*
15 | #cgo CFLAGS: -DEGL_NO_X11
16 | #cgo LDFLAGS: -lEGL -lGLESv2
17 |
18 | #include
19 | #include
20 | #define EGL_EGLEXT_PROTOTYPES
21 | #include
22 |
23 | */
24 | import "C"
25 |
26 | func getDisplay(_ app.ViewEvent) C.EGLDisplay {
27 | var EGL_NO_DISPLAY C.EGLDisplay
28 | platformExts := strings.Split(C.GoString(C.eglQueryString(EGL_NO_DISPLAY, C.EGL_EXTENSIONS)), " ")
29 | platformType := C.EGLint(C.EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE)
30 | if hasExtension(platformExts, "EGL_ANGLE_platform_angle_metal") {
31 | // The Metal backend works better than the OpenGL backend.
32 | platformType = C.EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE
33 | }
34 | attrs := []C.EGLint{
35 | C.EGL_PLATFORM_ANGLE_TYPE_ANGLE,
36 | platformType,
37 | C.EGL_NONE,
38 | }
39 | return C.eglGetPlatformDisplayEXT(C.EGL_PLATFORM_ANGLE_ANGLE, nil, &attrs[0])
40 | }
41 |
--------------------------------------------------------------------------------
/opengl/egl_linux.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "unsafe"
8 |
9 | "gioui.org/app"
10 | )
11 |
12 | /*
13 | #cgo linux pkg-config: egl wayland-egl
14 | #cgo CFLAGS: -DEGL_NO_X11
15 | #cgo LDFLAGS: -lEGL -lGLESv2
16 |
17 | #include
18 | #include
19 | #include
20 | #include
21 | #define EGL_EGLEXT_PROTOTYPES
22 | #include
23 |
24 | */
25 | import "C"
26 |
27 | func getDisplay(ve app.ViewEvent) C.EGLDisplay {
28 | switch ve := ve.(type) {
29 | case app.X11ViewEvent:
30 | return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
31 | case app.WaylandViewEvent:
32 | return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
33 | }
34 | panic("no display available")
35 | }
36 |
37 | func nativeViewFor(e app.ViewEvent, size image.Point) (C.EGLNativeWindowType, func()) {
38 | switch e := e.(type) {
39 | case app.X11ViewEvent:
40 | return C.EGLNativeWindowType(uintptr(e.Window)), func() {}
41 | case app.WaylandViewEvent:
42 | eglWin := C.wl_egl_window_create((*C.struct_wl_surface)(e.Surface), C.int(size.X), C.int(size.Y))
43 | return C.EGLNativeWindowType(uintptr(unsafe.Pointer(eglWin))), func() {
44 | C.wl_egl_window_destroy(eglWin)
45 | }
46 | }
47 | panic("no native view available")
48 | }
49 |
50 | func nativeViewResize(e app.ViewEvent, view C.EGLNativeWindowType, newSize image.Point) {
51 | switch e.(type) {
52 | case app.X11ViewEvent:
53 | case app.WaylandViewEvent:
54 | C.wl_egl_window_resize(*(**C.struct_wl_egl_window)(unsafe.Pointer(&view)), C.int(newSize.X), C.int(newSize.Y), 0, 0)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/opengl/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | //go:build darwin || windows || linux
4 | // +build darwin windows linux
5 |
6 | // This program demonstrates the use of a custom OpenGL ES context with
7 | // app.Window. It is similar to the GLFW example, but uses Gio's window
8 | // implementation instead of the one in GLFW.
9 | //
10 | // The example runs on Linux using the normal EGL and X11 libraries, so
11 | // no additional libraries need to be installed. However, it must be
12 | // build with -tags nowayland until app.ViewEvent is implemented for
13 | // Wayland.
14 | //
15 | // The example runs on macOS and Windows using ANGLE:
16 | //
17 | // $ CGO_CFLAGS=-I/include CGO_LDFLAGS=-L go build -o opengl.exe ./opengl
18 | //
19 | // You'll need the ANGLE libraries (EGL and GLESv2) in your library search path. On macOS:
20 | //
21 | // $ DYLD_LIBRARY_PATH= ./opengl.exe
22 | package main
23 |
24 | import (
25 | "bytes"
26 | "errors"
27 | "fmt"
28 | "image"
29 | "image/png"
30 | "log"
31 | "os"
32 | "runtime"
33 | "strings"
34 | "unsafe"
35 |
36 | "gioui.org/app"
37 | "gioui.org/gpu"
38 | "gioui.org/io/event"
39 | "gioui.org/io/pointer"
40 | "gioui.org/io/system"
41 | "gioui.org/layout"
42 | "gioui.org/op"
43 | "gioui.org/text"
44 | "gioui.org/widget"
45 | "gioui.org/widget/material"
46 |
47 | "gioui.org/font/gofont"
48 | )
49 |
50 | /*
51 | #cgo CFLAGS: -DEGL_NO_X11
52 | #cgo LDFLAGS: -lEGL -lGLESv2
53 |
54 | #include
55 | #include
56 | #define EGL_EGLEXT_PROTOTYPES
57 | #include
58 |
59 | */
60 | import "C"
61 |
62 | type eglContext struct {
63 | view C.EGLNativeWindowType
64 | disp C.EGLDisplay
65 | ctx C.EGLContext
66 | surf C.EGLSurface
67 | cleanup func()
68 | }
69 |
70 | func main() {
71 | go func() {
72 | // Set CustomRenderer so we can provide our own rendering context.
73 | w := new(app.Window)
74 | w.Option(app.CustomRenderer(true))
75 | if err := loop(w); err != nil {
76 | log.Fatal(err)
77 | }
78 | os.Exit(0)
79 | }()
80 | app.Main()
81 | }
82 |
83 | var btnScreenshot widget.Clickable
84 |
85 | func discardContext(w *app.Window, gioGPU gpu.GPU, eglCtx *eglContext) error {
86 | var err error
87 | w.Run(func() {
88 | if gioGPU != nil {
89 | gioGPU.Release()
90 | }
91 | if eglCtx != nil {
92 | if ok := C.eglMakeCurrent(eglCtx.disp, eglCtx.surf, eglCtx.surf, eglCtx.ctx); ok != C.EGL_TRUE {
93 | err = fmt.Errorf("eglMakeCurrent failed (%#x)", C.eglGetError())
94 | } else {
95 | eglCtx.Release()
96 | }
97 | }
98 | })
99 | return err
100 | }
101 |
102 | func recreateContext(w *app.Window, ve app.ViewEvent, gioGPU gpu.GPU, eglCtx *eglContext, size image.Point) (gpu.GPU, *eglContext, error) {
103 | err := discardContext(w, gioGPU, eglCtx)
104 | gioGPU = nil
105 | eglCtx = nil
106 | if err != nil {
107 | return nil, nil, err
108 | }
109 | w.Run(func() {
110 | eglCtx, err = createContext(ve, size)
111 | if err != nil {
112 | err = fmt.Errorf("failed creating context: %w", err)
113 | }
114 | })
115 | if err != nil {
116 | return nil, nil, err
117 | }
118 | if ok := C.eglMakeCurrent(eglCtx.disp, eglCtx.surf, eglCtx.surf, eglCtx.ctx); ok != C.EGL_TRUE {
119 | return gioGPU, eglCtx, fmt.Errorf("eglMakeCurrent failed (%#x)", C.eglGetError())
120 | }
121 | glGetString := func(e C.GLenum) string {
122 | return C.GoString((*C.char)(unsafe.Pointer(C.glGetString(e))))
123 | }
124 | fmt.Printf("GL_VERSION: %s\nGL_RENDERER: %s\n", glGetString(C.GL_VERSION), glGetString(C.GL_RENDERER))
125 | gioGPU, err = gpu.New(gpu.OpenGL{ES: true, Shared: true})
126 | return gioGPU, eglCtx, err
127 | }
128 |
129 | func loop(w *app.Window) (windowErr error) {
130 | // eglMakeCurrent binds a context to an operating system thread. Prevent Go from switching thread.
131 | runtime.LockOSThread()
132 | defer runtime.UnlockOSThread()
133 | th := material.NewTheme()
134 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
135 | var ops op.Ops
136 | var (
137 | ctx *eglContext
138 | gioCtx gpu.GPU
139 | ve app.ViewEvent
140 | size image.Point
141 | )
142 |
143 | for {
144 | switch e := w.Event().(type) {
145 | case app.ViewEvent:
146 | ve = e
147 | err := discardContext(w, gioCtx, ctx)
148 | windowErr = errors.Join(windowErr, err)
149 | gioCtx = nil
150 | ctx = nil
151 | case app.DestroyEvent:
152 | windowErr = errors.Join(windowErr, e.Err)
153 | return windowErr
154 | case app.FrameEvent:
155 | resized := e.Size != size
156 | size = e.Size
157 | if ve.Valid() && size != (image.Point{}) && gioCtx == nil {
158 | var err error
159 | gioCtx, ctx, err = recreateContext(w, ve, gioCtx, ctx, size)
160 | windowErr = errors.Join(windowErr, err)
161 | } else if ve.Valid() && resized {
162 | ctx.resizeSurface(ve, size)
163 | }
164 | if windowErr != nil {
165 | w.Perform(system.ActionClose)
166 | continue
167 | }
168 | // Build ops.
169 | gtx := app.NewContext(&ops, e)
170 | // Catch pointer events not hitting UI.
171 | types := pointer.Move | pointer.Press | pointer.Release
172 | event.Op(gtx.Ops, w)
173 | for {
174 | e, ok := gtx.Event(pointer.Filter{
175 | Target: w,
176 | Kinds: types,
177 | })
178 | if !ok {
179 | break
180 | }
181 | log.Println("Event:", e)
182 | }
183 | if btnScreenshot.Clicked(gtx) {
184 | if err := screenshot(gioCtx, e.Size, gtx.Ops); err != nil {
185 | log.Printf("failed taking screenshot: %v", err)
186 | }
187 | }
188 |
189 | drawUI(th, gtx)
190 | // Trigger window resize detection in ANGLE.
191 | C.eglWaitClient()
192 | // Draw custom OpenGL content.
193 | drawGL()
194 |
195 | // Render drawing ops.
196 | if err := gioCtx.Frame(gtx.Ops, gpu.OpenGLRenderTarget{}, e.Size); err != nil {
197 | windowErr = errors.Join(windowErr, fmt.Errorf("render failed: %v", err))
198 | continue
199 | }
200 |
201 | if ok := C.eglSwapBuffers(ctx.disp, ctx.surf); ok != C.EGL_TRUE {
202 | windowErr = errors.Join(windowErr, fmt.Errorf("swap failed: 0x%x", C.eglGetError()))
203 | continue
204 | }
205 |
206 | // Process non-drawing ops.
207 | e.Frame(gtx.Ops)
208 | }
209 | }
210 | }
211 |
212 | func screenshot(ctx gpu.GPU, size image.Point, ops *op.Ops) error {
213 | var tex C.GLuint
214 | C.glGenTextures(1, &tex)
215 | defer C.glDeleteTextures(1, &tex)
216 | C.glBindTexture(C.GL_TEXTURE_2D, tex)
217 | C.glTexImage2D(C.GL_TEXTURE_2D, 0, C.GL_RGBA, C.GLint(size.X), C.GLint(size.Y), 0, C.GL_RGBA, C.GL_UNSIGNED_BYTE, nil)
218 | var fbo C.GLuint
219 | C.glGenFramebuffers(1, &fbo)
220 | defer C.glDeleteFramebuffers(1, &fbo)
221 | C.glBindFramebuffer(C.GL_FRAMEBUFFER, fbo)
222 | defer C.glBindFramebuffer(C.GL_FRAMEBUFFER, 0)
223 | C.glFramebufferTexture2D(C.GL_FRAMEBUFFER, C.GL_COLOR_ATTACHMENT0, C.GL_TEXTURE_2D, tex, 0)
224 | if st := C.glCheckFramebufferStatus(C.GL_FRAMEBUFFER); st != C.GL_FRAMEBUFFER_COMPLETE {
225 | return fmt.Errorf("screenshot: framebuffer incomplete (%#x)", st)
226 | }
227 | drawGL()
228 | if err := ctx.Frame(ops, gpu.OpenGLRenderTarget{V: uint(fbo)}, size); err != nil {
229 | return fmt.Errorf("screenshot: %w", err)
230 | }
231 | r := image.Rectangle{Max: size}
232 | ss := image.NewRGBA(r)
233 | C.glReadPixels(C.GLint(r.Min.X), C.GLint(r.Min.Y), C.GLint(r.Dx()), C.GLint(r.Dy()), C.GL_RGBA, C.GL_UNSIGNED_BYTE, unsafe.Pointer(&ss.Pix[0]))
234 | var buf bytes.Buffer
235 | if err := png.Encode(&buf, ss); err != nil {
236 | return fmt.Errorf("screenshot: %w", err)
237 | }
238 | const file = "screenshot.png"
239 | if err := os.WriteFile(file, buf.Bytes(), 0o644); err != nil {
240 | return fmt.Errorf("screenshot: %w", err)
241 | }
242 | fmt.Printf("wrote %q\n", file)
243 | return nil
244 | }
245 |
246 | func drawGL() {
247 | C.glClearColor(.5, .5, 0, 1)
248 | C.glClear(C.GL_COLOR_BUFFER_BIT | C.GL_DEPTH_BUFFER_BIT)
249 | }
250 |
251 | func drawUI(th *material.Theme, gtx layout.Context) layout.Dimensions {
252 | return layout.Center.Layout(gtx,
253 | material.Button(th, &btnScreenshot, "Screenshot").Layout,
254 | )
255 | }
256 |
257 | func createContext(ve app.ViewEvent, size image.Point) (*eglContext, error) {
258 | view, cleanup := nativeViewFor(ve, size)
259 | var nilv C.EGLNativeWindowType
260 | if view == nilv {
261 | return nil, fmt.Errorf("failed creating native view")
262 | }
263 | disp := getDisplay(ve)
264 | if disp == 0 {
265 | return nil, fmt.Errorf("eglGetPlatformDisplay failed: 0x%x", C.eglGetError())
266 | }
267 | var major, minor C.EGLint
268 | if ok := C.eglInitialize(disp, &major, &minor); ok != C.EGL_TRUE {
269 | return nil, fmt.Errorf("eglInitialize failed: 0x%x", C.eglGetError())
270 | }
271 | exts := strings.Split(C.GoString(C.eglQueryString(disp, C.EGL_EXTENSIONS)), " ")
272 | srgb := hasExtension(exts, "EGL_KHR_gl_colorspace")
273 | attribs := []C.EGLint{
274 | C.EGL_RENDERABLE_TYPE, C.EGL_OPENGL_ES2_BIT,
275 | C.EGL_SURFACE_TYPE, C.EGL_WINDOW_BIT,
276 | C.EGL_BLUE_SIZE, 8,
277 | C.EGL_GREEN_SIZE, 8,
278 | C.EGL_RED_SIZE, 8,
279 | C.EGL_CONFIG_CAVEAT, C.EGL_NONE,
280 | }
281 | if srgb {
282 | // Some drivers need alpha for sRGB framebuffers to work.
283 | attribs = append(attribs, C.EGL_ALPHA_SIZE, 8)
284 | }
285 | attribs = append(attribs, C.EGL_NONE)
286 | var (
287 | cfg C.EGLConfig
288 | numCfgs C.EGLint
289 | )
290 | if ok := C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &numCfgs); ok != C.EGL_TRUE {
291 | return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", C.eglGetError())
292 | }
293 | if numCfgs == 0 {
294 | supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
295 | if !supportsNoCfg {
296 | return nil, errors.New("eglChooseConfig returned no configs")
297 | }
298 | }
299 | ctxAttribs := []C.EGLint{
300 | C.EGL_CONTEXT_CLIENT_VERSION, 3,
301 | C.EGL_NONE,
302 | }
303 | ctx := C.eglCreateContext(disp, cfg, nil, &ctxAttribs[0])
304 | if ctx == nil {
305 | return nil, fmt.Errorf("eglCreateContext failed: 0x%x", C.eglGetError())
306 | }
307 | var surfAttribs []C.EGLint
308 | if srgb {
309 | surfAttribs = append(surfAttribs, C.EGL_GL_COLORSPACE, C.EGL_GL_COLORSPACE_SRGB)
310 | }
311 | surfAttribs = append(surfAttribs, C.EGL_NONE)
312 | surf := C.eglCreateWindowSurface(disp, cfg, view, &surfAttribs[0])
313 | if surf == nil {
314 | return nil, fmt.Errorf("eglCreateWindowSurface failed (0x%x)", C.eglGetError())
315 | }
316 | e := &eglContext{
317 | view: view,
318 | disp: disp,
319 | ctx: ctx,
320 | surf: surf,
321 | cleanup: cleanup,
322 | }
323 | return e, nil
324 | }
325 |
326 | func (c *eglContext) resizeSurface(ve app.ViewEvent, size image.Point) {
327 | nativeViewResize(ve, c.view, size)
328 | }
329 |
330 | func (c *eglContext) Release() {
331 | if c.ctx != nil {
332 | C.eglDestroyContext(c.disp, c.ctx)
333 | }
334 | if c.surf != nil {
335 | C.eglDestroySurface(c.disp, c.surf)
336 | }
337 | if c.cleanup != nil {
338 | c.cleanup()
339 | }
340 | *c = eglContext{}
341 | }
342 |
343 | func hasExtension(exts []string, ext string) bool {
344 | for _, e := range exts {
345 | if ext == e {
346 | return true
347 | }
348 | }
349 | return false
350 | }
351 |
--------------------------------------------------------------------------------
/opengl/view_darwin.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 |
8 | "gioui.org/app"
9 | )
10 |
11 | /*
12 | #include
13 | */
14 | import "C"
15 |
16 | func nativeViewFor(e app.ViewEvent, _ image.Point) (C.EGLNativeWindowType, func()) {
17 | return C.EGLNativeWindowType(e.(app.AppKitViewEvent).Layer), func() {}
18 | }
19 |
20 | func nativeViewResize(e app.ViewEvent, view C.EGLNativeWindowType, newSize image.Point) {}
21 |
--------------------------------------------------------------------------------
/opengl/view_windows.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "unsafe"
8 |
9 | "gioui.org/app"
10 | )
11 |
12 | /*
13 | #include
14 | */
15 | import "C"
16 |
17 | func nativeViewFor(e app.ViewEvent, _ image.Point) (C.EGLNativeWindowType, func()) {
18 | return C.EGLNativeWindowType(unsafe.Pointer(e.(app.Win32ViewEvent).HWND)), func() {}
19 | }
20 | func nativeViewResize(e app.ViewEvent, view C.EGLNativeWindowType, newSize image.Point) {}
21 |
--------------------------------------------------------------------------------
/outlay/fan/cribbage/cmd/crib.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "gioui.org/example/outlay/fan/cribbage"
7 | )
8 |
9 | func main() {
10 | g := cribbage.NewGame(2)
11 | fmt.Println(g)
12 | g.DealRound()
13 | fmt.Println(g)
14 | g.Sacrifice(0, 0)
15 | g.Sacrifice(0, 4)
16 | g.Sacrifice(1, 0)
17 | g.Sacrifice(1, 4)
18 | fmt.Println(g)
19 | g.CutAt(10)
20 | fmt.Println(g)
21 | g.Reset()
22 | fmt.Println(g)
23 | g.DealRound()
24 | fmt.Println(g)
25 | }
26 |
--------------------------------------------------------------------------------
/outlay/fan/cribbage/cribbage.go:
--------------------------------------------------------------------------------
1 | package cribbage
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "slices"
7 |
8 | "gioui.org/example/outlay/fan/playing"
9 | )
10 |
11 | type Phase uint8
12 |
13 | const (
14 | BetweenHands Phase = iota
15 | Dealing
16 | Sacrifice
17 | Cut
18 | CircularCount
19 | CountHands
20 | CountCrib
21 | )
22 |
23 | func (p Phase) String() string {
24 | switch p {
25 | case BetweenHands:
26 | return "between"
27 | case Dealing:
28 | return "dealing"
29 | case Sacrifice:
30 | return "sacrifice"
31 | case Cut:
32 | return "cut"
33 | case CircularCount:
34 | return "circular count"
35 | case CountHands:
36 | return "count hands"
37 | case CountCrib:
38 | return "count crib"
39 | default:
40 | return "unknown"
41 | }
42 | }
43 |
44 | type Game struct {
45 | Phase
46 | Deck []playing.Card
47 | CutCard *playing.Card
48 | Dealer int
49 | Crib []playing.Card
50 | Players []Player
51 | }
52 |
53 | type Player struct {
54 | Hand, Table []playing.Card
55 | }
56 |
57 | func (p Player) String() string {
58 | return fmt.Sprintf("[Hand: %s, Table: %s]", p.Hand, p.Table)
59 | }
60 |
61 | func (g Game) String() string {
62 | return fmt.Sprintf("[Phase: %v\nDealer: %v\nCrib: %v\nCut: %v\nPlayers: %v\nDeck: %v]\n", g.Phase, g.Dealer, g.Crib, g.CutCard, g.Players, g.Deck)
63 | }
64 |
65 | const MinHand = 4
66 |
67 | func NewGame(players int) Game {
68 | var g Game
69 | g.Players = make([]Player, players)
70 | g.Dealer = g.NumPlayers() - 1
71 | for i := range 4 {
72 | for j := range 13 {
73 | g.Deck = append(g.Deck, playing.Card{
74 | Suit: playing.Suit(i),
75 | Rank: playing.Rank(j),
76 | })
77 | }
78 | }
79 | g.Phase = Dealing
80 | return g
81 | }
82 |
83 | func (g Game) NumPlayers() int {
84 | return len(g.Players)
85 | }
86 |
87 | func (g Game) Right(player int) int {
88 | return (player + g.NumPlayers() - 1) % g.NumPlayers()
89 | }
90 |
91 | func (g Game) Left(player int) int {
92 | return (player + 1) % g.NumPlayers()
93 | }
94 |
95 | func (g *Game) CutAt(depth int) {
96 | g.CutCard = &g.Deck[depth]
97 | g.Phase = CircularCount
98 | }
99 |
100 | func DrainInto(src, dest *[]playing.Card) {
101 | for _, c := range *src {
102 | *dest = append(*dest, c)
103 | }
104 | *src = (*src)[:0]
105 | }
106 |
107 | func (g *Game) Reset() {
108 | for i := range g.Players {
109 | DrainInto(&(g.Players[i].Hand), &g.Deck)
110 | DrainInto(&(g.Players[i].Table), &g.Deck)
111 | }
112 | DrainInto(&(g.Crib), &g.Deck)
113 | g.Phase = Dealing
114 | g.CutCard = nil
115 | }
116 |
117 | func (g *Game) DealCardTo(dest *[]playing.Card) {
118 | card := g.Deck[0]
119 | g.Deck = g.Deck[1:]
120 | *dest = append(*dest, card)
121 | }
122 |
123 | func (g *Game) DealRound() {
124 | g.Dealer = g.Left(g.Dealer)
125 | g.Reset()
126 | g.Shuffle()
127 | for range g.CardsToDealPerPlayer() {
128 | for i := range g.Players {
129 | g.DealCardTo(&(g.Players[i].Hand))
130 | }
131 | }
132 | for range g.CardsDealtToCrib() {
133 | g.DealCardTo(&g.Crib)
134 | }
135 | g.Phase = Sacrifice
136 | }
137 |
138 | func (g Game) CardsToDealPerPlayer() int {
139 | switch g.NumPlayers() {
140 | case 2:
141 | return 6
142 | case 3:
143 | return 5
144 | case 4:
145 | return 5
146 | default:
147 | return 0
148 | }
149 | }
150 |
151 | func (g Game) CardsDealtToCrib() int {
152 | if g.NumPlayers() == 3 {
153 | return 1
154 | }
155 | return 0
156 | }
157 |
158 | func (g *Game) Shuffle() {
159 | rand.Shuffle(len(g.Deck), func(i, j int) {
160 | g.Deck[i], g.Deck[j] = g.Deck[j], g.Deck[i]
161 | })
162 | }
163 |
164 | func (g *Game) Sacrifice(player, card int) {
165 | hand := g.Players[player].Hand
166 | if len(hand) <= MinHand {
167 | return
168 | }
169 | c := hand[card]
170 | g.Players[player].Hand = slices.Delete(hand, card, card+1)
171 | g.Crib = append(g.Crib, c)
172 | }
173 |
--------------------------------------------------------------------------------
/outlay/fan/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "math"
6 | "math/rand"
7 | "os"
8 | "time"
9 |
10 | "gioui.org/app"
11 | "gioui.org/example/outlay/fan/playing"
12 | xwidget "gioui.org/example/outlay/fan/widget"
13 | "gioui.org/example/outlay/fan/widget/boring"
14 | "gioui.org/font/gofont"
15 | "gioui.org/layout"
16 | "gioui.org/op"
17 | "gioui.org/text"
18 | "gioui.org/unit"
19 | "gioui.org/widget"
20 | "gioui.org/widget/material"
21 | "gioui.org/x/outlay"
22 | )
23 |
24 | type (
25 | C = layout.Context
26 | D = layout.Dimensions
27 | )
28 |
29 | func main() {
30 | go func() {
31 | w := new(app.Window)
32 | if err := loop(w); err != nil {
33 | log.Fatal(err)
34 | }
35 | os.Exit(0)
36 | }()
37 | app.Main()
38 | }
39 |
40 | func genCards(th *material.Theme) []boring.HoverCard {
41 | cards := []boring.HoverCard{}
42 | max := 30
43 | deck := playing.Deck()
44 | rand.Shuffle(len(deck), func(i, j int) {
45 | deck[i], deck[j] = deck[j], deck[i]
46 | })
47 | for i := range max {
48 | cards = append(cards, boring.HoverCard{
49 | CardStyle: boring.CardStyle{
50 | Card: deck[i],
51 | Theme: th,
52 | Height: unit.Dp(200),
53 | },
54 | HoverState: &xwidget.HoverState{},
55 | })
56 | }
57 | return cards
58 | }
59 |
60 | func loop(w *app.Window) error {
61 | th := material.NewTheme()
62 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
63 | fan := outlay.Fan{
64 | Animation: outlay.Animation{
65 | Duration: time.Second / 4,
66 | },
67 | WidthRadians: math.Pi,
68 | OffsetRadians: 2 * math.Pi,
69 | }
70 | numCards := widget.Float{}
71 | numCards.Value = 1.0
72 | var width, offset, radius widget.Float
73 | var useRadius widget.Bool
74 | cardChildren := []outlay.FanItem{}
75 | cards := genCards(th)
76 | for i := range cards {
77 | cardChildren = append(cardChildren, outlay.Item(i == 5, cards[i].Layout))
78 | }
79 | var ops op.Ops
80 | for {
81 | switch e := w.Event().(type) {
82 | case app.DestroyEvent:
83 | return e.Err
84 | case app.FrameEvent:
85 | gtx := app.NewContext(&ops, e)
86 | for i := range cards {
87 | cardChildren[i].Elevate = cards[i].Hovering(gtx)
88 | }
89 | visibleCards := int(math.Round(float64(numCards.Value*float32(len(cardChildren)-1)))) + 1
90 | fan.OffsetRadians = offset.Value * 2 * math.Pi
91 | fan.WidthRadians = width.Value * 2 * math.Pi
92 | if useRadius.Update(gtx) || radius.Update(gtx) {
93 | if useRadius.Value {
94 | r := cards[0].Height * unit.Dp(radius.Value*2)
95 | fan.HollowRadius = &r
96 | } else {
97 | fan.HollowRadius = nil
98 | }
99 | }
100 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
101 | layout.Rigid(func(gtx C) D {
102 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
103 | layout.Rigid(func(gtx C) D {
104 | return material.Body1(th, "1").Layout(gtx)
105 | }),
106 | layout.Flexed(1, func(gtx C) D {
107 | return material.Slider(th, &numCards).Layout(gtx)
108 | }),
109 | layout.Rigid(func(gtx C) D {
110 | return material.Body1(th, "10").Layout(gtx)
111 | }),
112 | )
113 | }),
114 | layout.Rigid(func(gtx C) D {
115 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
116 | layout.Rigid(func(gtx C) D {
117 | return material.Body1(th, "width 0").Layout(gtx)
118 | }),
119 | layout.Flexed(1, func(gtx C) D {
120 | return material.Slider(th, &width).Layout(gtx)
121 | }),
122 | layout.Rigid(func(gtx C) D {
123 | return material.Body1(th, "2pi").Layout(gtx)
124 | }),
125 | )
126 | }),
127 | layout.Rigid(func(gtx C) D {
128 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
129 | layout.Rigid(func(gtx C) D {
130 | return material.Body1(th, "offset 0").Layout(gtx)
131 | }),
132 | layout.Flexed(1, func(gtx C) D {
133 | return material.Slider(th, &offset).Layout(gtx)
134 | }),
135 | layout.Rigid(func(gtx C) D {
136 | return material.Body1(th, "2pi").Layout(gtx)
137 | }),
138 | )
139 | }),
140 | layout.Rigid(func(gtx C) D {
141 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
142 | layout.Rigid(func(gtx C) D {
143 | return material.CheckBox(th, &useRadius, "use").Layout(gtx)
144 | }),
145 | layout.Rigid(func(gtx C) D {
146 | return material.Body1(th, "radius 0%").Layout(gtx)
147 | }),
148 | layout.Flexed(1, func(gtx C) D {
149 | return material.Slider(th, &radius).Layout(gtx)
150 | }),
151 | layout.Rigid(func(gtx C) D {
152 | return material.Body1(th, "200%").Layout(gtx)
153 | }),
154 | )
155 | }),
156 | layout.Flexed(1, func(gtx C) D {
157 | return fan.Layout(gtx, cardChildren[:visibleCards]...)
158 | }),
159 | )
160 | e.Frame(gtx.Ops)
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/outlay/fan/playing/card.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package playing provides types for modeling a deck of conventional
3 | playing cards.
4 | */
5 | package playing
6 |
7 | type (
8 | Suit uint8
9 | Rank uint8
10 | Color bool
11 | )
12 |
13 | const (
14 | Spades Suit = iota
15 | Clubs
16 | Hearts
17 | Diamonds
18 | UnknownSuit
19 | )
20 |
21 | const (
22 | Ace Rank = iota
23 | Two
24 | Three
25 | Four
26 | Five
27 | Six
28 | Seven
29 | Eight
30 | Nine
31 | Ten
32 | Jack
33 | Queen
34 | King
35 | UnknownRank
36 | )
37 |
38 | const (
39 | Red Color = true
40 | Black Color = false
41 | )
42 |
43 | type Card struct {
44 | Suit
45 | Rank
46 | }
47 |
48 | func Deck() []Card {
49 | d := make([]Card, 0, 52)
50 | for i := range 4 {
51 | for k := range 13 {
52 | d = append(d, Card{
53 | Suit: Suit(i),
54 | Rank: Rank(k),
55 | })
56 | }
57 | }
58 | return d
59 | }
60 |
61 | func (r Rank) String() string {
62 | switch r {
63 | case Ace:
64 | return "A"
65 | case Two:
66 | return "2"
67 | case Three:
68 | return "3"
69 | case Four:
70 | return "4"
71 | case Five:
72 | return "5"
73 | case Six:
74 | return "6"
75 | case Seven:
76 | return "7"
77 | case Eight:
78 | return "8"
79 | case Nine:
80 | return "9"
81 | case Ten:
82 | return "10"
83 | case Jack:
84 | return "J"
85 | case Queen:
86 | return "Q"
87 | case King:
88 | return "K"
89 | default:
90 | return "?"
91 | }
92 | }
93 |
94 | func (s Suit) String() string {
95 | switch s {
96 | case Spades:
97 | return "♠"
98 | case Hearts:
99 | return "♥"
100 | case Diamonds:
101 | return "♦"
102 | case Clubs:
103 | return "♣"
104 | default:
105 | return "?"
106 | }
107 | }
108 |
109 | func (s Suit) Color() Color {
110 | switch s {
111 | case Spades, Clubs:
112 | return Black
113 | case Hearts, Diamonds:
114 | return Red
115 | default:
116 | return Black
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/outlay/fan/widget/boring/cards.go:
--------------------------------------------------------------------------------
1 | package boring
2 |
3 | import (
4 | "image/color"
5 | "math"
6 |
7 | "gioui.org/example/outlay/fan/playing"
8 | xwidget "gioui.org/example/outlay/fan/widget"
9 | "gioui.org/f32"
10 | "gioui.org/layout"
11 | "gioui.org/op"
12 | "gioui.org/unit"
13 | "gioui.org/widget/material"
14 | )
15 |
16 | type (
17 | C = layout.Context
18 | D = layout.Dimensions
19 | )
20 |
21 | type CardPalette struct {
22 | RedSuit, BlackSuit color.NRGBA
23 | Border, Background color.NRGBA
24 | }
25 |
26 | func (p CardPalette) ColorFor(s playing.Suit) color.NRGBA {
27 | if s.Color() == playing.Red {
28 | return p.RedSuit
29 | }
30 | return p.BlackSuit
31 | }
32 |
33 | var DefaultPalette = &CardPalette{
34 | RedSuit: color.NRGBA{R: 0xa0, B: 0x20, A: 0xff},
35 | BlackSuit: color.NRGBA{A: 0xff},
36 | Border: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff},
37 | Background: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff},
38 | }
39 |
40 | type CardStyle struct {
41 | *material.Theme
42 | playing.Card
43 | Height unit.Dp
44 | *CardPalette
45 | }
46 |
47 | const (
48 | cardHeightToWidth = 14.0 / 9.0
49 | cardRadiusToWidth = 1.0 / 16.0
50 | borderWidth = 0.005
51 | )
52 |
53 | func (c *CardStyle) Palette() *CardPalette {
54 | if c.CardPalette == nil {
55 | return DefaultPalette
56 | }
57 | return c.CardPalette
58 | }
59 |
60 | func (c *CardStyle) Layout(gtx C) D {
61 | gtx.Constraints.Max.Y = gtx.Dp(c.Height)
62 | gtx.Constraints.Max.X = int(float32(gtx.Constraints.Max.Y) / cardHeightToWidth)
63 | outerRadius := float32(gtx.Constraints.Max.X) * cardRadiusToWidth
64 | innerRadius := (1 - borderWidth) * outerRadius
65 |
66 | borderWidth := c.Height * borderWidth
67 | return layout.Stack{}.Layout(gtx,
68 | layout.Expanded(func(gtx C) D {
69 | return Rect{
70 | Color: c.Palette().Border,
71 | Size: layout.FPt(gtx.Constraints.Max),
72 | Radii: outerRadius,
73 | }.Layout(gtx)
74 | }),
75 | layout.Stacked(func(gtx C) D {
76 | return layout.UniformInset(borderWidth).Layout(gtx, func(gtx C) D {
77 | return layout.Stack{}.Layout(gtx,
78 | layout.Expanded(func(gtx C) D {
79 | return Rect{
80 | Color: c.Palette().Background,
81 | Size: layout.FPt(gtx.Constraints.Max),
82 | Radii: innerRadius,
83 | }.Layout(gtx)
84 | }),
85 | layout.Stacked(func(gtx C) D {
86 | return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D {
87 | gtx.Constraints.Min = gtx.Constraints.Max
88 | origin := f32.Point{
89 | X: float32(gtx.Constraints.Max.X / 2),
90 | Y: float32(gtx.Constraints.Max.Y / 2),
91 | }
92 | layout.Center.Layout(gtx, func(gtx C) D {
93 | face := material.H1(c.Theme, c.Rank.String())
94 | face.Color = c.Palette().ColorFor(c.Suit)
95 | return face.Layout(gtx)
96 | })
97 | c.layoutCorner(gtx)
98 | defer op.Affine(f32.Affine2D{}.Rotate(origin, math.Pi)).Push(gtx.Ops).Pop()
99 | c.layoutCorner(gtx)
100 |
101 | return D{Size: gtx.Constraints.Max}
102 | })
103 | }),
104 | )
105 | })
106 | }),
107 | )
108 | }
109 |
110 | func (c *CardStyle) layoutCorner(gtx layout.Context) layout.Dimensions {
111 | col := c.Palette().ColorFor(c.Suit)
112 | return layout.NW.Layout(gtx, func(gtx C) D {
113 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx C) D {
114 | return layout.Flex{
115 | Axis: layout.Vertical,
116 | Alignment: layout.Middle,
117 | }.Layout(gtx,
118 | layout.Rigid(func(gtx C) D {
119 | label := material.H6(c.Theme, c.Rank.String())
120 | label.Color = col
121 | return label.Layout(gtx)
122 | }),
123 | layout.Rigid(func(gtx C) D {
124 | label := material.H6(c.Theme, c.Suit.String())
125 | label.Color = col
126 | return label.Layout(gtx)
127 | }),
128 | )
129 | })
130 | })
131 | }
132 |
133 | type HoverCard struct {
134 | CardStyle
135 | *xwidget.HoverState
136 | }
137 |
138 | func (h HoverCard) Layout(gtx C) D {
139 | dims := h.CardStyle.Layout(gtx)
140 | gtx.Constraints.Max = dims.Size
141 | gtx.Constraints.Min = dims.Size
142 | h.HoverState.Layout(gtx)
143 | return dims
144 | }
145 |
--------------------------------------------------------------------------------
/outlay/fan/widget/boring/rect.go:
--------------------------------------------------------------------------------
1 | package boring
2 |
3 | import (
4 | "image"
5 | "image/color"
6 |
7 | "gioui.org/f32"
8 | "gioui.org/layout"
9 | "gioui.org/op/clip"
10 | "gioui.org/op/paint"
11 | )
12 |
13 | // Rect creates a rectangle of the provided background color with
14 | // Dimensions specified by size and a corner radius (on all corners)
15 | // specified by radii.
16 | type Rect struct {
17 | Color color.NRGBA
18 | Size f32.Point
19 | Radii float32
20 | }
21 |
22 | // Layout renders the Rect into the provided context
23 | func (r Rect) Layout(gtx C) D {
24 | return DrawRect(gtx, r.Color, r.Size, r.Radii)
25 | }
26 |
27 | // DrawRect creates a rectangle of the provided background color with
28 | // Dimensions specified by size and a corner radius (on all corners)
29 | // specified by radii.
30 | func DrawRect(gtx C, background color.NRGBA, size f32.Point, radii float32) D {
31 | bounds := image.Rectangle{
32 | Max: image.Point{
33 | X: int(size.X),
34 | Y: int(size.Y),
35 | },
36 | }
37 | paint.FillShape(gtx.Ops, background, clip.UniformRRect(bounds, int(radii)).Op(gtx.Ops))
38 | return layout.Dimensions{Size: image.Pt(int(size.X), int(size.Y))}
39 | }
40 |
--------------------------------------------------------------------------------
/outlay/fan/widget/state.go:
--------------------------------------------------------------------------------
1 | package widget
2 |
3 | import (
4 | "image"
5 |
6 | "gioui.org/io/event"
7 | "gioui.org/io/pointer"
8 | "gioui.org/layout"
9 | "gioui.org/op"
10 | "gioui.org/op/clip"
11 | )
12 |
13 | type (
14 | C = layout.Context
15 | D = layout.Dimensions
16 | )
17 |
18 | type HoverState struct {
19 | hovering bool
20 | }
21 |
22 | func (c *HoverState) Hovering(gtx C) bool {
23 | start := c.hovering
24 | for {
25 | ev, ok := gtx.Event(pointer.Filter{
26 | Target: c,
27 | Kinds: pointer.Enter | pointer.Leave,
28 | })
29 | if !ok {
30 | break
31 | }
32 | switch ev := ev.(type) {
33 | case pointer.Event:
34 | switch ev.Kind {
35 | case pointer.Enter:
36 | c.hovering = true
37 | case pointer.Leave:
38 | c.hovering = false
39 | case pointer.Cancel:
40 | c.hovering = false
41 | }
42 | }
43 | }
44 | if c.hovering != start {
45 | gtx.Execute(op.InvalidateCmd{})
46 | }
47 | return c.hovering
48 | }
49 |
50 | func (c *HoverState) Layout(gtx C) D {
51 | defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
52 | event.Op(gtx.Ops, c)
53 | return D{Size: gtx.Constraints.Max}
54 | }
55 |
--------------------------------------------------------------------------------
/outlay/grid/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 | "log"
8 | "os"
9 | "strconv"
10 |
11 | "gioui.org/app"
12 | "gioui.org/font/gofont"
13 | "gioui.org/layout"
14 | "gioui.org/op"
15 | "gioui.org/op/clip"
16 | "gioui.org/op/paint"
17 | "gioui.org/text"
18 | "gioui.org/unit"
19 | "gioui.org/widget"
20 | "gioui.org/widget/material"
21 |
22 | "gioui.org/x/outlay"
23 | )
24 |
25 | func main() {
26 | go func() {
27 | w := new(app.Window)
28 | w.Option(
29 | app.Size(unit.Dp(800), unit.Dp(400)),
30 | app.Title("Gio layouts"),
31 | )
32 | if err := loop(w); err != nil {
33 | log.Fatal(err)
34 | }
35 | os.Exit(0)
36 | }()
37 | app.Main()
38 | }
39 |
40 | func loop(w *app.Window) error {
41 | ui := newUI()
42 |
43 | var ops op.Ops
44 | for {
45 | switch e := w.Event().(type) {
46 | case app.DestroyEvent:
47 | return e.Err
48 |
49 | case app.FrameEvent:
50 | gtx := app.NewContext(&ops, e)
51 | ui.Layout(gtx)
52 | e.Frame(gtx.Ops)
53 | }
54 | }
55 | return nil
56 | }
57 |
58 | type UI struct {
59 | theme *material.Theme
60 | active int
61 | tabs []uiTab
62 | list layout.List
63 | }
64 |
65 | type uiTab struct {
66 | name string
67 | click widget.Clickable
68 | text string
69 | w func(tab *uiTab, gtx layout.Context) layout.Dimensions
70 | num int
71 | ed widget.Editor
72 | }
73 |
74 | var (
75 | vWrap = outlay.FlowWrap{
76 | Axis: layout.Vertical,
77 | Alignment: layout.End,
78 | }
79 | hWrap = outlay.FlowWrap{
80 | Axis: layout.Horizontal,
81 | Alignment: layout.End,
82 | }
83 | vGrid = outlay.Flow{
84 | Num: 11,
85 | Axis: layout.Vertical,
86 | }
87 | hGrid = outlay.Flow{
88 | Num: 11,
89 | Axis: layout.Horizontal,
90 | }
91 | )
92 |
93 | func newUI() *UI {
94 | ui := &UI{
95 | theme: material.NewTheme(),
96 | list: layout.List{
97 | Axis: layout.Horizontal,
98 | Alignment: layout.Baseline,
99 | },
100 | }
101 | ui.theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
102 | ui.tabs = append(ui.tabs,
103 | uiTab{
104 | name: "V wrap",
105 | text: "Lay out items vertically before wrapping to the next column.",
106 | w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
107 | return vWrap.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
108 | s := fmt.Sprintf("item %d", i)
109 | return material.Body1(ui.theme, s).Layout(gtx)
110 | })
111 | },
112 | },
113 | uiTab{
114 | name: "H wrap",
115 | text: "Lay out items horizontally before wrapping to the next row.",
116 | w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
117 | return hWrap.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
118 | s := fmt.Sprintf("item %d", i)
119 | return material.Body1(ui.theme, s).Layout(gtx)
120 | })
121 | },
122 | },
123 | uiTab{
124 | name: "V grid",
125 | text: fmt.Sprintf("Lay out %d items vertically before going to the next column.", vGrid.Num),
126 | w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
127 | return vGrid.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
128 | s := fmt.Sprintf("item %d", i)
129 | return material.Body1(ui.theme, s).Layout(gtx)
130 | })
131 | },
132 | },
133 | uiTab{
134 | name: "H grid",
135 | text: fmt.Sprintf("Lay out %d items horizontally before going to the next row.", hGrid.Num),
136 | w: func(tab *uiTab, gtx layout.Context) layout.Dimensions {
137 | return hGrid.Layout(gtx, tab.num, func(gtx layout.Context, i int) layout.Dimensions {
138 | s := fmt.Sprintf("item %d", i)
139 | return material.Body1(ui.theme, s).Layout(gtx)
140 | })
141 | },
142 | },
143 | )
144 | for i := range ui.tabs {
145 | tab := &ui.tabs[i]
146 | tab.ed = widget.Editor{
147 | SingleLine: true,
148 | Submit: true,
149 | }
150 | tab.num = 99
151 | tab.ed.SetText(strconv.Itoa(tab.num))
152 | }
153 | return ui
154 | }
155 |
156 | func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
157 | for i := range ui.tabs {
158 | for ui.tabs[i].click.Clicked(gtx) {
159 | ui.active = i
160 | }
161 | }
162 | activeTab := &ui.tabs[ui.active]
163 | return layout.Flex{
164 | Axis: layout.Vertical,
165 | }.Layout(gtx,
166 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
167 | return ui.list.Layout(gtx, len(ui.tabs), func(gtx layout.Context, idx int) layout.Dimensions {
168 | tab := &ui.tabs[idx]
169 | title := func(gtx layout.Context) layout.Dimensions {
170 | return layout.UniformInset(unit.Dp(6)).Layout(gtx, material.H6(ui.theme, tab.name).Layout)
171 | }
172 | if idx != ui.active {
173 | return material.Clickable(gtx, &tab.click, title)
174 | }
175 | return layout.Stack{}.Layout(gtx,
176 | layout.Expanded(func(gtx layout.Context) layout.Dimensions {
177 | defer clip.UniformRRect(image.Rectangle{
178 | Max: gtx.Constraints.Min,
179 | }, 0).Push(gtx.Ops).Pop()
180 | paint.Fill(gtx.Ops, color.NRGBA{A: 64})
181 | return layout.Dimensions{}
182 | }),
183 | layout.Stacked(title),
184 | )
185 | })
186 | }),
187 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
188 | pt := image.Point{X: gtx.Constraints.Max.X, Y: 4}
189 | defer clip.UniformRRect(image.Rectangle{
190 | Max: pt,
191 | }, 0).Push(gtx.Ops).Pop()
192 | paint.Fill(gtx.Ops, ui.theme.Palette.ContrastBg)
193 | return layout.Dimensions{Size: pt}
194 | }),
195 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
196 | return layout.Stack{}.Layout(gtx,
197 | layout.Expanded(func(gtx layout.Context) layout.Dimensions {
198 | defer clip.UniformRRect(image.Rectangle{
199 | Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Min.Y),
200 | }, 0).Push(gtx.Ops).Pop()
201 | paint.Fill(gtx.Ops, color.NRGBA{A: 24})
202 | return layout.Dimensions{}
203 | }),
204 | layout.Stacked(func(gtx layout.Context) layout.Dimensions {
205 | return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
206 | if x, _ := strconv.Atoi(activeTab.ed.Text()); x != activeTab.num {
207 | activeTab.num = x
208 | }
209 | return layout.Flex{
210 | Alignment: layout.Baseline,
211 | }.Layout(gtx,
212 | layout.Rigid(material.Body1(ui.theme, activeTab.text).Layout),
213 | layout.Rigid(material.Body1(ui.theme, " Num = ").Layout),
214 | layout.Rigid(material.Editor(ui.theme, &activeTab.ed, "").Layout),
215 | )
216 | })
217 | }),
218 | )
219 | }),
220 | layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
221 | return activeTab.w(activeTab, gtx)
222 | }),
223 | )
224 | }
225 |
--------------------------------------------------------------------------------
/tabs/slider.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "image"
7 | "time"
8 |
9 | "gioui.org/layout"
10 | "gioui.org/op"
11 | )
12 |
13 | const defaultDuration = 300 * time.Millisecond
14 |
15 | // Slider implements sliding between old/new widget values.
16 | type Slider struct {
17 | Duration time.Duration
18 |
19 | push int
20 |
21 | next *op.Ops
22 |
23 | nextCall op.CallOp
24 | lastCall op.CallOp
25 |
26 | t0 time.Time
27 | offset float32
28 | }
29 |
30 | // PushLeft pushes the existing widget to the left.
31 | func (s *Slider) PushLeft() { s.push = 1 }
32 |
33 | // PushRight pushes the existing widget to the right.
34 | func (s *Slider) PushRight() { s.push = -1 }
35 |
36 | // Layout lays out widget that can be pushed.
37 | func (s *Slider) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
38 | if s.push != 0 {
39 | s.next = nil
40 | s.lastCall = s.nextCall
41 | s.offset = float32(s.push)
42 | s.t0 = gtx.Now
43 | s.push = 0
44 | }
45 |
46 | var delta time.Duration
47 | if !s.t0.IsZero() {
48 | now := gtx.Now
49 | delta = now.Sub(s.t0)
50 | s.t0 = now
51 | }
52 |
53 | if s.offset != 0 {
54 | duration := s.Duration
55 | if duration == 0 {
56 | duration = defaultDuration
57 | }
58 | movement := float32(delta.Seconds()) / float32(duration.Seconds())
59 | if s.offset < 0 {
60 | s.offset += movement
61 | if s.offset >= 0 {
62 | s.offset = 0
63 | }
64 | } else {
65 | s.offset -= movement
66 | if s.offset <= 0 {
67 | s.offset = 0
68 | }
69 | }
70 |
71 | gtx.Execute(op.InvalidateCmd{})
72 | }
73 |
74 | var dims layout.Dimensions
75 | {
76 | if s.next == nil {
77 | s.next = new(op.Ops)
78 | }
79 | gtx := gtx
80 | gtx.Ops = s.next
81 | gtx.Ops.Reset()
82 | m := op.Record(gtx.Ops)
83 | dims = w(gtx)
84 | s.nextCall = m.Stop()
85 | }
86 |
87 | if s.offset == 0 {
88 | s.nextCall.Add(gtx.Ops)
89 | return dims
90 | }
91 |
92 | offset := smooth(s.offset)
93 |
94 | if s.offset > 0 {
95 | defer op.Offset(image.Point{
96 | X: int(float32(dims.Size.X) * (offset - 1)),
97 | }).Push(gtx.Ops).Pop()
98 | s.lastCall.Add(gtx.Ops)
99 |
100 | defer op.Offset(image.Point{
101 | X: dims.Size.X,
102 | }).Push(gtx.Ops).Pop()
103 | s.nextCall.Add(gtx.Ops)
104 | } else {
105 | defer op.Offset(image.Point{
106 | X: int(float32(dims.Size.X) * (offset + 1)),
107 | }).Push(gtx.Ops).Pop()
108 | s.lastCall.Add(gtx.Ops)
109 |
110 | defer op.Offset(image.Point{
111 | X: -dims.Size.X,
112 | }).Push(gtx.Ops).Pop()
113 | s.nextCall.Add(gtx.Ops)
114 | }
115 | return dims
116 | }
117 |
118 | // smooth handles -1 to 1 with ease-in-out cubic easing func.
119 | func smooth(t float32) float32 {
120 | if t < 0 {
121 | return -easeInOutCubic(-t)
122 | }
123 | return easeInOutCubic(t)
124 | }
125 |
126 | // easeInOutCubic maps a linear value to a ease-in-out-cubic easing function.
127 | func easeInOutCubic(t float32) float32 {
128 | if t < 0.5 {
129 | return 4 * t * t * t
130 | }
131 | return (t-1)*(2*t-2)*(2*t-2) + 1
132 | }
133 |
--------------------------------------------------------------------------------
/tabs/tabs.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "image"
8 | "image/color"
9 | "log"
10 | "math"
11 | "os"
12 |
13 | "gioui.org/app"
14 | "gioui.org/f32"
15 | "gioui.org/layout"
16 | "gioui.org/op"
17 | "gioui.org/op/clip"
18 | "gioui.org/op/paint"
19 | "gioui.org/text"
20 | "gioui.org/unit"
21 | "gioui.org/widget"
22 | "gioui.org/widget/material"
23 |
24 | "gioui.org/font/gofont"
25 | )
26 |
27 | func main() {
28 | go func() {
29 | defer os.Exit(0)
30 | w := new(app.Window)
31 | if err := loop(w); err != nil {
32 | log.Fatal(err)
33 | }
34 | }()
35 | app.Main()
36 | }
37 |
38 | func loop(w *app.Window) error {
39 | th := material.NewTheme()
40 | th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
41 | var ops op.Ops
42 | for {
43 | switch e := w.Event().(type) {
44 | case app.DestroyEvent:
45 | return e.Err
46 | case app.FrameEvent:
47 | gtx := app.NewContext(&ops, e)
48 | drawTabs(gtx, th)
49 | e.Frame(gtx.Ops)
50 | }
51 | }
52 | }
53 |
54 | var (
55 | tabs Tabs
56 | slider Slider
57 | )
58 |
59 | type Tabs struct {
60 | list layout.List
61 | tabs []Tab
62 | selected int
63 | }
64 |
65 | type Tab struct {
66 | btn widget.Clickable
67 | Title string
68 | }
69 |
70 | func init() {
71 | for i := 1; i <= 100; i++ {
72 | tabs.tabs = append(tabs.tabs,
73 | Tab{Title: fmt.Sprintf("Tab %d", i)},
74 | )
75 | }
76 | }
77 |
78 | type (
79 | C = layout.Context
80 | D = layout.Dimensions
81 | )
82 |
83 | func drawTabs(gtx layout.Context, th *material.Theme) layout.Dimensions {
84 | return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
85 | layout.Rigid(func(gtx C) D {
86 | return tabs.list.Layout(gtx, len(tabs.tabs), func(gtx C, tabIdx int) D {
87 | t := &tabs.tabs[tabIdx]
88 | if t.btn.Clicked(gtx) {
89 | if tabs.selected < tabIdx {
90 | slider.PushLeft()
91 | } else if tabs.selected > tabIdx {
92 | slider.PushRight()
93 | }
94 | tabs.selected = tabIdx
95 | }
96 | var tabWidth int
97 | return layout.Stack{Alignment: layout.S}.Layout(gtx,
98 | layout.Stacked(func(gtx C) D {
99 | dims := material.Clickable(gtx, &t.btn, func(gtx C) D {
100 | return layout.UniformInset(unit.Dp(12)).Layout(gtx,
101 | material.H6(th, t.Title).Layout,
102 | )
103 | })
104 | tabWidth = dims.Size.X
105 | return dims
106 | }),
107 | layout.Stacked(func(gtx C) D {
108 | if tabs.selected != tabIdx {
109 | return layout.Dimensions{}
110 | }
111 | tabHeight := gtx.Dp(unit.Dp(4))
112 | tabRect := image.Rect(0, 0, tabWidth, tabHeight)
113 | paint.FillShape(gtx.Ops, th.Palette.ContrastBg, clip.Rect(tabRect).Op())
114 | return layout.Dimensions{
115 | Size: image.Point{X: tabWidth, Y: tabHeight},
116 | }
117 | }),
118 | )
119 | })
120 | }),
121 | layout.Flexed(1, func(gtx C) D {
122 | return slider.Layout(gtx, func(gtx C) D {
123 | fill(gtx, dynamicColor(tabs.selected), dynamicColor(tabs.selected+1))
124 | return layout.Center.Layout(gtx,
125 | material.H1(th, fmt.Sprintf("Tab content #%d", tabs.selected+1)).Layout,
126 | )
127 | })
128 | }),
129 | )
130 | }
131 |
132 | func fill(gtx layout.Context, col1, col2 color.NRGBA) {
133 | dr := image.Rectangle{Max: gtx.Constraints.Min}
134 | paint.FillShape(gtx.Ops,
135 | color.NRGBA{R: 0, G: 0, B: 0, A: 0xFF},
136 | clip.Rect(dr).Op(),
137 | )
138 |
139 | col2.R = byte(float32(col2.R))
140 | col2.G = byte(float32(col2.G))
141 | col2.B = byte(float32(col2.B))
142 | paint.LinearGradientOp{
143 | Stop1: f32.Pt(float32(dr.Min.X), 0),
144 | Stop2: f32.Pt(float32(dr.Max.X), 0),
145 | Color1: col1,
146 | Color2: col2,
147 | }.Add(gtx.Ops)
148 | defer clip.Rect(dr).Push(gtx.Ops).Pop()
149 | paint.PaintOp{}.Add(gtx.Ops)
150 | }
151 |
152 | func dynamicColor(i int) color.NRGBA {
153 | sn, cs := math.Sincos(float64(i) * math.Phi)
154 | return color.NRGBA{
155 | R: 0xA0 + byte(0x30*sn),
156 | G: 0xA0 + byte(0x30*cs),
157 | B: 0xD0,
158 | A: 0xFF,
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/textfeatures/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Unlicense OR MIT
2 |
3 | package main
4 |
5 | // A simple Gio program. See https://gioui.org for more information.
6 |
7 | import (
8 | "log"
9 | "math"
10 | "os"
11 | "strconv"
12 |
13 | "gioui.org/app"
14 | "gioui.org/layout"
15 | "gioui.org/op"
16 | "gioui.org/text"
17 | "gioui.org/widget"
18 | "gioui.org/widget/material"
19 |
20 | colEmoji "eliasnaur.com/font/noto/emoji/color"
21 | "gioui.org/font/gofont"
22 | "gioui.org/font/opentype"
23 | )
24 |
25 | func main() {
26 | go func() {
27 | w := new(app.Window)
28 | if err := loop(w); err != nil {
29 | log.Fatal(err)
30 | }
31 | os.Exit(0)
32 | }()
33 | app.Main()
34 | }
35 |
36 | func loop(w *app.Window) error {
37 | // Load default font collection.
38 | collection := gofont.Collection()
39 | // Load a color emoji font.
40 | faces, err := opentype.ParseCollection(colEmoji.TTF)
41 | if err != nil {
42 | panic(err)
43 | }
44 | th := material.NewTheme()
45 | th.Shaper = text.NewShaper(text.WithCollection(append(collection, faces...)))
46 | var ops op.Ops
47 | var sel widget.Selectable
48 | message := "🥳🧁🍰🎁🎂🎈🎺🎉🎊\n📧〽️🧿🌶️🔋\n😂❤️😍🤣😊\n🥺🙏💕😭😘\n👍😅👏"
49 | var customTruncator widget.Bool
50 | var maxLines widget.Float
51 | maxLines.Value = 0
52 |
53 | const (
54 | minLinesRange = 1
55 | maxLinesRange = 5
56 | )
57 | for {
58 | switch e := w.Event().(type) {
59 | case app.DestroyEvent:
60 | return e.Err
61 | case app.FrameEvent:
62 | gtx := app.NewContext(&ops, e)
63 | inset := layout.UniformInset(5)
64 | layout.Flex{Axis: layout.Vertical}.Layout(gtx,
65 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
66 | l := material.H4(th, message)
67 | if customTruncator.Value {
68 | l.Truncator = "cont..."
69 | } else {
70 | l.Truncator = ""
71 | }
72 | l.MaxLines = minLinesRange + int(math.Round(float64(maxLines.Value)*(maxLinesRange-minLinesRange)))
73 | l.State = &sel
74 | return inset.Layout(gtx, l.Layout)
75 | }),
76 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
77 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
78 | return layout.Flex{}.Layout(gtx,
79 | layout.Rigid(material.Switch(th, &customTruncator, "Use Custom Truncator").Layout),
80 | layout.Rigid(layout.Spacer{Width: 5}.Layout),
81 | layout.Rigid(material.Body1(th, "Use Custom Truncator").Layout),
82 | )
83 | })
84 | }),
85 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
86 | return inset.Layout(gtx, material.Body1(th, "Max Lines:").Layout)
87 | }),
88 | layout.Rigid(func(gtx layout.Context) layout.Dimensions {
89 | return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
90 | return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
91 | layout.Rigid(material.Body2(th, strconv.Itoa(minLinesRange)).Layout),
92 | layout.Rigid(layout.Spacer{Width: 5}.Layout),
93 | layout.Flexed(1, material.Slider(th, &maxLines).Layout),
94 | layout.Rigid(layout.Spacer{Width: 5}.Layout),
95 | layout.Rigid(material.Body2(th, strconv.Itoa(maxLinesRange)).Layout),
96 | )
97 | })
98 | }),
99 | )
100 | e.Frame(gtx.Ops)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tools.go:
--------------------------------------------------------------------------------
1 | //go:build tools
2 |
3 | package tools
4 |
5 | import _ "gioui.org/cmd/gogio"
6 |
--------------------------------------------------------------------------------