├── .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 | [![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio-example.svg)](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 | --------------------------------------------------------------------------------