├── .gitignore ├── Icon.png ├── img ├── bugs.png ├── xkcd.png ├── fractal.png ├── clock-dark.png ├── icon │ ├── bug.png │ ├── clock.png │ ├── xkcd.png │ ├── fractal.png │ ├── icon.go │ └── gen.sh └── clock-light.png ├── AUTHORS ├── bugs ├── flag.svg ├── code.svg ├── gen.sh ├── bug.svg ├── bundled.go ├── board_test.go ├── button.go ├── board.go └── main.go ├── data.go ├── tictactoe ├── grid.go └── board.go ├── LICENSE ├── .github └── workflows │ ├── static_analysis.yml │ └── platform_tests.yml ├── go.mod ├── main.go ├── fractal └── main.go ├── README.md ├── xkcd └── main.go ├── clock └── clock.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | examples 5 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/Icon.png -------------------------------------------------------------------------------- /img/bugs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/bugs.png -------------------------------------------------------------------------------- /img/xkcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/xkcd.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andy Williams 2 | Steve OConnor 3 | -------------------------------------------------------------------------------- /img/fractal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/fractal.png -------------------------------------------------------------------------------- /img/clock-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/clock-dark.png -------------------------------------------------------------------------------- /img/icon/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/icon/bug.png -------------------------------------------------------------------------------- /img/icon/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/icon/clock.png -------------------------------------------------------------------------------- /img/icon/xkcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/icon/xkcd.png -------------------------------------------------------------------------------- /img/clock-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/clock-light.png -------------------------------------------------------------------------------- /img/icon/fractal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyne-io/examples/HEAD/img/icon/fractal.png -------------------------------------------------------------------------------- /bugs/flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bugs/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bugs/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=`dirname "$0"` 4 | FILE=bundled.go 5 | BIN=`go env GOPATH`/bin 6 | 7 | cd $DIR 8 | rm $FILE 9 | 10 | $BIN/fyne bundle -package bugs -name codeIcon code.svg > $FILE 11 | $BIN/fyne bundle -package bugs -name bugIcon -append bug.svg >> $FILE 12 | $BIN/fyne bundle -package bugs -name flagIcon -append flag.svg >> $FILE 13 | 14 | -------------------------------------------------------------------------------- /data.go: -------------------------------------------------------------------------------- 1 | // auto-generated 2 | // Code generated by '$ fyne bundle'. DO NOT EDIT. 3 | 4 | package main 5 | 6 | import ( 7 | _ "embed" 8 | 9 | "fyne.io/fyne/v2" 10 | ) 11 | 12 | //go:embed Icon.png 13 | var resourceIconPngData []byte 14 | var resourceIconPng = &fyne.StaticResource{ 15 | StaticName: "Icon.png", 16 | StaticContent: resourceIconPngData, 17 | } 18 | -------------------------------------------------------------------------------- /img/icon/icon.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | // BugBitmap an icon for the bug example 4 | var BugBitmap = bugBitmap 5 | 6 | // ClockBitmap an icon for the clock example 7 | var ClockBitmap = clockBitmap 8 | 9 | // FractalBitmap an icon for the fractal example 10 | var FractalBitmap = fractalBitmap 11 | 12 | // XKCDBitmap an icon for the XKCD example 13 | var XKCDBitmap = xkcdBitmap 14 | -------------------------------------------------------------------------------- /img/icon/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=`dirname "$0"` 4 | FILE=bundled.go 5 | BIN=`go env GOPATH`/bin 6 | 7 | cd $DIR 8 | rm $FILE 9 | 10 | $BIN/fyne bundle -package icon -name bugBitmap bug.png > $FILE 11 | $BIN/fyne bundle -package icon -append -name clockBitmap clock.png >> $FILE 12 | $BIN/fyne bundle -package icon -append -name fractalBitmap fractal.png >> $FILE 13 | $BIN/fyne bundle -package icon -append -name xkcdBitmap xkcd.png >> $FILE 14 | 15 | -------------------------------------------------------------------------------- /bugs/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bugs/bundled.go: -------------------------------------------------------------------------------- 1 | // auto-generated 2 | // Code generated by '$ fyne bundle'. DO NOT EDIT. 3 | 4 | package bugs 5 | 6 | import ( 7 | _ "embed" 8 | 9 | "fyne.io/fyne/v2" 10 | ) 11 | 12 | //go:embed code.svg 13 | var codeIconData []byte 14 | var codeIcon = &fyne.StaticResource{ 15 | StaticName: "code.svg", 16 | StaticContent: codeIconData, 17 | } 18 | 19 | //go:embed bug.svg 20 | var bugIconData []byte 21 | var bugIcon = &fyne.StaticResource{ 22 | StaticName: "bug.svg", 23 | StaticContent: bugIconData, 24 | } 25 | 26 | //go:embed flag.svg 27 | var flagIconData []byte 28 | var flagIcon = &fyne.StaticResource{ 29 | StaticName: "flag.svg", 30 | StaticContent: flagIconData, 31 | } 32 | -------------------------------------------------------------------------------- /tictactoe/grid.go: -------------------------------------------------------------------------------- 1 | package tictactoe 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/container" 6 | "fyne.io/fyne/v2/theme" 7 | "fyne.io/fyne/v2/widget" 8 | ) 9 | 10 | // Show loads a tic-tac-toe example window for the specified app context 11 | func Show(win fyne.Window) fyne.CanvasObject { 12 | board := &board{} 13 | 14 | grid := container.NewGridWithColumns(3) 15 | for r := 0; r < 3; r++ { 16 | for c := 0; c < 3; c++ { 17 | grid.Add(newBoardIcon(r, c, board)) 18 | } 19 | } 20 | 21 | reset := widget.NewButtonWithIcon("Reset Board", theme.ViewRefreshIcon(), func() { 22 | for i := range grid.Objects { 23 | grid.Objects[i].(*boardIcon).Reset() 24 | } 25 | 26 | board.Reset() 27 | }) 28 | 29 | return container.NewBorder(reset, nil, nil, nil, grid) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018-2020 Fyne.io developers (see AUTHORS) 2 | All rights reserved. 3 | 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Fyne.io nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/static_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | on: [push, pull_request] 3 | permissions: 4 | contents: read 5 | env: 6 | CC: "clang" 7 | CGO_LDFLAGS: "-fuse-ld=lld" 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v5 15 | with: 16 | persist-credentials: false 17 | - uses: actions/setup-go@v6 18 | with: 19 | go-version: "stable" 20 | 21 | - name: Get dependencies 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install --no-install-recommends clang lld libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev xvfb language-pack-en 25 | 26 | - name: Set environment variable LANG 27 | run: export LANG=en_EN.UTF-8 28 | 29 | - name: Install analysis tools 30 | run: | 31 | go install mvdan.cc/gofumpt@latest 32 | go install golang.org/x/tools/cmd/goimports@latest 33 | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 34 | go install honnef.co/go/tools/cmd/staticcheck@latest 35 | go install lucor.dev/lian@latest 36 | 37 | - name: Vet 38 | run: go vet ./... 39 | 40 | - name: Formatting 41 | run: | 42 | gofumpt -d -e . 43 | test -z "$(goimports -e -d . | tee /dev/stderr)" 44 | 45 | - name: Gocyclo 46 | run: gocyclo -over 30 . 47 | 48 | - name: Staticcheck 49 | run: staticcheck ./... 50 | 51 | - name: Check license of dependencies 52 | run: lian -d --allowed="Apache-2.0, BSD-2-Clause, BSD-3-Clause, MIT, ISC" 53 | -------------------------------------------------------------------------------- /.github/workflows/platform_tests.yml: -------------------------------------------------------------------------------- 1 | name: Platform Tests 2 | on: [push, pull_request] 3 | permissions: 4 | contents: read 5 | 6 | jobs: 7 | platform_tests: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | go-version: ["", "stable"] 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | 15 | steps: 16 | - uses: actions/checkout@v5 17 | with: 18 | persist-credentials: false 19 | - uses: actions/setup-go@v6 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | go-version-file: "go.mod" 23 | 24 | - name: Get dependencies 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install --no-install-recommends bc clang lld libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev xvfb language-pack-en 28 | echo "CC=clang" >> "$GITHUB_ENV" 29 | echo "CGO_LDFLAGS=-fuse-ld=lld" >> "$GITHUB_ENV" 30 | if: ${{ runner.os == 'Linux' }} 31 | 32 | - name: Test 33 | run: go test -tags ci -covermode=atomic -coverprofile="coverage.out" ./... 34 | 35 | - name: Update coverage 36 | run: | 37 | set -e 38 | coverage=`go tool cover -func coverage.out | grep total | tr -s '\t' | cut -f 3 | grep -o '[^%]*'` 39 | if (( $(echo "$coverage < 13" | bc) )); then echo "Test coverage lowered"; exit 1; fi 40 | if: ${{ runner.os == 'Linux' && matrix.go-version == 'stable' }} 41 | 42 | - name: Update PR Coverage 43 | uses: shogo82148/actions-goveralls@v1 44 | with: 45 | path-to-profile: coverage.out 46 | if: ${{ runner.os == 'Linux' && matrix.go-version == 'stable' }} 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fyne-io/examples 2 | 3 | go 1.19 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.7.0 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect 12 | github.com/BurntSushi/toml v1.5.0 // indirect 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/fredbi/uri v1.1.1 // indirect 15 | github.com/fsnotify/fsnotify v1.9.0 // indirect 16 | github.com/fyne-io/gl-js v0.2.0 // indirect 17 | github.com/fyne-io/glfw-js v0.3.0 // indirect 18 | github.com/fyne-io/image v0.1.1 // indirect 19 | github.com/fyne-io/oksvg v0.2.0 // indirect 20 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect 21 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect 22 | github.com/go-text/render v0.2.0 // indirect 23 | github.com/go-text/typesetting v0.2.1 // indirect 24 | github.com/godbus/dbus/v5 v5.1.0 // indirect 25 | github.com/hack-pad/go-indexeddb v0.3.2 // indirect 26 | github.com/hack-pad/safejs v0.1.0 // indirect 27 | github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect 28 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect 29 | github.com/kr/text v0.2.0 // indirect 30 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 31 | github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/rymdport/portal v0.4.2 // indirect 34 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect 35 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect 36 | github.com/yuin/goldmark v1.7.8 // indirect 37 | golang.org/x/image v0.24.0 // indirect 38 | golang.org/x/net v0.35.0 // indirect 39 | golang.org/x/sys v0.30.0 // indirect 40 | golang.org/x/text v0.22.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate fyne bundle -o data.go Icon.png 2 | 3 | package main 4 | 5 | import ( 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/app" 8 | "fyne.io/fyne/v2/canvas" 9 | "fyne.io/fyne/v2/container" 10 | "fyne.io/fyne/v2/theme" 11 | "fyne.io/fyne/v2/widget" 12 | 13 | "github.com/fyne-io/examples/bugs" 14 | "github.com/fyne-io/examples/clock" 15 | "github.com/fyne-io/examples/fractal" 16 | "github.com/fyne-io/examples/img/icon" 17 | "github.com/fyne-io/examples/tictactoe" 18 | "github.com/fyne-io/examples/xkcd" 19 | ) 20 | 21 | type appInfo struct { 22 | name string 23 | icon fyne.Resource 24 | canv bool 25 | run func(fyne.Window) fyne.CanvasObject 26 | } 27 | 28 | var apps = []appInfo{ 29 | {"Bugs", icon.BugBitmap, false, bugs.Show}, 30 | {"XKCD", icon.XKCDBitmap, false, xkcd.Show}, 31 | {"Clock", icon.ClockBitmap, true, clock.Show}, 32 | {"Fractal", icon.FractalBitmap, true, fractal.Show}, 33 | {"Tic Tac Toe", nil, true, tictactoe.Show}, 34 | } 35 | 36 | func main() { 37 | a := app.New() 38 | a.SetIcon(resourceIconPng) 39 | 40 | content := container.NewStack() 41 | w := a.NewWindow("Examples") 42 | 43 | apps[4].icon = theme.RadioButtonIcon() // lazy load Fyne resource to avoid error 44 | appList := widget.NewList( 45 | func() int { 46 | return len(apps) 47 | }, 48 | func() fyne.CanvasObject { 49 | icon := &canvas.Image{} 50 | label := widget.NewLabel("Text Editor") 51 | labelHeight := label.MinSize().Height 52 | icon.SetMinSize(fyne.NewSize(labelHeight, labelHeight)) 53 | return container.NewBorder(nil, nil, icon, nil, 54 | label) 55 | }, 56 | func(id widget.ListItemID, obj fyne.CanvasObject) { 57 | img := obj.(*fyne.Container).Objects[1].(*canvas.Image) 58 | text := obj.(*fyne.Container).Objects[0].(*widget.Label) 59 | img.Resource = apps[id].icon 60 | img.Refresh() 61 | text.SetText(apps[id].name) 62 | }) 63 | appList.OnSelected = func(id widget.ListItemID) { 64 | content.Objects = []fyne.CanvasObject{apps[id].run(w)} 65 | } 66 | 67 | split := container.NewHSplit(appList, content) 68 | split.Offset = 0.1 69 | w.SetContent(split) 70 | w.Resize(fyne.NewSize(480, 360)) 71 | w.ShowAndRun() 72 | } 73 | -------------------------------------------------------------------------------- /bugs/board_test.go: -------------------------------------------------------------------------------- 1 | package bugs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func countMines(b *board) int { 10 | count := 0 11 | 12 | for y := 0; y < b.height; y++ { 13 | for x := 0; x < b.width; x++ { 14 | item := b.bugs[y][x] 15 | 16 | if item.bug { 17 | count++ 18 | } 19 | } 20 | } 21 | 22 | return count 23 | } 24 | 25 | func TestBoard(t *testing.T) { 26 | b := newBoard(10, 10) 27 | 28 | assert.Equal(t, 10, b.width) 29 | 30 | b.load(0) 31 | assert.Equal(t, 10, len(b.bugs)) 32 | assert.Equal(t, 10, len(b.bugs[0])) 33 | 34 | assert.Equal(t, 0, b.bugs[2][2].near) 35 | assert.Equal(t, false, b.bugs[2][2].shown) 36 | } 37 | 38 | func TestBoard_load(t *testing.T) { 39 | b := newBoard(10, 10) 40 | b.load(4) 41 | 42 | assert.Equal(t, 4, countMines(b)) 43 | } 44 | 45 | func TestBoard_setMine(t *testing.T) { 46 | b := newBoard(3, 3) 47 | b.load(0) 48 | b.setMine(1, 1) 49 | 50 | assert.True(t, b.bugs[1][1].bug) 51 | 52 | assert.Equal(t, 1, b.bugs[0][0].near) 53 | assert.Equal(t, 1, b.bugs[0][1].near) 54 | assert.Equal(t, 1, b.bugs[0][2].near) 55 | assert.Equal(t, 1, b.bugs[1][0].near) 56 | assert.Equal(t, 1, b.bugs[1][2].near) 57 | assert.Equal(t, 1, b.bugs[2][0].near) 58 | assert.Equal(t, 1, b.bugs[2][1].near) 59 | assert.Equal(t, 1, b.bugs[2][2].near) 60 | } 61 | 62 | func TestBoard_setMines(t *testing.T) { 63 | b := newBoard(3, 4) 64 | b.load(0) 65 | b.setMine(1, 1) 66 | b.setMine(2, 1) 67 | 68 | assert.Equal(t, 2, countMines(b)) 69 | assert.True(t, b.bugs[1][1].bug) 70 | assert.True(t, b.bugs[1][2].bug) 71 | 72 | assert.Equal(t, 1, b.bugs[0][0].near) 73 | assert.Equal(t, 2, b.bugs[0][1].near) 74 | assert.Equal(t, 2, b.bugs[0][2].near) 75 | assert.Equal(t, 1, b.bugs[0][3].near) 76 | assert.Equal(t, 1, b.bugs[1][0].near) 77 | assert.Equal(t, 1, b.bugs[1][3].near) 78 | assert.Equal(t, 1, b.bugs[2][0].near) 79 | assert.Equal(t, 2, b.bugs[2][1].near) 80 | assert.Equal(t, 2, b.bugs[2][2].near) 81 | assert.Equal(t, 1, b.bugs[2][3].near) 82 | } 83 | 84 | func TestBoard_remaining(t *testing.T) { 85 | b := newBoard(3, 3) 86 | b.load(1) 87 | 88 | assert.Equal(t, 1, b.remaining()) 89 | b.flag(0, 0) 90 | assert.Equal(t, 0, b.remaining()) 91 | } 92 | 93 | func TestBoard_reveal(t *testing.T) { 94 | b := newBoard(3, 3) 95 | b.load(0) 96 | b.setMine(1, 1) 97 | 98 | b.reveal(2, 1) 99 | assert.True(t, b.bugs[1][2].shown) 100 | } 101 | 102 | func TestBoard_revealCascade(t *testing.T) { 103 | b := newBoard(3, 4) 104 | b.load(0) 105 | b.setMine(1, 1) 106 | 107 | b.reveal(3, 1) 108 | assert.True(t, b.bugs[0][3].shown) 109 | assert.True(t, b.bugs[1][3].shown) 110 | assert.True(t, b.bugs[2][3].shown) 111 | } 112 | -------------------------------------------------------------------------------- /tictactoe/board.go: -------------------------------------------------------------------------------- 1 | package tictactoe 2 | 3 | import ( 4 | "fyne.io/fyne/v2" 5 | "fyne.io/fyne/v2/dialog" 6 | "fyne.io/fyne/v2/theme" 7 | "fyne.io/fyne/v2/widget" 8 | ) 9 | 10 | type board struct { 11 | pieces [3][3]uint8 12 | turn uint8 13 | finished bool 14 | } 15 | 16 | func (b *board) result() uint8 { 17 | // Check for a win in the diagonal direction from top left to bottom right. 18 | if b.pieces[0][0] != 0 && b.pieces[0][0] == b.pieces[1][1] && b.pieces[1][1] == b.pieces[2][2] { 19 | return b.pieces[0][0] 20 | } 21 | 22 | // Check for a win in the diagonal direction from bottom left to top right. 23 | if b.pieces[0][2] != 0 && b.pieces[0][2] == b.pieces[1][1] && b.pieces[1][1] == b.pieces[2][0] { 24 | return b.pieces[0][2] 25 | } 26 | 27 | for i := range b.pieces { 28 | // Check for a win in the horizontal direction. 29 | if b.pieces[i][0] != 0 && b.pieces[i][0] == b.pieces[i][1] && b.pieces[i][1] == b.pieces[i][2] { 30 | return b.pieces[i][0] 31 | } 32 | 33 | // Check for a win in the vertical direction. 34 | if b.pieces[0][i] != 0 && b.pieces[0][i] == b.pieces[1][i] && b.pieces[1][i] == b.pieces[2][i] { 35 | return b.pieces[0][i] 36 | } 37 | } 38 | 39 | return 0 40 | } 41 | 42 | func (b *board) newClick(row, column int) { 43 | b.pieces[row][column] = b.turn%2 + 1 44 | 45 | if b.turn > 3 { 46 | winner := b.result() 47 | if winner == 0 { 48 | if b.turn == 8 { 49 | dialog.ShowInformation("It is a tie!", "Nobody has won. Better luck next time.", fyne.CurrentApp().Driver().AllWindows()[0]) 50 | b.finished = true 51 | } 52 | return 53 | } 54 | 55 | number := string(winner + 48) // Number 1 is ascii #49 and 2 is ascii #50. 56 | dialog.ShowInformation("Player "+number+" has won!", "Congratulations to player "+number+" for winning.", fyne.CurrentApp().Driver().AllWindows()[0]) 57 | b.finished = true 58 | } 59 | } 60 | 61 | func (b *board) Reset() { 62 | for i := range b.pieces { 63 | b.pieces[i][0] = 0 64 | b.pieces[i][1] = 0 65 | b.pieces[i][2] = 0 66 | } 67 | 68 | b.finished = false 69 | b.turn = 0 70 | } 71 | 72 | type boardIcon struct { 73 | widget.Icon 74 | board *board 75 | row, column int 76 | } 77 | 78 | func (i *boardIcon) Tapped(ev *fyne.PointEvent) { 79 | if i.board.pieces[i.row][i.column] != 0 || i.board.finished { 80 | return 81 | } 82 | 83 | if i.board.turn%2 == 0 { 84 | i.SetResource(theme.RadioButtonIcon()) 85 | } else { 86 | i.SetResource(theme.CancelIcon()) 87 | } 88 | 89 | i.board.newClick(i.row, i.column) 90 | i.board.turn++ 91 | } 92 | 93 | func (i *boardIcon) Reset() { 94 | i.SetResource(theme.ViewFullScreenIcon()) 95 | } 96 | 97 | func newBoardIcon(row, column int, board *board) *boardIcon { 98 | i := &boardIcon{board: board, row: row, column: column} 99 | i.SetResource(theme.ViewFullScreenIcon()) 100 | i.ExtendBaseWidget(i) 101 | return i 102 | } 103 | -------------------------------------------------------------------------------- /bugs/button.go: -------------------------------------------------------------------------------- 1 | package bugs 2 | 3 | import ( 4 | "image/color" 5 | 6 | "fyne.io/fyne/v2" 7 | "fyne.io/fyne/v2/canvas" 8 | "fyne.io/fyne/v2/theme" 9 | "fyne.io/fyne/v2/widget" 10 | ) 11 | 12 | type bugRenderer struct { 13 | icon *canvas.Image 14 | label *canvas.Text 15 | 16 | objects []fyne.CanvasObject 17 | button *bugButton 18 | } 19 | 20 | const bugSize = 18 21 | 22 | // MinSize calculates the minimum size of a bug button. A fixed amount. 23 | func (b *bugRenderer) MinSize() fyne.Size { 24 | return fyne.NewSize(bugSize+theme.Padding()*2, bugSize+theme.Padding()*2) 25 | } 26 | 27 | // Layout the components of the widget 28 | func (b *bugRenderer) Layout(size fyne.Size) { 29 | inner := size.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)) 30 | b.icon.Resize(inner) 31 | b.icon.Move(fyne.NewPos(theme.Padding(), theme.Padding())) 32 | 33 | textSize := size.Height * .67 34 | textMin := fyne.MeasureText(b.label.Text, textSize, fyne.TextStyle{Bold: true}) 35 | 36 | b.label.TextSize = textSize 37 | b.label.Resize(fyne.NewSize(size.Width, textMin.Height)) 38 | b.label.Move(fyne.NewPos(0, (size.Height-textMin.Height)/2)) 39 | } 40 | 41 | // ApplyTheme is called when the bugButton may need to update it's look 42 | func (b *bugRenderer) ApplyTheme() { 43 | b.label.Color = theme.Color(theme.ColorNameForeground) 44 | b.Refresh() 45 | } 46 | 47 | func (b *bugRenderer) BackgroundColor() color.Color { 48 | return theme.Color(theme.ColorNameButton) 49 | } 50 | 51 | func (b *bugRenderer) Refresh() { 52 | b.label.Text = b.button.text 53 | 54 | b.icon.Hidden = b.button.icon == nil 55 | if b.button.icon != nil { 56 | b.icon.Resource = b.button.icon 57 | } 58 | 59 | b.Layout(b.button.Size()) 60 | canvas.Refresh(b.button) 61 | } 62 | 63 | func (b *bugRenderer) Objects() []fyne.CanvasObject { 64 | return b.objects 65 | } 66 | 67 | func (b *bugRenderer) Destroy() { 68 | } 69 | 70 | // bugButton widget is a scalable button that has a text label and icon and triggers an event func when clicked 71 | type bugButton struct { 72 | widget.BaseWidget 73 | text string 74 | icon fyne.Resource 75 | 76 | tap func(bool) 77 | } 78 | 79 | // Tapped is called when a regular tap is reported 80 | func (b *bugButton) Tapped(ev *fyne.PointEvent) { 81 | b.tap(true) 82 | } 83 | 84 | // TappedSecondary is called when an alternative tap is reported 85 | func (b *bugButton) TappedSecondary(ev *fyne.PointEvent) { 86 | b.tap(false) 87 | } 88 | 89 | func (b *bugButton) CreateRenderer() fyne.WidgetRenderer { 90 | text := canvas.NewText(b.text, theme.Color(theme.ColorNameForeground)) 91 | text.Alignment = fyne.TextAlignCenter 92 | text.TextStyle.Bold = true 93 | 94 | icon := canvas.NewImageFromResource(b.icon) 95 | icon.FillMode = canvas.ImageFillContain 96 | 97 | objects := []fyne.CanvasObject{ 98 | text, 99 | icon, 100 | } 101 | 102 | return &bugRenderer{icon, text, objects, b} 103 | } 104 | 105 | // SetText allows the button label to be changed 106 | func (b *bugButton) SetText(text string) { 107 | b.text = text 108 | 109 | b.Refresh() 110 | } 111 | 112 | // SetIcon updates the icon on a label - pass nil to hide an icon 113 | func (b *bugButton) SetIcon(icon fyne.Resource) { 114 | b.icon = icon 115 | 116 | b.Refresh() 117 | } 118 | 119 | // newButton creates a new button widget with the specified label, themed icon and tap handler 120 | func newButton(label string, icon fyne.Resource, tap func(bool)) *bugButton { 121 | button := &bugButton{text: label, icon: icon, tap: tap} 122 | button.ExtendBaseWidget(button) 123 | return button 124 | } 125 | -------------------------------------------------------------------------------- /fractal/main.go: -------------------------------------------------------------------------------- 1 | package fractal 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/canvas" 9 | "fyne.io/fyne/v2/container" 10 | "fyne.io/fyne/v2/theme" 11 | ) 12 | 13 | type fractal struct { 14 | currIterations uint 15 | currScale, currX, currY float64 16 | 17 | window fyne.Window 18 | canvas fyne.CanvasObject 19 | } 20 | 21 | func (f *fractal) Layout(objects []fyne.CanvasObject, size fyne.Size) { 22 | f.canvas.Resize(size) 23 | } 24 | 25 | func (f *fractal) MinSize(objects []fyne.CanvasObject) fyne.Size { 26 | return fyne.NewSize(320, 240) 27 | } 28 | 29 | //lint:ignore U1000 See TODO inside the .Show() method. 30 | func (f *fractal) refresh() { 31 | if f.currScale >= 1.0 { 32 | f.currIterations = 100 33 | } else { 34 | f.currIterations = uint(100 * (1 + math.Pow((math.Log10(1/f.currScale)), 1.25))) 35 | } 36 | 37 | f.window.Canvas().Refresh(f.canvas) 38 | } 39 | 40 | func (f *fractal) scaleChannel(c float64, start, end uint32) uint8 { 41 | if end >= start { 42 | return (uint8)(c*float64(uint8(end-start))) + uint8(start) 43 | } 44 | 45 | return (uint8)((1-c)*float64(uint8(start-end))) + uint8(end) 46 | } 47 | 48 | func (f *fractal) scaleColor(c float64, start, end color.Color) color.Color { 49 | r1, g1, b1, _ := start.RGBA() 50 | r2, g2, b2, _ := end.RGBA() 51 | return color.RGBA{f.scaleChannel(c, r1, r2), f.scaleChannel(c, g1, g2), f.scaleChannel(c, b1, b2), 0xff} 52 | } 53 | 54 | func (f *fractal) mandelbrot(px, py, w, h int) color.Color { 55 | drawScale := 3.5 * f.currScale 56 | aspect := (float64(h) / float64(w)) 57 | cRe := ((float64(px)/float64(w))-0.5)*drawScale + f.currX 58 | cIm := ((float64(py)/float64(w))-(0.5*aspect))*drawScale - f.currY 59 | 60 | var i uint 61 | var x, y, xsq, ysq float64 62 | 63 | for i = 0; i < f.currIterations && (xsq+ysq <= 4); i++ { 64 | xNew := float64(xsq-ysq) + cRe 65 | y = 2*x*y + cIm 66 | x = xNew 67 | 68 | xsq = x * x 69 | ysq = y * y 70 | } 71 | 72 | if i == f.currIterations { 73 | return theme.Color(theme.ColorNameBackground) 74 | } 75 | 76 | mu := (float64(i) / float64(f.currIterations)) 77 | c := math.Sin((mu / 2) * math.Pi) 78 | 79 | return f.scaleColor(c, theme.Color(theme.ColorNamePrimary), theme.Color(theme.ColorNameForeground)) 80 | } 81 | 82 | //lint:ignore U1000 See TODO inside the .Show() method. 83 | func (f *fractal) fractalRune(r rune) { 84 | if r == '+' { 85 | f.currScale /= 1.1 86 | } else if r == '-' { 87 | f.currScale *= 1.1 88 | } 89 | 90 | f.refresh() 91 | } 92 | 93 | //lint:ignore U1000 See TODO inside the .Show() method. 94 | func (f *fractal) fractalKey(ev *fyne.KeyEvent) { 95 | delta := f.currScale * 0.2 96 | if ev.Name == fyne.KeyUp { 97 | f.currY -= delta 98 | } else if ev.Name == fyne.KeyDown { 99 | f.currY += delta 100 | } else if ev.Name == fyne.KeyLeft { 101 | f.currX += delta 102 | } else if ev.Name == fyne.KeyRight { 103 | f.currX -= delta 104 | } 105 | 106 | f.refresh() 107 | } 108 | 109 | // Show loads a Mandelbrot fractal example window for the specified app context 110 | func Show(win fyne.Window) fyne.CanvasObject { 111 | fractal := &fractal{window: win} 112 | fractal.canvas = canvas.NewRasterWithPixels(fractal.mandelbrot) 113 | 114 | fractal.currIterations = 100 115 | fractal.currScale = 1.0 116 | fractal.currX = -0.75 117 | fractal.currY = 0.0 118 | 119 | return container.New(fractal, fractal.canvas) 120 | // TODO: Register, and unregister, these keys: 121 | // window.Canvas().SetOnTypedRune(fractal.fractalRune) 122 | // window.Canvas().SetOnTypedKey(fractal.fractalKey) 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Code Status 3 | Join us on Slack 4 | Support Fyne.io 5 | 6 |
7 | Build Status 8 | Coverage Status 9 |

10 | 11 | # Fyne Examples 12 | 13 | Here we will gather example apps that use the [Fyne](http://fyne.io) toolkit. 14 | 15 | You can start the main example app that links to all the others by running 16 | 17 | ```bash 18 | go run . 19 | ``` 20 | 21 | All these examples are fully scalable - try setting the `FYNE_SCALE` 22 | environment variable to override the detection of your screen's density. 23 | Many also respond to the current theme (this is default behaviour for 24 | apps built using Fyne widgets) - you can try setting `FYNE_THEME=light` 25 | to change from the default dark theme. 26 | 27 | ## Widget based examples 28 | 29 | The following examples use mostly built in widgets making applications 30 | trivial to build :). 31 | 32 | ### Calculator 33 | 34 | Moved to [calculator repository](https://github.com/fyne-io/calculator/) 35 | 36 | ### Bugs game (like MineSweeper) 37 | 38 | Hunt the squares to reveal everything apart from the bugs! 39 | 40 | ![](img/bugs.png) 41 | 42 | ### XKCD 43 | 44 | An XKCD comic browser with random and lookup features. 45 | 46 | ![](img/xkcd.png) 47 | 48 | ## Graphics based examples 49 | 50 | These examples use the Fyne canvas API to draw primitive shapes, 51 | text and images to create custom user interfaces. 52 | 53 | ### Clock 54 | 55 | A simple analog clock that matches the current theme. 56 | 57 | ![](img/clock-dark.png)   ![](img/clock-light.png) 58 | 59 | ### Fractal 60 | 61 | A fractal viewer that can be panned and zoomed 62 | 63 | ![](img/fractal.png) 64 | 65 | ### Solitaire 66 | 67 | Moved to [solitaire repository](https://github.com/fyne-io/solitaire/) 68 | 69 | ### Life 70 | 71 | Moved to [life repository](https://github.com/fyne-io/life/) 72 | 73 | -------------------------------------------------------------------------------- /bugs/board.go: -------------------------------------------------------------------------------- 1 | package bugs 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type square struct { 11 | bug bool 12 | shown bool 13 | flagged bool 14 | 15 | near int 16 | } 17 | 18 | type board struct { 19 | height, width int 20 | 21 | win, lose func() 22 | 23 | bugCount int 24 | flagCount int 25 | bugs [][]square 26 | } 27 | 28 | func (b *board) countHidden() int { 29 | count := 0 30 | 31 | for y := 0; y < b.height; y++ { 32 | for x := 0; x < b.width; x++ { 33 | item := b.bugs[y][x] 34 | 35 | if !item.shown { 36 | count++ 37 | } 38 | } 39 | } 40 | 41 | return count 42 | } 43 | 44 | func (b *board) load(count int) { 45 | b.bugCount = count 46 | b.flagCount = 0 47 | if b.bugs == nil { 48 | b.bugs = make([][]square, b.height) 49 | 50 | for y := 0; y < b.height; y++ { 51 | b.bugs[y] = make([]square, b.width) 52 | } 53 | } else { 54 | for y := 0; y < b.height; y++ { 55 | for x := 0; x < b.width; x++ { 56 | b.bugs[y][x].shown = false 57 | b.bugs[y][x].bug = false 58 | b.bugs[y][x].flagged = false 59 | b.bugs[y][x].near = 0 60 | } 61 | } 62 | } 63 | 64 | for i := 0; i < count; i++ { 65 | x := rand.Intn(b.width) 66 | y := rand.Intn(b.height) 67 | 68 | if b.bugs[y][x].bug { 69 | i-- 70 | } else { 71 | b.setMine(x, y) 72 | } 73 | } 74 | } 75 | 76 | func (b *board) incSquare(x, y int) { 77 | if x < 0 || y < 0 { 78 | return 79 | } 80 | if x >= b.width || y >= b.height { 81 | return 82 | } 83 | 84 | if b.bugs[y][x].bug { 85 | return 86 | } 87 | b.bugs[y][x].near++ 88 | } 89 | 90 | func (b *board) setMine(x, y int) { 91 | if b.bugs[y][x].bug { 92 | return 93 | } 94 | b.bugs[y][x].bug = true 95 | 96 | b.incSquare(x-1, y-1) 97 | b.incSquare(x, y-1) 98 | b.incSquare(x+1, y-1) 99 | 100 | b.incSquare(x-1, y) 101 | b.incSquare(x+1, y) 102 | 103 | b.incSquare(x-1, y+1) 104 | b.incSquare(x, y+1) 105 | b.incSquare(x+1, y+1) 106 | } 107 | 108 | func (b *board) reveal(x, y int) { 109 | if x < 0 || y < 0 { 110 | return 111 | } 112 | if x >= b.width || y >= b.height { 113 | return 114 | } 115 | 116 | sq := b.bugs[y][x] 117 | if sq.shown || sq.flagged { 118 | return 119 | } 120 | b.bugs[y][x].shown = true 121 | 122 | if sq.bug { 123 | if b.lose != nil { 124 | b.lose() 125 | } 126 | return 127 | } 128 | 129 | if sq.near == 0 { 130 | b.reveal(x-1, y-1) 131 | b.reveal(x, y-1) 132 | b.reveal(x+1, y-1) 133 | b.reveal(x-1, y) 134 | b.reveal(x+1, y) 135 | b.reveal(x-1, y+1) 136 | b.reveal(x, y+1) 137 | b.reveal(x+1, y+1) 138 | } 139 | 140 | if b.countHidden() == b.bugCount && b.win != nil { 141 | b.win() 142 | } 143 | } 144 | 145 | func (b *board) flag(x, y int) { 146 | if x < 0 || y < 0 { 147 | return 148 | } 149 | if x >= b.width || y >= b.height { 150 | return 151 | } 152 | 153 | sq := b.bugs[y][x] 154 | if sq.shown { 155 | return 156 | } 157 | 158 | if sq.flagged { 159 | b.bugs[y][x].flagged = false 160 | b.flagCount-- 161 | } else { 162 | b.bugs[y][x].flagged = true 163 | b.flagCount++ 164 | } 165 | } 166 | 167 | func (b *board) flagged(x, y int) bool { 168 | if x < 0 || y < 0 { 169 | return false 170 | } 171 | if x >= b.width || y >= b.height { 172 | return false 173 | } 174 | 175 | sq := b.bugs[y][x] 176 | return sq.flagged 177 | } 178 | 179 | func (b *board) remaining() int { 180 | return b.bugCount - b.flagCount 181 | } 182 | 183 | func squareString(sq square) string { 184 | if !sq.shown { 185 | return "?" 186 | } else if sq.bug { 187 | return "*" 188 | } else if sq.near == 0 { 189 | return " " 190 | } 191 | 192 | return fmt.Sprintf("%d", sq.near) 193 | } 194 | 195 | func (b *board) String() string { 196 | buf := strings.Builder{} 197 | for y := 0; y < b.height; y++ { 198 | for x := 0; x < b.width; x++ { 199 | sq := b.bugs[y][x] 200 | 201 | buf.WriteString(squareString(sq)) 202 | } 203 | buf.WriteByte('\n') 204 | } 205 | 206 | return buf.String() 207 | } 208 | 209 | func newBoard(height, width int) *board { 210 | rand.Seed(time.Now().Unix()) 211 | return &board{height: height, width: width} 212 | } 213 | -------------------------------------------------------------------------------- /xkcd/main.go: -------------------------------------------------------------------------------- 1 | package xkcd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "fyne.io/fyne/v2" 17 | "fyne.io/fyne/v2/canvas" 18 | "fyne.io/fyne/v2/container" 19 | "fyne.io/fyne/v2/layout" 20 | "fyne.io/fyne/v2/widget" 21 | ) 22 | 23 | // XKCD is an app to get xkcd images and display them 24 | type XKCD struct { 25 | ID int `json:"num"` 26 | Title string `json:"title"` 27 | Day string `json:"day"` 28 | Month string `json:"month"` 29 | Year string `json:"year"` 30 | Link string `json:"link"` 31 | SafeTitle string `json:"safe_title"` 32 | Transcript string `json:"transcript"` 33 | News string `json:"news"` 34 | Alt string `json:"alt"` 35 | Img string `json:"img"` 36 | 37 | image *canvas.Image 38 | iDEntry *widget.Entry 39 | labels map[string]*widget.Label 40 | } 41 | 42 | func (x *XKCD) newLabel(name string) *widget.Label { 43 | w := widget.NewLabel("") 44 | if name == "alt" || name == "transcript" { 45 | w.Wrapping = fyne.TextWrapWord 46 | } 47 | x.labels[name] = w 48 | return w 49 | } 50 | 51 | // NewXKCD returns a new xkcd app 52 | func NewXKCD() *XKCD { 53 | rand.Seed(time.Now().UnixNano()) 54 | return &XKCD{ 55 | labels: make(map[string]*widget.Label), 56 | } 57 | } 58 | 59 | // Submit will lookup the xkcd cartoon and do something useful with it 60 | func (x *XKCD) Submit() { 61 | // Get the ID 62 | id, _ := strconv.Atoi(x.iDEntry.Text) 63 | if id == 0 { 64 | id = rand.Intn(2075) 65 | } 66 | 67 | resp, err := http.Get(fmt.Sprintf("https://xkcd.com/%d/info.0.json", id)) 68 | if err != nil { 69 | return 70 | } 71 | defer resp.Body.Close() 72 | if resp.StatusCode == http.StatusOK { 73 | data, _ := io.ReadAll(resp.Body) 74 | json.Unmarshal(data, x) 75 | x.DataToScreen() 76 | } else { 77 | fmt.Println("Error getting ID", id, resp.Status, resp.StatusCode) 78 | } 79 | } 80 | 81 | func (x *XKCD) downloadImage(url string) { 82 | response, e := http.Get(url) 83 | if e != nil { 84 | log.Fatal(e) 85 | } 86 | defer response.Body.Close() 87 | 88 | file, err := os.CreateTemp(os.TempDir(), "xkcd.png") 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | defer file.Close() 93 | 94 | _, err = io.Copy(file, response.Body) 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | 99 | fyne.Do(func() { 100 | x.image.Image = nil 101 | x.image.File = file.Name() 102 | canvas.Refresh(x.image) 103 | }) 104 | } 105 | 106 | // DataToScreen copies the data model to the screen 107 | func (x *XKCD) DataToScreen() { 108 | myType := reflect.TypeOf(x).Elem() 109 | myValue := reflect.ValueOf(x).Elem() 110 | for i := 0; i < myType.NumField(); i++ { 111 | tag := myType.Field(i).Tag.Get("json") 112 | switch tag { 113 | case "": // not a display field 114 | case "img": // special field for images 115 | url := myValue.Field(i).String() 116 | 117 | go x.downloadImage(url) 118 | case "num": 119 | v := myValue.Field(i).Int() 120 | x.iDEntry.SetText(fmt.Sprintf("%d", v)) 121 | default: 122 | v := myValue.Field(i).String() 123 | if newline := strings.IndexAny(v, "\n.-,"); newline > -1 { 124 | v = v[:newline] + "..." 125 | } 126 | x.labels[tag].SetText(v) 127 | } 128 | } 129 | } 130 | 131 | // NewForm generates a new XKCD form 132 | func (x *XKCD) NewForm(w fyne.Window) fyne.Widget { 133 | form := &widget.Form{} 134 | tt := reflect.TypeOf(x).Elem() 135 | for i := 0; i < tt.NumField(); i++ { 136 | fld := tt.Field(i) 137 | tag := fld.Tag.Get("json") 138 | switch tag { 139 | case "": // not a display field 140 | case "img": // special field for images 141 | // we created this in the setup 142 | case "num": // special field for ID 143 | entry := widget.NewEntry() 144 | x.iDEntry = entry 145 | form.Append(fld.Name, entry) 146 | default: 147 | form.Append(fld.Name, x.newLabel(tag)) 148 | } 149 | } 150 | return form 151 | } 152 | 153 | // Show starts a new xkcd widget 154 | func Show(win fyne.Window) fyne.CanvasObject { 155 | x := NewXKCD() 156 | 157 | form := x.NewForm(win) 158 | submit := widget.NewButton("Submit", func() { 159 | x.Submit() 160 | }) 161 | submit.Importance = widget.HighImportance 162 | buttons := container.NewHBox( 163 | layout.NewSpacer(), 164 | widget.NewButton("Random", func() { 165 | x.iDEntry.Text = "" 166 | x.Submit() 167 | }), 168 | submit) 169 | x.image = &canvas.Image{FillMode: canvas.ImageFillOriginal} 170 | return container.NewBorder(form, buttons, nil, nil, x.image) 171 | } 172 | -------------------------------------------------------------------------------- /clock/clock.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "math" 5 | "time" 6 | 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/canvas" 9 | "fyne.io/fyne/v2/container" 10 | "fyne.io/fyne/v2/theme" 11 | ) 12 | 13 | type clockLayout struct { 14 | hour, minute, second *canvas.Line 15 | pips [12]*canvas.Line 16 | hourDot, secondDot, face *canvas.Circle 17 | 18 | canvas fyne.CanvasObject 19 | stop bool 20 | } 21 | 22 | func (c *clockLayout) rotate(hand *canvas.Line, middle fyne.Position, facePosition float64, offset, length float32) { 23 | rotation := math.Pi * 2 / 60 * facePosition 24 | x2 := length * float32(math.Sin(rotation)) 25 | y2 := -length * float32(math.Cos(rotation)) 26 | 27 | offX := float32(0) 28 | offY := float32(0) 29 | if offset > 0 { 30 | offX += offset * float32(math.Sin(rotation)) 31 | offY += -offset * float32(math.Cos(rotation)) 32 | } 33 | 34 | hand.Position1 = fyne.NewPos(middle.X+offX, middle.Y+offY) 35 | hand.Position2 = fyne.NewPos(middle.X+offX+x2, middle.Y+offY+y2) 36 | hand.Refresh() 37 | } 38 | 39 | func (c *clockLayout) Layout(_ []fyne.CanvasObject, size fyne.Size) { 40 | diameter := fyne.Min(size.Width, size.Height) 41 | radius := diameter / 2 42 | dotRadius := radius / 12 43 | smallDotRadius := dotRadius / 8 44 | 45 | stroke := diameter / 40 46 | midStroke := diameter / 90 47 | smallStroke := diameter / 200 48 | 49 | size = fyne.NewSize(diameter, diameter) 50 | middle := fyne.NewPos(size.Width/2, size.Height/2) 51 | topleft := fyne.NewPos(middle.X-radius, middle.Y-radius) 52 | 53 | c.face.Resize(size) 54 | c.face.Move(topleft) 55 | 56 | c.hour.StrokeWidth = stroke 57 | c.rotate(c.hour, middle, float64((time.Now().Hour()%12)*5)+(float64(time.Now().Minute())/12), dotRadius, radius/2) 58 | c.minute.StrokeWidth = midStroke 59 | c.rotate(c.minute, middle, float64(time.Now().Minute())+(float64(time.Now().Second())/60), dotRadius, radius*.9) 60 | c.second.StrokeWidth = smallStroke 61 | c.rotate(c.second, middle, float64(time.Now().Second()), 0, radius-3) 62 | 63 | c.hourDot.StrokeWidth = stroke 64 | c.hourDot.Resize(fyne.NewSize(dotRadius*2, dotRadius*2)) 65 | c.hourDot.Move(fyne.NewPos(middle.X-dotRadius, middle.Y-dotRadius)) 66 | c.secondDot.StrokeWidth = smallStroke 67 | c.secondDot.Resize(fyne.NewSize(smallDotRadius*2, smallDotRadius*2)) 68 | c.secondDot.Move(fyne.NewPos(middle.X-smallDotRadius, middle.Y-smallDotRadius)) 69 | c.face.StrokeWidth = smallStroke 70 | 71 | for i, p := range c.pips { 72 | c.rotate(p, middle, float64((i)*5), radius/8*7, radius/8) 73 | p.StrokeWidth = smallStroke 74 | } 75 | } 76 | 77 | func (c *clockLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { 78 | return fyne.NewSize(200, 200) 79 | } 80 | 81 | func (c *clockLayout) render() *fyne.Container { 82 | c.hourDot = &canvas.Circle{StrokeColor: theme.Color(theme.ColorNameForeground), StrokeWidth: 5} 83 | c.secondDot = &canvas.Circle{StrokeColor: theme.Color(theme.ColorNamePrimary), StrokeWidth: 3} 84 | c.face = &canvas.Circle{StrokeColor: theme.Color(theme.ColorNameDisabled), StrokeWidth: 1} 85 | 86 | c.hour = &canvas.Line{StrokeColor: theme.Color(theme.ColorNameForeground), StrokeWidth: 5} 87 | c.minute = &canvas.Line{StrokeColor: theme.Color(theme.ColorNameForeground), StrokeWidth: 3} 88 | c.second = &canvas.Line{StrokeColor: theme.Color(theme.ColorNamePrimary), StrokeWidth: 1} 89 | 90 | container := container.NewWithoutLayout(c.hourDot, c.secondDot) 91 | for i := range c.pips { 92 | pip := &canvas.Line{StrokeColor: theme.Color(theme.ColorNameDisabled), StrokeWidth: 1} 93 | container.Add(pip) 94 | c.pips[i] = pip 95 | } 96 | container.Objects = append(container.Objects, c.face, c.hour, c.minute, c.second) 97 | container.Layout = c 98 | 99 | c.canvas = container 100 | return container 101 | } 102 | 103 | func (c *clockLayout) animate(co fyne.CanvasObject) { 104 | tick := time.NewTicker(time.Second) 105 | go func() { 106 | for !c.stop { 107 | fyne.Do(func() { 108 | c.Layout(nil, co.Size()) 109 | }) 110 | <-tick.C 111 | } 112 | }() 113 | } 114 | 115 | func (c *clockLayout) applyTheme(_ fyne.Settings) { 116 | c.hourDot.StrokeColor = theme.Color(theme.ColorNameForeground) 117 | c.secondDot.StrokeColor = theme.Color(theme.ColorNamePrimary) 118 | c.face.StrokeColor = theme.Color(theme.ColorNameDisabled) 119 | 120 | c.hour.StrokeColor = theme.Color(theme.ColorNameForeground) 121 | c.minute.StrokeColor = theme.Color(theme.ColorNameForeground) 122 | c.second.StrokeColor = theme.Color(theme.ColorNamePrimary) 123 | 124 | for _, p := range c.pips { 125 | p.StrokeColor = theme.Color(theme.ColorNameDisabled) 126 | } 127 | } 128 | 129 | // Show loads a clock example window for the specified app context 130 | func Show(win fyne.Window) fyne.CanvasObject { 131 | clock := &clockLayout{} 132 | win.SetOnClosed(func() { 133 | clock.stop = true 134 | }) 135 | 136 | content := clock.render() 137 | go clock.animate(content) 138 | 139 | fyne.CurrentApp().Settings().AddListener(func(settings fyne.Settings) { 140 | clock.applyTheme(settings) 141 | }) 142 | 143 | return content 144 | } 145 | -------------------------------------------------------------------------------- /bugs/main.go: -------------------------------------------------------------------------------- 1 | package bugs 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/canvas" 9 | "fyne.io/fyne/v2/container" 10 | "fyne.io/fyne/v2/dialog" 11 | "fyne.io/fyne/v2/theme" 12 | "fyne.io/fyne/v2/widget" 13 | ) 14 | 15 | var bug, code, flag *theme.ThemedResource 16 | 17 | func init() { 18 | bug = theme.NewThemedResource(bugIcon) 19 | code = theme.NewThemedResource(codeIcon) 20 | flag = theme.NewThemedResource(flagIcon) 21 | } 22 | 23 | type gameRenderer struct { 24 | grid *fyne.Container 25 | header fyne.CanvasObject 26 | 27 | game *game 28 | } 29 | 30 | func (g *gameRenderer) MinSize() fyne.Size { 31 | return g.grid.MinSize().Add(fyne.NewSize(0, g.header.MinSize().Height)) 32 | } 33 | 34 | func (g *gameRenderer) Layout(size fyne.Size) { 35 | headerHeight := g.header.MinSize().Height 36 | g.header.Resize(fyne.NewSize(size.Width, headerHeight)) 37 | g.grid.Move(fyne.NewPos(0, headerHeight)) // TODO why ignored? 38 | gridSize := size.Subtract(fyne.NewSize(0, headerHeight)) 39 | g.grid.Layout.Layout(g.grid.Objects, gridSize) 40 | } 41 | 42 | func (g *gameRenderer) ApplyTheme() { 43 | } 44 | 45 | func (g *gameRenderer) BackgroundColor() color.Color { 46 | return theme.Color(theme.ColorNameBackground) 47 | } 48 | 49 | func (g *gameRenderer) Refresh() { 50 | canvas.Refresh(g.grid) 51 | } 52 | 53 | func (g *gameRenderer) Objects() []fyne.CanvasObject { 54 | return []fyne.CanvasObject{g.grid, g.header} 55 | } 56 | 57 | func (g *gameRenderer) Destroy() { 58 | } 59 | 60 | type game struct { 61 | widget.BaseWidget 62 | board *board 63 | remain *widget.Label 64 | 65 | grid *fyne.Container 66 | window fyne.Window 67 | } 68 | 69 | func (g *game) refreshSquare(x, y int) { 70 | if x < 0 || y < 0 || x >= g.board.width || y >= g.board.height { 71 | return 72 | } 73 | 74 | sq := g.board.bugs[y][x] 75 | i := y*g.board.width + x 76 | button := g.grid.Objects[i].(*bugButton) 77 | 78 | if sq.flagged { 79 | if button.icon == flag { 80 | return 81 | } 82 | button.icon = flag 83 | button.text = "" 84 | } else if !sq.shown { 85 | if button.icon == code { 86 | return 87 | } 88 | button.icon = code 89 | button.text = "" 90 | } else if sq.bug { 91 | if button.icon == bug { 92 | return 93 | } 94 | button.icon = bug 95 | button.text = "" 96 | } else if button.icon == nil { 97 | return 98 | } else { 99 | button.icon = nil 100 | button.text = squareString(sq) 101 | } 102 | 103 | button.Refresh() 104 | } 105 | 106 | func (g *game) refreshAround(xp, yp, d int) { 107 | x, y := xp-d, yp-d 108 | for ; x < xp+d; x++ { 109 | g.refreshSquare(x, y) 110 | } 111 | for ; y < yp+d; y++ { 112 | g.refreshSquare(x, y) 113 | } 114 | for ; x > xp-d; x-- { 115 | g.refreshSquare(x, y) 116 | } 117 | for ; y > yp-d; y-- { 118 | g.refreshSquare(x, y) 119 | } 120 | } 121 | 122 | func (g *game) refreshFrom(x, y int) { 123 | g.refreshSquare(x, y) 124 | 125 | for i := 1; i < int(fyne.Max(float32(g.board.width), float32(g.board.height))); i++ { 126 | g.refreshAround(x, y, i) 127 | } 128 | } 129 | 130 | func (g *game) CreateRenderer() fyne.WidgetRenderer { 131 | renderer := &gameRenderer{game: g} 132 | title := widget.NewLabel("Hunt bugs!") 133 | g.remain = widget.NewLabel("") 134 | g.updateRemain() 135 | renderer.header = container.NewBorder(nil, nil, title, g.remain) 136 | 137 | var buttons []fyne.CanvasObject 138 | for y := 0; y < g.board.height; y++ { 139 | for x := 0; x < g.board.width; x++ { 140 | xx, yy := x, y 141 | 142 | buttons = append(buttons, newButton("", code, func(reveal bool) { 143 | if reveal { 144 | g.squareReveal(xx, yy) 145 | } else { 146 | g.squareFlagged(xx, yy) 147 | } 148 | 149 | g.updateRemain() 150 | })) 151 | } 152 | } 153 | 154 | renderer.grid = container.NewGridWithColumns(g.board.width, buttons...) 155 | g.grid = renderer.grid 156 | return renderer 157 | } 158 | 159 | func (g *game) squareReveal(x, y int) { 160 | if g.board.flagged(x, y) { 161 | return 162 | } 163 | 164 | g.board.reveal(x, y) 165 | g.refreshFrom(x, y) 166 | } 167 | 168 | func (g *game) squareFlagged(x, y int) { 169 | g.board.flag(x, y) 170 | g.refreshSquare(x, y) 171 | } 172 | 173 | func (g *game) loseCallback(yes bool) { 174 | if !yes { 175 | return 176 | } 177 | 178 | g.board.load(40) 179 | g.updateRemain() 180 | g.refreshFrom(g.board.width/2, g.board.height/2) 181 | } 182 | 183 | func (g *game) win() { 184 | dialog.ShowInformation("You won!", "Congratulations, you found all the bugs", g.window) 185 | } 186 | 187 | func (g *game) lose() { 188 | dialog.ShowConfirm("You lost!", "You hit a bug and lost the game, try again?", g.loseCallback, g.window) 189 | } 190 | 191 | func (g *game) updateRemain() { 192 | g.remain.SetText(fmt.Sprintf("remaining: %d", g.board.remaining())) 193 | } 194 | 195 | func newGame(f *board) *game { 196 | g := &game{board: f} 197 | g.ExtendBaseWidget(g) 198 | 199 | return g 200 | } 201 | 202 | // Show starts a new bugs game 203 | func Show(win fyne.Window) fyne.CanvasObject { 204 | b := newBoard(20, 14) 205 | game := newGame(b) 206 | 207 | b.win = game.win 208 | b.lose = game.lose 209 | b.load(40) 210 | 211 | game.window = win 212 | return game 213 | } 214 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ= 2 | fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE= 3 | fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI= 4 | fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= 5 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 6 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= 11 | github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= 12 | github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= 13 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 14 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 15 | github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= 16 | github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= 17 | github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= 18 | github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= 19 | github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= 20 | github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= 21 | github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= 22 | github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= 23 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= 24 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 25 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= 26 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 27 | github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= 28 | github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= 29 | github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= 30 | github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= 31 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= 32 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 33 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 34 | github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= 35 | github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= 36 | github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= 37 | github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= 38 | github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= 39 | github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= 40 | github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= 41 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= 42 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= 43 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 44 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 45 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 46 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 47 | github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= 48 | github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 50 | github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= 54 | github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= 55 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= 56 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= 57 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= 58 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= 59 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 60 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 61 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 62 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 63 | golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= 64 | golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 65 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 66 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 67 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 68 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 69 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 70 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 73 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | --------------------------------------------------------------------------------