├── .github └── workflows │ ├── platform_tests.yml │ └── static_analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── icon.go ├── img └── screenshot.png ├── json.go ├── json_test.go ├── layout.go ├── main.go ├── utils.go ├── widget.go └── wrap.go /.github/workflows/platform_tests.yml: -------------------------------------------------------------------------------- 1 | name: Platform Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | platform_tests: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | go-version: [1.14.x, 1.17.x] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | 18 | - name: Get dependencies 19 | run: sudo apt-get update && sudo apt-get install golang gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev 20 | if: ${{ runner.os == 'Linux' }} 21 | 22 | #- name: Verify go modules 23 | # run: | 24 | # if [ "$GO111MODULE" == "on" ] 25 | # then 26 | # # For some reason `git diff-index HEAD` does not work properly if the following line is missing. 27 | # git diff 28 | # # check that go mod tidy does not change go.mod/go.sum 29 | # go mod tidy && git diff-index --quiet HEAD -- || ( echo "go.mod/go.sum not up-to-date"; git diff-index HEAD --; false ) 30 | # fi 31 | 32 | - name: Tests 33 | run: go test -tags ci ./... 34 | 35 | - name: Update coverage 36 | run: | 37 | GO111MODULE=off go get github.com/mattn/goveralls 38 | 39 | set -e 40 | go test -tags ci -covermode=atomic -coverprofile=coverage.out ./... 41 | if [ $coverage -lt 28 ]; then echo "Test coverage lowered"; exit 1; fi 42 | if: ${{ runner.os == 'Linux' }} 43 | 44 | - name: Update PR Coverage 45 | uses: shogo82148/actions-goveralls@v1 46 | with: 47 | path-to-profile: coverage.out 48 | if: ${{ runner.os == 'Linux' && github.event_name == 'push' }} 49 | -------------------------------------------------------------------------------- /.github/workflows/static_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | checks: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-go@v2 10 | with: 11 | go-version: '^1.16.x' 12 | 13 | - name: Get dependencies 14 | run: | 15 | sudo apt-get update 16 | sudo apt-get install golang gcc libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev libx11-dev xorg-dev 17 | GO111MODULE=off go get golang.org/x/tools/cmd/goimports 18 | GO111MODULE=off go get github.com/fzipp/gocyclo/cmd/gocyclo 19 | GO111MODULE=off go get golang.org/x/lint/golint 20 | GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck 21 | - name: Cleanup repository 22 | run: rm -rf vendor/ 23 | 24 | - name: Vet 25 | run: go vet -tags ci ./... 26 | 27 | - name: Goimports 28 | run: test -z $(goimports -e -d . | tee /dev/stderr) 29 | 30 | - name: Gocyclo 31 | run: gocyclo -over 50 . 32 | 33 | - name: Golint 34 | run: golint -set_exit_status $(go list -tags ci ./...) 35 | 36 | - name: Staticcheck 37 | run: CGO_ENABLED=1 staticcheck -f stylish ./... 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | fynebuilder 3 | tmp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Andy Williams 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fyne GUI Builder 2 | 3 | This project has been moved into [Defyne](https://github.com/fyne-io/defyne) 4 | 5 | Really early days proof of concept. 6 | Feel free to play, but don't expect it to do much! 7 | 8 | ![](img/screenshot.png) 9 | 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/andydotxyz/fynebuilder 2 | 3 | go 1.14 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.1.1-0.20211004204655-1e6e32efc443 7 | github.com/stretchr/testify v1.5.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | fyne.io/fyne/v2 v2.1.1-0.20211004204655-1e6e32efc443 h1:Sib4cTILmZat68f2eZs8SPRePl1v8KkOxIOTiPjuGw8= 2 | fyne.io/fyne/v2 v2.1.1-0.20211004204655-1e6e32efc443/go.mod h1:c1vwI38Ebd0dAdxVa6H1Pj6/+cK1xtDy61+I31g+s14= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 5 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= 6 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= 12 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= 13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 14 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 15 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU= 16 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= 17 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= 18 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 19 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 20 | github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= 21 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 22 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= 23 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= 24 | github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= 25 | github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc= 30 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 31 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 32 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 33 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 38 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 39 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 40 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 41 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= 42 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= 43 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= 44 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 47 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 48 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 49 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 50 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 51 | github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E= 52 | github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 53 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 54 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 55 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= 56 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 57 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 58 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 60 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= 61 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 72 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 74 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 75 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 85 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 87 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 88 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 89 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 90 | -------------------------------------------------------------------------------- /icon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/theme" 9 | ) 10 | 11 | var ( 12 | // iconNames is an array with the list of names of all the icons 13 | iconNames = extractIconNames() 14 | 15 | // iconsReverse Contains the key value pair where the key is the address of the icon and the value is the name 16 | iconReverse map[string]string 17 | 18 | // icons Has the hashmap of icons from the standard theme. 19 | // ToDo: Will have to look for a way to sync the list from `fyne_demo` 20 | icons map[string]fyne.Resource 21 | ) 22 | 23 | func initIcons() { 24 | icons = map[string]fyne.Resource{ 25 | "CancelIcon": theme.CancelIcon(), 26 | "ConfirmIcon": theme.ConfirmIcon(), 27 | "DeleteIcon": theme.DeleteIcon(), 28 | "SearchIcon": theme.SearchIcon(), 29 | "SearchReplaceIcon": theme.SearchReplaceIcon(), 30 | 31 | "CheckButtonIcon": theme.CheckButtonIcon(), 32 | "CheckButtonCheckedIcon": theme.CheckButtonCheckedIcon(), 33 | "RadioButtonIcon": theme.RadioButtonIcon(), 34 | "RadioButtonCheckedIcon": theme.RadioButtonCheckedIcon(), 35 | 36 | "ColorAchromaticIcon": theme.ColorAchromaticIcon(), 37 | "ColorChromaticIcon": theme.ColorChromaticIcon(), 38 | "ColorPaletteIcon": theme.ColorPaletteIcon(), 39 | 40 | "ContentAddIcon": theme.ContentAddIcon(), 41 | "ContentRemoveIcon": theme.ContentRemoveIcon(), 42 | "ContentClearIcon": theme.ContentClearIcon(), 43 | "ContentCutIcon": theme.ContentCutIcon(), 44 | "ContentCopyIcon": theme.ContentCopyIcon(), 45 | "ContentPasteIcon": theme.ContentPasteIcon(), 46 | "ContentRedoIcon": theme.ContentRedoIcon(), 47 | "ContentUndoIcon": theme.ContentUndoIcon(), 48 | 49 | "InfoIcon": theme.InfoIcon(), 50 | "ErrorIcon": theme.ErrorIcon(), 51 | "QuestionIcon": theme.QuestionIcon(), 52 | "WarningIcon": theme.WarningIcon(), 53 | 54 | "DocumentIcon": theme.DocumentIcon(), 55 | "DocumentCreateIcon": theme.DocumentCreateIcon(), 56 | "DocumentPrintIcon": theme.DocumentPrintIcon(), 57 | "DocumentSaveIcon": theme.DocumentSaveIcon(), 58 | 59 | "FileIcon": theme.FileIcon(), 60 | "FileApplicationIcon": theme.FileApplicationIcon(), 61 | "FileAudioIcon": theme.FileAudioIcon(), 62 | "FileImageIcon": theme.FileImageIcon(), 63 | "FileTextIcon": theme.FileTextIcon(), 64 | "FileVideoIcon": theme.FileVideoIcon(), 65 | "FolderIcon": theme.FolderIcon(), 66 | "FolderNewIcon": theme.FolderNewIcon(), 67 | "FolderOpenIcon": theme.FolderOpenIcon(), 68 | "ComputerIcon": theme.ComputerIcon(), 69 | "HomeIcon": theme.HomeIcon(), 70 | "HelpIcon": theme.HelpIcon(), 71 | "HistoryIcon": theme.HistoryIcon(), 72 | "SettingsIcon": theme.SettingsIcon(), 73 | "StorageIcon": theme.StorageIcon(), 74 | "DownloadIcon": theme.DownloadIcon(), 75 | // "UploadIcon": theme.UploadIcon(), 76 | 77 | "ViewFullScreenIcon": theme.ViewFullScreenIcon(), 78 | "ViewRestoreIcon": theme.ViewRestoreIcon(), 79 | "ViewRefreshIcon": theme.ViewRefreshIcon(), 80 | "VisibilityIcon": theme.VisibilityIcon(), 81 | "VisibilityOffIcon": theme.VisibilityOffIcon(), 82 | "ZoomFitIcon": theme.ZoomFitIcon(), 83 | "ZoomInIcon": theme.ZoomInIcon(), 84 | "ZoomOutIcon": theme.ZoomOutIcon(), 85 | 86 | "MoveDownIcon": theme.MoveDownIcon(), 87 | "MoveUpIcon": theme.MoveUpIcon(), 88 | 89 | "NavigateBackIcon": theme.NavigateBackIcon(), 90 | "NavigateNextIcon": theme.NavigateNextIcon(), 91 | 92 | "MenuIcon": theme.MenuIcon(), 93 | "MenuExpandIcon": theme.MenuExpandIcon(), 94 | "MenuDropDownIcon": theme.MenuDropDownIcon(), 95 | "MenuDropUpIcon": theme.MenuDropUpIcon(), 96 | 97 | "MailAttachmentIcon": theme.MailAttachmentIcon(), 98 | "MailComposeIcon": theme.MailComposeIcon(), 99 | "MailForwardIcon": theme.MailForwardIcon(), 100 | "MailReplyIcon": theme.MailReplyIcon(), 101 | "MailReplyAllIcon": theme.MailReplyAllIcon(), 102 | "MailSendIcon": theme.MailSendIcon(), 103 | 104 | "MediaFastForwardIcon": theme.MediaFastForwardIcon(), 105 | "MediaFastRewindIcon": theme.MediaFastRewindIcon(), 106 | "MediaPauseIcon": theme.MediaPauseIcon(), 107 | "MediaPlayIcon": theme.MediaPlayIcon(), 108 | // "MediaStopIcon": theme.MediaStopIcon(), 109 | "MediaRecordIcon": theme.MediaRecordIcon(), 110 | "MediaReplayIcon": theme.MediaReplayIcon(), 111 | "MediaSkipNextIcon": theme.MediaSkipNextIcon(), 112 | "MediaSkipPreviousIcon": theme.MediaSkipPreviousIcon(), 113 | 114 | "VolumeDownIcon": theme.VolumeDownIcon(), 115 | "VolumeMuteIcon": theme.VolumeMuteIcon(), 116 | "VolumeUpIcon": theme.VolumeUpIcon(), 117 | } 118 | iconNames = extractIconNames() 119 | iconReverse = reverseIconMap() 120 | } 121 | 122 | // extractIconNames returns all the list of names of all the icons from the hashmap `icons` 123 | func extractIconNames() []string { 124 | var iconNamesFromData = make([]string, len(icons)) 125 | i := 0 126 | for k := range icons { 127 | iconNamesFromData[i] = k 128 | i++ 129 | } 130 | 131 | sort.Strings(iconNamesFromData) 132 | return iconNamesFromData 133 | } 134 | 135 | // reverseIconMap returns all the list of icons and their addresses 136 | func reverseIconMap() map[string]string { 137 | var iconReverseFromData = make(map[string]string, len(icons)) 138 | for k, v := range icons { 139 | s := fmt.Sprintf("%p", v) 140 | iconReverseFromData[s] = k 141 | } 142 | return iconReverseFromData 143 | } 144 | -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andydotxyz/fynebuilder/a122833965a1dc8887abc68d6f4af41ad060d39d/img/screenshot.png -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "reflect" 7 | "strings" 8 | 9 | "fyne.io/fyne/v2" 10 | ) 11 | 12 | type canvObj struct { 13 | Type string 14 | Struct fyne.CanvasObject `json:",omitempty"` 15 | } 16 | 17 | type cont struct { 18 | canvObj 19 | Layout string `json:",omitempty"` 20 | Objects []interface{} 21 | } 22 | 23 | func encodeObj(obj fyne.CanvasObject) interface{} { 24 | if c, ok := obj.(*fyne.Container); ok { // the content of an overlayWidget container 25 | return encodeObj(c.Objects[0]) // 0 is the widget, 1 is the overlayWidget 26 | } else if c, ok := obj.(*overlayContainer); ok { 27 | var node cont 28 | node.Type = "*fyne.Container" 29 | node.Layout = strings.Split(reflect.TypeOf(c.c.Layout).String(), ".")[1] 30 | node.Layout = strings.ToTitle(node.Layout[0:1]) + node.Layout[1:] 31 | p := strings.Index(node.Layout, "Layout") 32 | if p > 0 { 33 | node.Layout = node.Layout[:p] 34 | } 35 | if node.Layout == "Box" { 36 | node.Layout = "VBox" // TODO remove this hack with layoutProps 37 | } 38 | for _, o := range c.c.Objects { 39 | node.Objects = append(node.Objects, encodeObj(o)) 40 | } 41 | return &node 42 | } 43 | 44 | return encodeWidget(obj) 45 | } 46 | 47 | func encodeWidget(obj fyne.CanvasObject) interface{} { 48 | return &canvObj{Type: reflect.TypeOf(obj).String(), Struct: obj} 49 | } 50 | 51 | // DecodeJSON returns a tree of `CanvasObject` elements from the provided JSON `Reader`. 52 | func DecodeJSON(r io.Reader) fyne.CanvasObject { 53 | var data interface{} 54 | _ = json.NewDecoder(r).Decode(&data) 55 | 56 | return decodeMap(data.(map[string]interface{})) 57 | } 58 | 59 | func decodeTextStyle(m map[string]interface{}) (s fyne.TextStyle) { 60 | if m["Bold"] == true { 61 | s.Bold = true 62 | } 63 | if m["Italic"] == true { 64 | s.Italic = true 65 | } 66 | if m["Monospace"] == true { 67 | s.Monospace = true 68 | } 69 | 70 | if m["TabWidth"] != 0 { 71 | s.TabWidth = int(m["TabWidth"].(float64)) 72 | } 73 | return 74 | } 75 | 76 | func decodeMap(m map[string]interface{}) fyne.CanvasObject { 77 | if m["Type"] == "*fyne.Container" { 78 | obj := &fyne.Container{} 79 | obj.Layout = layouts[m["Layout"].(string)].create(nil) 80 | for _, o := range m["Objects"].([]interface{}) { 81 | obj.Objects = append(obj.Objects, decodeMap(o.(map[string]interface{}))) 82 | } 83 | return obj 84 | } 85 | 86 | obj := widgets[m["Type"].(string)].create() 87 | e := reflect.ValueOf(obj).Elem() 88 | for k, v := range m["Struct"].(map[string]interface{}) { 89 | f := e.FieldByName(k) 90 | 91 | if f.Type().String() == "fyne.TextAlign" || f.Type().String() == "fyne.TextWrap" || 92 | f.Type().String() == "widget.ButtonAlign" || f.Type().String() == "widget.ButtonImportance" || f.Type().String() == "widget.ButtonIconPlacement" { 93 | f.SetInt(int64(reflect.ValueOf(v).Float())) 94 | } else if f.Type().String() == "fyne.TextStyle" { 95 | f.Set(reflect.ValueOf(decodeTextStyle(reflect.ValueOf(v).Interface().(map[string]interface{})))) 96 | } else if f.Type().String() == "fyne.Resource" { 97 | res := icons[reflect.ValueOf(v).String()] 98 | if res != nil { 99 | f.Set(reflect.ValueOf(res)) 100 | } 101 | } else { 102 | if strings.Index(f.Type().String(), "int") == 0 { 103 | f.SetInt(int64(reflect.ValueOf(v).Float())) 104 | } else { 105 | f.Set(reflect.ValueOf(v)) 106 | } 107 | } 108 | } 109 | 110 | return obj 111 | } 112 | 113 | // EncodeJSON writes a JSON stream for the tree of `CanvasObject` elements provided. 114 | // If an error occurs it will be returned, otherwise nil. 115 | func EncodeJSON(obj fyne.CanvasObject, w io.Writer) error { 116 | tree := encodeObj(obj) 117 | 118 | e := json.NewEncoder(w) 119 | e.SetIndent("", " ") 120 | return e.Encode(tree) 121 | } 122 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "fyne.io/fyne/v2" 8 | _ "fyne.io/fyne/v2/test" 9 | "fyne.io/fyne/v2/widget" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const labelJSON = `{ 15 | "Type": "*widget.Label", 16 | "Struct": { 17 | "Hidden": false, 18 | "Text": "Hi", 19 | "Alignment": 1, 20 | "Wrapping": 0, 21 | "TextStyle": { 22 | "Bold": true, 23 | "Italic": false, 24 | "Monospace": false, 25 | "TabWidth": 0 26 | } 27 | } 28 | } 29 | ` 30 | 31 | func TestDecodeJSON(t *testing.T) { 32 | initIcons() 33 | initWidgets() 34 | 35 | buf := bytes.NewReader([]byte(labelJSON)) 36 | obj := DecodeJSON(buf) 37 | 38 | l, ok := obj.(*widget.Label) 39 | require.True(t, ok) 40 | assert.Equal(t, "Hi", l.Text) 41 | assert.Equal(t, fyne.TextAlignCenter, l.Alignment) 42 | assert.Equal(t, fyne.TextStyle{Bold: true}, l.TextStyle) 43 | } 44 | 45 | func TestEncodeJSON(t *testing.T) { 46 | l := widget.NewLabelWithStyle("Hi", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) 47 | 48 | var buf bytes.Buffer 49 | EncodeJSON(l, &buf) 50 | assert.Equal(t, labelJSON, buf.String()) 51 | } 52 | -------------------------------------------------------------------------------- /layout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | 7 | "fyne.io/fyne/v2" 8 | "fyne.io/fyne/v2/layout" 9 | "fyne.io/fyne/v2/widget" 10 | ) 11 | 12 | type layoutInfo struct { 13 | create func(map[string]string) fyne.Layout 14 | edit func(*fyne.Container, map[string]string) []*widget.FormItem 15 | } 16 | 17 | var ( 18 | // layoutNames is an array with the list of names of all the layouts 19 | layoutNames = extractLayoutNames() 20 | layoutProps = make(map[*fyne.Container]map[string]string) 21 | 22 | layouts = map[string]layoutInfo{ 23 | "Center": { 24 | func(map[string]string) fyne.Layout { 25 | return layout.NewCenterLayout() 26 | }, 27 | nil, 28 | }, 29 | "Form": { 30 | func(map[string]string) fyne.Layout { 31 | return layout.NewFormLayout() 32 | }, 33 | nil, 34 | }, 35 | "Grid": { 36 | func(props map[string]string) fyne.Layout { 37 | rowCol := props["grid_type"] 38 | if rowCol == "" { 39 | rowCol = "Columns" 40 | } 41 | count := props["count"] 42 | if count == "" { 43 | count = "2" 44 | } 45 | 46 | num, err := strconv.ParseInt(count, 0, 0) 47 | if err != nil { 48 | num = 2 49 | } 50 | 51 | if rowCol == "Rows" { 52 | return layout.NewGridLayoutWithRows(int(num)) 53 | } 54 | return layout.NewGridLayoutWithColumns(int(num)) 55 | }, 56 | func(c *fyne.Container, props map[string]string) []*widget.FormItem { 57 | rowCol := props["grid_type"] 58 | if rowCol == "" { 59 | rowCol = "Columns" 60 | } 61 | count := props["count"] 62 | if count == "" { 63 | count = "2" 64 | } 65 | 66 | cols := widget.NewEntry() 67 | cols.SetText(count) 68 | vert := widget.NewSelect([]string{"Columns", "Rows"}, nil) 69 | vert.SetSelected(rowCol) 70 | change := func(string) { 71 | if cols.Text == "" { 72 | return 73 | } 74 | num, err := strconv.ParseInt(cols.Text, 0, 0) 75 | if err != nil { 76 | return 77 | } 78 | 79 | props["grid_type"] = vert.Selected 80 | props["count"] = cols.Text 81 | if vert.Selected == "Rows" { 82 | c.Layout = layout.NewGridLayoutWithRows(int(num)) 83 | } else { 84 | c.Layout = layout.NewGridLayoutWithColumns(int(num)) 85 | } 86 | c.Refresh() 87 | } 88 | cols.OnChanged = change 89 | vert.OnChanged = change 90 | return []*widget.FormItem{ 91 | widget.NewFormItem("Count", cols), 92 | widget.NewFormItem("Arrange in", vert), 93 | } 94 | }, 95 | }, 96 | "GridWrap": { 97 | func(props map[string]string) fyne.Layout { 98 | width := props["width"] 99 | if width == "" { 100 | width = "100" 101 | } 102 | height := props["height"] 103 | if height == "" { 104 | height = "100" 105 | } 106 | w, err := strconv.ParseInt(width, 0, 0) 107 | if err != nil { 108 | w = 100 109 | } 110 | h, err := strconv.ParseInt(height, 0, 0) 111 | if err != nil { 112 | h = 100 113 | } 114 | 115 | return layout.NewGridWrapLayout(fyne.NewSize(float32(w), float32(h))) 116 | }, 117 | func(c *fyne.Container, props map[string]string) []*widget.FormItem { 118 | width := props["width"] 119 | if width == "" { 120 | width = "100" 121 | } 122 | height := props["height"] 123 | if height == "" { 124 | height = "100" 125 | } 126 | 127 | widthEnt := widget.NewEntry() 128 | widthEnt.SetText(width) 129 | heightEnt := widget.NewEntry() 130 | heightEnt.SetText(height) 131 | change := func(string) { 132 | if widthEnt.Text == "" { 133 | return 134 | } 135 | w, err := strconv.ParseInt(widthEnt.Text, 0, 0) 136 | if err != nil { 137 | return 138 | } 139 | if widthEnt.Text == "" { 140 | return 141 | } 142 | h, err := strconv.ParseInt(heightEnt.Text, 0, 0) 143 | if err != nil { 144 | return 145 | } 146 | 147 | props["width"] = widthEnt.Text 148 | props["height"] = heightEnt.Text 149 | c.Layout = layout.NewGridWrapLayout(fyne.NewSize(float32(w), float32(h))) 150 | c.Refresh() 151 | } 152 | widthEnt.OnChanged = change 153 | heightEnt.OnChanged = change 154 | return []*widget.FormItem{ 155 | widget.NewFormItem("Item Width", widthEnt), 156 | widget.NewFormItem("Item Height", heightEnt), 157 | } 158 | }, 159 | }, 160 | "HBox": { 161 | func(props map[string]string) fyne.Layout { 162 | return layout.NewHBoxLayout() 163 | }, 164 | nil, 165 | }, 166 | "Max": { 167 | func(props map[string]string) fyne.Layout { 168 | return layout.NewMaxLayout() 169 | }, 170 | nil, 171 | }, 172 | "Padded": { 173 | func(props map[string]string) fyne.Layout { 174 | return layout.NewPaddedLayout() 175 | }, 176 | nil, 177 | }, 178 | "VBox": { 179 | func(props map[string]string) fyne.Layout { 180 | return layout.NewVBoxLayout() 181 | }, 182 | nil, 183 | }, 184 | } 185 | ) 186 | 187 | // extractLayoutNames returns all the list of names of all the layouts known 188 | func extractLayoutNames() []string { 189 | var layoutsNamesFromData = make([]string, len(layouts)) 190 | i := 0 191 | for k := range layouts { 192 | layoutsNamesFromData[i] = k 193 | i++ 194 | } 195 | 196 | sort.Strings(layoutsNamesFromData) 197 | return layoutsNamesFromData 198 | } 199 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/format" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | 14 | "fyne.io/fyne/v2" 15 | "fyne.io/fyne/v2/app" 16 | "fyne.io/fyne/v2/container" 17 | "fyne.io/fyne/v2/dialog" 18 | "fyne.io/fyne/v2/layout" 19 | "fyne.io/fyne/v2/storage" 20 | "fyne.io/fyne/v2/theme" 21 | "fyne.io/fyne/v2/widget" 22 | ) 23 | 24 | var ( 25 | editForm *widget.Form 26 | widType *widget.Label 27 | paletteList *fyne.Container 28 | ) 29 | 30 | func buildLibrary() fyne.CanvasObject { 31 | var selected *widgetInfo 32 | tempNames := []string{} 33 | widgetLowerNames := []string{} 34 | for _, name := range widgetNames { 35 | widgetLowerNames = append(widgetLowerNames, strings.ToLower(name)) 36 | tempNames = append(tempNames, name) 37 | } 38 | list := widget.NewList(func() int { 39 | return len(tempNames) 40 | }, func() fyne.CanvasObject { 41 | return widget.NewLabel("") 42 | }, func(i widget.ListItemID, obj fyne.CanvasObject) { 43 | obj.(*widget.Label).SetText(widgets[tempNames[i]].name) 44 | }) 45 | list.OnSelected = func(i widget.ListItemID) { 46 | if match, ok := widgets[tempNames[i]]; ok { 47 | selected = &match 48 | } 49 | } 50 | list.OnUnselected = func(widget.ListItemID) { 51 | selected = nil 52 | } 53 | 54 | searchBox := widget.NewEntry() 55 | searchBox.SetPlaceHolder("Search Widgets") 56 | searchBox.OnChanged = func(s string) { 57 | s = strings.ToLower(s) 58 | tempNames = []string{} 59 | for i := 0; i < len(widgetLowerNames); i++ { 60 | if strings.Contains(widgetLowerNames[i], s) { 61 | tempNames = append(tempNames, widgetNames[i]) 62 | } 63 | } 64 | list.Refresh() 65 | list.Select(0) // Needed for new selection 66 | list.Unselect(0) // Without this (and with the above), list is behaving in a weird way 67 | } 68 | 69 | return container.NewBorder(searchBox, widget.NewButtonWithIcon("Insert", theme.ContentAddIcon(), func() { 70 | if c, ok := current.(*overlayContainer); ok { 71 | if selected != nil { 72 | c.c.Objects = append(c.c.Objects, wrapContent(selected.create(), c.c)) 73 | c.c.Refresh() 74 | } 75 | return 76 | } 77 | log.Println("Please select a container") 78 | }), nil, nil, list) 79 | } 80 | 81 | func buildUI(win fyne.Window) fyne.CanvasObject { 82 | content := previewUI().(*fyne.Container) 83 | overlay := wrapContent(content, nil) 84 | wrap := container.NewMax(overlay) 85 | 86 | toolbar := widget.NewToolbar( 87 | widget.NewToolbarAction(theme.FolderOpenIcon(), func() { 88 | d := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) { 89 | if err != nil { 90 | dialog.ShowError(err, win) 91 | } 92 | if r == nil { 93 | return 94 | } 95 | 96 | obj := DecodeJSON(r) 97 | _ = r.Close() 98 | 99 | overlay = wrapContent(obj, nil) 100 | wrap.Objects[0] = overlay 101 | wrap.Refresh() 102 | }, win) 103 | d.SetFilter(storage.NewExtensionFileFilter([]string{".json"})) 104 | d.Show() 105 | }), 106 | widget.NewToolbarAction(theme.DocumentSaveIcon(), func() { 107 | d := dialog.NewFileSave(func(w fyne.URIWriteCloser, err error) { 108 | if err != nil { 109 | dialog.ShowError(err, win) 110 | } 111 | if w == nil { 112 | return 113 | } 114 | 115 | err = EncodeJSON(overlay, w) 116 | if err != nil { 117 | dialog.ShowError(err, win) 118 | } 119 | _ = w.Close() 120 | }, win) 121 | d.SetFilter(storage.NewExtensionFileFilter([]string{".json"})) 122 | d.SetFileName("main.ui.json") 123 | d.Show() 124 | }), 125 | widget.NewToolbarAction(theme.DownloadIcon(), func() { 126 | packagesList := packagesRequired(overlay) 127 | code := exportCode(packagesList, overlay) 128 | fmt.Println(code) 129 | }), 130 | widget.NewToolbarAction(theme.MailForwardIcon(), func() { 131 | packagesList := append(packagesRequired(overlay), "app") 132 | code := exportCode(packagesList, overlay) 133 | code += ` 134 | func main() { 135 | myApp := app.New() 136 | myWindow := myApp.NewWindow("Hello") 137 | myWindow.SetContent(makeUI()) 138 | myWindow.ShowAndRun() 139 | } 140 | ` 141 | path := filepath.Join(os.TempDir(), "fynebuilder") 142 | os.MkdirAll(path, 0711) 143 | path = filepath.Join(path, "main.go") 144 | _ = ioutil.WriteFile(path, []byte(code), 0600) 145 | 146 | cmd := exec.Command("go", "run", path) 147 | cmd.Stderr = os.Stderr 148 | cmd.Stdout = os.Stdout 149 | cmd.Start() 150 | })) 151 | 152 | widType = widget.NewLabelWithStyle("(None Selected)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) 153 | paletteList = container.NewVBox() 154 | palette := container.NewBorder(widType, nil, nil, nil, 155 | container.NewGridWithRows(2, widget.NewCard("Properties", "", paletteList), 156 | widget.NewCard("Component List", "", buildLibrary()), 157 | )) 158 | 159 | split := container.NewHSplit(wrap, palette) 160 | split.Offset = 0.8 161 | return container.New(layout.NewBorderLayout(toolbar, nil, nil, nil), toolbar, 162 | split) 163 | } 164 | 165 | func packagesRequired(obj fyne.CanvasObject) []string { 166 | if w, ok := obj.(*overlayWidget); ok { 167 | return w.Packages() 168 | } 169 | 170 | ret := []string{"container"} 171 | var objs []fyne.CanvasObject 172 | if c, ok := obj.(*fyne.Container); ok { 173 | objs = c.Objects 174 | } else if c, ok := obj.(*overlayContainer); ok { 175 | objs = c.c.Objects 176 | } 177 | for _, w := range objs { 178 | for _, p := range packagesRequired(w) { 179 | added := false 180 | for _, exists := range ret { 181 | if p == exists { 182 | added = true 183 | break 184 | } 185 | } 186 | if !added { 187 | ret = append(ret, p) 188 | } 189 | } 190 | } 191 | return ret 192 | } 193 | 194 | func choose(o fyne.CanvasObject) { 195 | typeName := reflect.TypeOf(o).Elem().Name() 196 | widName := reflect.TypeOf(o).String() 197 | l := reflect.ValueOf(o).Elem() 198 | if typeName == "Entry" { 199 | if l.FieldByName("Password").Bool() { 200 | typeName = "PasswordEntry" 201 | } else if l.FieldByName("MultiLine").Bool() { 202 | typeName = "MultiLineEntry" 203 | } 204 | widName = "*widget." + typeName 205 | } 206 | widType.SetText(typeName) 207 | 208 | var items []*widget.FormItem 209 | if match, ok := widgets[widName]; ok { 210 | items = match.edit(o) 211 | } 212 | 213 | editForm = widget.NewForm(items...) 214 | remove := widget.NewButton("Remove", func() { 215 | var parent *fyne.Container 216 | var obj fyne.CanvasObject 217 | if c, ok := current.(*overlayContainer); ok { 218 | parent = c.parent 219 | obj = c 220 | } else if w, ok := current.(*overlayWidget); ok { 221 | parent = w.parent 222 | for _, o := range parent.Objects { // match our widget in the container wrapping us 223 | if c, ok := o.(*fyne.Container); ok && c.Objects[0] == w.child { 224 | obj = c 225 | break 226 | } 227 | } 228 | } 229 | if parent == nil { 230 | log.Println("Nothing to remove") 231 | return 232 | } 233 | 234 | parent.Remove(obj) 235 | parent.Refresh() 236 | }) 237 | paletteList.Objects = []fyne.CanvasObject{editForm, remove} 238 | paletteList.Refresh() 239 | } 240 | 241 | func exportCode(pkgs []string, obj fyne.CanvasObject) string { 242 | for i := 0; i < len(pkgs); i++ { 243 | pkgs[i] = fmt.Sprintf(` "fyne.io/fyne/v2/%s"`, pkgs[i]) 244 | } 245 | code := fmt.Sprintf(` 246 | package main 247 | 248 | import ( 249 | "fyne.io/fyne/v2" 250 | %s 251 | ) 252 | 253 | func makeUI() fyne.CanvasObject { 254 | return %#v 255 | } 256 | `, 257 | strings.Join(pkgs, "\n"), 258 | obj) 259 | 260 | formatted, err := format.Source([]byte(code)) 261 | if err != nil { 262 | log.Fatal(err) 263 | } 264 | return string(formatted) 265 | } 266 | 267 | func main() { 268 | a := app.NewWithID("xyz.andy.fynebuilder") 269 | initIcons() 270 | initWidgets() 271 | 272 | w := a.NewWindow("Fyne Builder") 273 | w.SetContent(buildUI(w)) 274 | w.Resize(fyne.NewSize(600, 400)) 275 | w.ShowAndRun() 276 | } 277 | 278 | func previewUI() fyne.CanvasObject { 279 | return container.New(layout.NewVBoxLayout(), 280 | widget.NewIcon(theme.ContentAddIcon()), 281 | widget.NewLabel("label"), 282 | widget.NewButton("Button", func() {})) 283 | } 284 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "strings" 4 | 5 | func encodeDoubleQuote(inStr string) (outStr string) { 6 | outStr = strings.ReplaceAll(inStr, "\"", "\\\"") 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /widget.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | "fyne.io/fyne/v2" 11 | "fyne.io/fyne/v2/container" 12 | "fyne.io/fyne/v2/layout" 13 | "fyne.io/fyne/v2/theme" 14 | "fyne.io/fyne/v2/widget" 15 | ) 16 | 17 | var ( 18 | // widgetNames is an array with the list of names of all the widgets 19 | widgetNames []string 20 | 21 | widgets map[string]widgetInfo 22 | ) 23 | 24 | type widgetInfo struct { 25 | name string 26 | create func() fyne.CanvasObject 27 | edit func(fyne.CanvasObject) []*widget.FormItem 28 | gostring func(fyne.CanvasObject) string 29 | packages func(object fyne.CanvasObject) []string 30 | } 31 | 32 | func initWidgets() { 33 | widgets = map[string]widgetInfo{ 34 | "*widget.Button": { 35 | name: "Button", 36 | create: func() fyne.CanvasObject { 37 | return widget.NewButton("Button", func() {}) 38 | }, 39 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 40 | b := obj.(*widget.Button) 41 | entry := widget.NewEntry() 42 | entry.SetText(b.Text) 43 | entry.OnChanged = func(text string) { 44 | b.SetText(text) 45 | } 46 | return []*widget.FormItem{ 47 | widget.NewFormItem("Text", entry), 48 | widget.NewFormItem("Icon", widget.NewSelect(iconNames, func(selected string) { 49 | b.SetIcon(wrapResource(icons[selected])) 50 | }))} 51 | }, 52 | gostring: func(obj fyne.CanvasObject) string { 53 | b := obj.(*widget.Button) 54 | if b.Icon == nil { 55 | return fmt.Sprintf("widget.NewButton(\"%s\", func() {})", encodeDoubleQuote(b.Text)) 56 | } 57 | 58 | icon := "theme." + iconReverse[fmt.Sprintf("%p", b.Icon)] + "()" 59 | return fmt.Sprintf("widget.NewButtonWithIcon(\"%s\", %s, func() {})", encodeDoubleQuote(b.Text), icon) 60 | }, 61 | packages: func(obj fyne.CanvasObject) []string { 62 | b := obj.(*widget.Button) 63 | if b.Icon == nil { 64 | return []string{"widget"} 65 | } 66 | 67 | return []string{"widget", "theme"} 68 | }, 69 | }, 70 | "*widget.Hyperlink": { 71 | name: "Hyperlink", 72 | create: func() fyne.CanvasObject { 73 | fyneURL, _ := url.Parse("https://fyne.io") 74 | return widget.NewHyperlink("Link Text", fyneURL) 75 | }, 76 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 77 | link := obj.(*widget.Hyperlink) 78 | title := widget.NewEntry() 79 | title.SetText(link.Text) 80 | title.OnChanged = func(text string) { 81 | link.SetText(text) 82 | } 83 | subtitle := widget.NewEntry() 84 | subtitle.SetText(link.URL.String()) 85 | subtitle.OnChanged = func(text string) { 86 | fyneURL, _ := url.Parse(text) 87 | link.SetURL(fyneURL) 88 | } 89 | return []*widget.FormItem{ 90 | widget.NewFormItem("Text", title), 91 | widget.NewFormItem("URL", subtitle)} 92 | }, 93 | gostring: func(obj fyne.CanvasObject) string { 94 | link := obj.(*widget.Hyperlink) 95 | return fmt.Sprintf("widget.NewHyperLink(\"%s\", \"%s\")", encodeDoubleQuote(link.Text), encodeDoubleQuote(link.URL.String())) 96 | }, 97 | }, 98 | "*widget.Card": { 99 | name: "Card", 100 | create: func() fyne.CanvasObject { 101 | return widget.NewCard("Title", "Subtitle", widget.NewLabel("Content here")) 102 | }, 103 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 104 | c := obj.(*widget.Card) 105 | title := widget.NewEntry() 106 | title.SetText(c.Title) 107 | title.OnChanged = func(text string) { 108 | c.SetTitle(text) 109 | } 110 | subtitle := widget.NewEntry() 111 | subtitle.SetText(c.Subtitle) 112 | subtitle.OnChanged = func(text string) { 113 | c.SetSubTitle(text) 114 | } 115 | return []*widget.FormItem{ 116 | widget.NewFormItem("Title", title), 117 | widget.NewFormItem("Subtitle", subtitle)} 118 | }, 119 | gostring: func(obj fyne.CanvasObject) string { 120 | c := obj.(*widget.Card) 121 | return fmt.Sprintf("widget.NewCard(\"%s\", \"%s\", widget.NewLabel(\"Content here\")", 122 | encodeDoubleQuote(c.Title), encodeDoubleQuote(c.Subtitle)) 123 | }, 124 | }, 125 | "*widget.Entry": { 126 | name: "Entry", 127 | create: func() fyne.CanvasObject { 128 | e := widget.NewEntry() 129 | e.SetPlaceHolder("Entry") 130 | return e 131 | }, 132 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 133 | l := obj.(*widget.Entry) 134 | entry1 := widget.NewEntry() 135 | entry1.SetText(l.Text) 136 | entry1.OnChanged = func(text string) { 137 | l.SetText(text) 138 | } 139 | entry2 := widget.NewEntry() 140 | entry2.SetText(l.PlaceHolder) 141 | entry2.OnChanged = func(text string) { 142 | l.SetPlaceHolder(text) 143 | } 144 | return []*widget.FormItem{ 145 | widget.NewFormItem("Text", entry1), 146 | widget.NewFormItem("PlaceHolder", entry2)} 147 | }, 148 | gostring: func(obj fyne.CanvasObject) string { 149 | l := obj.(*widget.Entry) 150 | return fmt.Sprintf("&widget.Entry{Text: \"%s\", PlaceHolder: \"%s\"}", encodeDoubleQuote(l.Text), encodeDoubleQuote(l.PlaceHolder)) 151 | }, 152 | }, 153 | "*widget.Icon": { 154 | name: "Icon", 155 | create: func() fyne.CanvasObject { 156 | return widget.NewIcon(theme.HelpIcon()) 157 | }, 158 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 159 | i := obj.(*widget.Icon) 160 | return []*widget.FormItem{ 161 | widget.NewFormItem("Icon", widget.NewSelect(iconNames, func(selected string) { 162 | i.SetResource(wrapResource(icons[selected])) 163 | }))} 164 | }, 165 | gostring: func(obj fyne.CanvasObject) string { 166 | i := obj.(*widget.Icon) 167 | 168 | res := "theme." + iconReverse[fmt.Sprintf("%p", i.Resource)] + "()" 169 | return fmt.Sprintf("widget.NewIcon(%s)", res) 170 | }, 171 | packages: func(obj fyne.CanvasObject) []string { 172 | return []string{"widget", "theme"} 173 | }, 174 | }, 175 | "*widget.Label": { 176 | name: "Label", 177 | create: func() fyne.CanvasObject { 178 | return widget.NewLabel("Label") 179 | }, 180 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 181 | l := obj.(*widget.Label) 182 | entry := widget.NewEntry() 183 | entry.SetText(l.Text) 184 | entry.OnChanged = func(text string) { 185 | l.SetText(text) 186 | } 187 | return []*widget.FormItem{ 188 | widget.NewFormItem("Text", entry)} 189 | }, 190 | gostring: func(obj fyne.CanvasObject) string { 191 | l := obj.(*widget.Label) 192 | return fmt.Sprintf("widget.NewLabel(\"%s\")", encodeDoubleQuote(l.Text)) 193 | }, 194 | }, 195 | "*widget.Check": { 196 | name: "CheckBox", 197 | create: func() fyne.CanvasObject { 198 | return widget.NewCheck("Tick it or don't", func(b bool) {}) 199 | }, 200 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 201 | c := obj.(*widget.Check) 202 | title := widget.NewEntry() 203 | title.SetText(c.Text) 204 | title.OnChanged = func(text string) { 205 | c.Text = text 206 | c.Refresh() 207 | } 208 | isChecked := widget.NewCheck("", func(b bool) { c.SetChecked(b) }) 209 | isChecked.SetChecked(c.Checked) 210 | return []*widget.FormItem{ 211 | widget.NewFormItem("Title", title), 212 | widget.NewFormItem("isChecked", isChecked)} 213 | }, 214 | gostring: func(obj fyne.CanvasObject) string { 215 | c := obj.(*widget.Check) 216 | return fmt.Sprintf("widget.NewCheck(\"%s\", func(b bool) {}", encodeDoubleQuote(c.Text)) 217 | }, 218 | }, 219 | "*widget.RadioGroup": { 220 | name: "RadioGroup", 221 | create: func() fyne.CanvasObject { 222 | return widget.NewRadioGroup([]string{"Option 1", "Option 2"}, func(s string) {}) 223 | }, 224 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 225 | r := obj.(*widget.RadioGroup) 226 | initialOption := widget.NewRadioGroup(r.Options, func(s string) { r.SetSelected(s) }) 227 | initialOption.SetSelected(r.Selected) 228 | entry := widget.NewMultiLineEntry() 229 | entry.SetText(strings.Join(r.Options, "\n")) 230 | entry.OnChanged = func(text string) { 231 | r.Options = strings.Split(text, "\n") 232 | r.Refresh() 233 | initialOption.Options = strings.Split(text, "\n") 234 | initialOption.Refresh() 235 | } 236 | return []*widget.FormItem{ 237 | widget.NewFormItem("Options", entry), 238 | widget.NewFormItem("Initial Option", initialOption)} 239 | }, 240 | gostring: func(obj fyne.CanvasObject) string { 241 | r := obj.(*widget.RadioGroup) 242 | var opts []string 243 | for _, v := range r.Options { 244 | opts = append(opts, encodeDoubleQuote(v)) 245 | } 246 | return fmt.Sprintf("widget.NewRadioGroup([]string{%s}, func(s string) {})", "\""+strings.Join(opts, "\", \"")+"\"") 247 | }, 248 | }, 249 | "*widget.Select": { 250 | name: "Select", 251 | create: func() fyne.CanvasObject { 252 | return widget.NewSelect([]string{"Option 1", "Option 2"}, func(value string) {}) 253 | }, 254 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 255 | s := obj.(*widget.Select) 256 | initialOption := widget.NewSelect(append([]string{"(Select one)"}, s.Options...), func(opt string) { 257 | s.SetSelected(opt) 258 | if opt == "(Select one)" { 259 | s.ClearSelected() 260 | } 261 | }) 262 | initialOption.SetSelected(s.Selected) 263 | entry := widget.NewMultiLineEntry() 264 | entry.SetText(strings.Join(s.Options, "\n")) 265 | entry.OnChanged = func(text string) { 266 | s.Options = strings.Split(text, "\n") 267 | s.Refresh() 268 | initialOption.Options = strings.Split(text, "\n") 269 | initialOption.Refresh() 270 | } 271 | return []*widget.FormItem{ 272 | widget.NewFormItem("Options", entry), 273 | widget.NewFormItem("Initial Option", initialOption)} 274 | }, 275 | gostring: func(obj fyne.CanvasObject) string { 276 | s := obj.(*widget.Select) 277 | var opts []string 278 | for _, v := range s.Options { 279 | opts = append(opts, encodeDoubleQuote(v)) 280 | } 281 | return fmt.Sprintf("widget.NewSelect([]string{%s}, func(s string) {})", "\""+strings.Join(opts, "\", \"")+"\"") 282 | }, 283 | }, 284 | "*widget.Accordion": { 285 | name: "Accordion", 286 | create: func() fyne.CanvasObject { 287 | return widget.NewAccordion(widget.NewAccordionItem("Item 1", widget.NewLabel("The content goes here")), widget.NewAccordionItem("Item 2", widget.NewLabel("Content part 2 goes here"))) 288 | }, 289 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 290 | // TODO: Need to add the properties 291 | // entry := widget.NewEntry() 292 | return []*widget.FormItem{} 293 | }, 294 | gostring: func(obj fyne.CanvasObject) string { 295 | return "widget.NewAccordion(\"widget.NewAccordionItem(\"Item 1\", widget.NewLabel(\"The content goes here\")), widget.NewAccordionItem(\"Item 2\", widget.NewLabel(\"Content part 2 goes here\")))" 296 | }, 297 | }, 298 | "*widget.List": { 299 | name: "List", 300 | create: func() fyne.CanvasObject { 301 | myList := []string{"Item 1", "Item 2", "Item 3", "Item 4"} 302 | // TODO: Need to make the list get adjusted to show the full list of items, currently it has only one item height apprx. 303 | return widget.NewList(func() int { return len(myList) }, func() fyne.CanvasObject { 304 | return container.New(layout.NewHBoxLayout(), widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object")) 305 | }, func(id widget.ListItemID, item fyne.CanvasObject) { 306 | item.(*fyne.Container).Objects[1].(*widget.Label).SetText(myList[id]) 307 | }) 308 | }, 309 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 310 | return []*widget.FormItem{} 311 | }, 312 | gostring: func(obj fyne.CanvasObject) string { 313 | return `widget.NewList(func() int { return len(myList) }, func() fyne.CanvasObject { 314 | return container.New(layout.NewHBoxLayout(), widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object")) 315 | }, func(id widget.ListItemID, item fyne.CanvasObject) { 316 | item.(*fyne.Container).Objects[1].(*widget.Label).SetText(myList[id]) 317 | })` 318 | }, 319 | packages: func(obj fyne.CanvasObject) []string { 320 | return []string{"widget", "container"} 321 | }, 322 | }, 323 | "*widget.Menu": { 324 | name: "Menu", 325 | create: func() fyne.CanvasObject { 326 | myMenu := fyne.NewMenu("Menu Name", fyne.NewMenuItem("Item 1", func() { fmt.Println("From Item 1") }), fyne.NewMenuItem("Item 2", func() { fmt.Println("From Item 2") }), fyne.NewMenuItem("Item 3", func() { fmt.Println("From Item 3") })) 327 | return widget.NewMenu(myMenu) 328 | }, 329 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 330 | return []*widget.FormItem{} 331 | }, 332 | gostring: func(obj fyne.CanvasObject) string { 333 | return "widget.NewMenu(fyne.NewMenu(\"Menu Name\", fyne.NewMenuItem(\"Item 1\", func() {}), fyne.NewMenuItem(\"Item 2\", func() {}), fyne.NewMenuItem(\"Item 3\", func() {})))" 334 | }, 335 | }, 336 | "*widget.Form": { 337 | name: "Form", 338 | create: func() fyne.CanvasObject { 339 | return widget.NewForm(widget.NewFormItem("Username", widget.NewEntry()), widget.NewFormItem("Password", widget.NewPasswordEntry()), widget.NewFormItem("", container.NewGridWithColumns(2, widget.NewButton("Submit", func() { fmt.Println("Form is submitted") }), widget.NewButton("Cancel", func() { fmt.Println("Form is Cancelled") })))) 340 | }, 341 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 342 | return []*widget.FormItem{} 343 | }, 344 | gostring: func(obj fyne.CanvasObject) string { 345 | return "widget.NewForm(widget.NewFormItem(\"Username\", widget.NewEntry()), widget.NewFormItem(\"Password\", widget.NewPasswordEntry()), widget.NewFormItem(\"\", container.NewGridWithColumns(2, widget.NewButton(\"Submit\", func() {}), widget.NewButton(\"Cancel\", func() {}))))" 346 | }, 347 | }, 348 | "*widget.MultiLineEntry": { 349 | name: "Multi Line Entry", 350 | create: func() fyne.CanvasObject { 351 | mle := widget.NewMultiLineEntry() 352 | mle.SetPlaceHolder("Enter Some \nLong text \nHere") 353 | mle.Wrapping = fyne.TextWrapWord 354 | return mle 355 | }, 356 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 357 | mle := obj.(*widget.Entry) 358 | placeholder := widget.NewMultiLineEntry() 359 | placeholder.Wrapping = fyne.TextWrapWord 360 | placeholder.SetText(mle.PlaceHolder) 361 | placeholder.OnChanged = func(s string) { 362 | mle.SetPlaceHolder(s) 363 | } 364 | value := widget.NewMultiLineEntry() 365 | value.Wrapping = fyne.TextWrapWord 366 | value.SetText(mle.Text) 367 | value.OnChanged = func(s string) { 368 | mle.SetText(s) 369 | } 370 | return []*widget.FormItem{ 371 | widget.NewFormItem("Placeholder", placeholder), 372 | widget.NewFormItem("Value", value)} 373 | }, 374 | gostring: func(obj fyne.CanvasObject) string { 375 | mle := obj.(*widget.Entry) 376 | return fmt.Sprintf("&widget.MultiLineEntry{Text: \"%s\", PlaceHolder: \"%s\"}", encodeDoubleQuote(mle.Text), encodeDoubleQuote(mle.PlaceHolder)) 377 | }, 378 | }, 379 | "*widget.PasswordEntry": { 380 | name: "Password Entry", 381 | create: func() fyne.CanvasObject { 382 | e := widget.NewPasswordEntry() 383 | e.SetPlaceHolder("Password Entry") 384 | return e 385 | }, 386 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 387 | l := obj.(*widget.Entry) 388 | text := widget.NewPasswordEntry() 389 | text.SetText(l.Text) 390 | text.OnChanged = func(text string) { 391 | l.SetText(text) 392 | } 393 | placeholder := widget.NewEntry() 394 | placeholder.SetText(l.PlaceHolder) 395 | placeholder.OnChanged = func(text string) { 396 | l.SetPlaceHolder(text) 397 | } 398 | // hidePassword := widget.NewCheck("Hide Password", func(b bool) {}) 399 | // hidePassword.SetChecked(l.Hidden) 400 | // hidePassword.OnChanged = func(b bool) { 401 | // l.Hidden = b 402 | // } 403 | return []*widget.FormItem{ 404 | widget.NewFormItem("Text", text), 405 | // widget.NewFormItem("Hide password", placeholder), 406 | widget.NewFormItem("PlaceHolder", placeholder)} 407 | }, 408 | gostring: func(obj fyne.CanvasObject) string { 409 | l := obj.(*widget.Entry) 410 | return fmt.Sprintf("&widget.MultiLineEntry{Text: \"%s\", PlaceHolder: \"%s\"}", encodeDoubleQuote(l.Text), encodeDoubleQuote(l.PlaceHolder)) 411 | }, 412 | }, 413 | "*widget.ProgressBar": { 414 | name: "Progress Bar", 415 | create: func() fyne.CanvasObject { 416 | p := widget.NewProgressBar() 417 | p.SetValue(0.1) 418 | return p 419 | }, 420 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 421 | p := obj.(*widget.ProgressBar) 422 | value := widget.NewEntry() 423 | value.SetText(fmt.Sprintf("%f", p.Value)) 424 | value.OnChanged = func(s string) { 425 | if f, err := strconv.ParseFloat(s, 64); err == nil { 426 | p.SetValue(f) 427 | } 428 | } 429 | return []*widget.FormItem{ 430 | widget.NewFormItem("Value", value)} 431 | }, 432 | gostring: func(obj fyne.CanvasObject) string { 433 | p := obj.(*widget.ProgressBar) 434 | return fmt.Sprintf("&widget.ProgressBar{Value: %f}", p.Value) 435 | }, 436 | }, 437 | "*widget.Separator": { 438 | // Separator's height(or width as you may call) and color come from the theme, so not sure if we can change the color and height here 439 | name: "Separator", 440 | create: func() fyne.CanvasObject { 441 | return widget.NewSeparator() 442 | }, 443 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 444 | return []*widget.FormItem{} 445 | }, 446 | gostring: func(obj fyne.CanvasObject) string { 447 | return "widget.NewSeparator()" 448 | }, 449 | }, 450 | "*widget.Slider": { 451 | name: "Slider", 452 | create: func() fyne.CanvasObject { 453 | s := widget.NewSlider(0, 100) 454 | s.OnChanged = func(f float64) { 455 | fmt.Println("Slider changed to", f) 456 | } 457 | return s 458 | }, 459 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 460 | slider := obj.(*widget.Slider) 461 | val := widget.NewEntry() 462 | val.SetText(fmt.Sprintf("%f", slider.Value)) 463 | val.OnChanged = func(s string) { 464 | if f, err := strconv.ParseFloat(s, 64); err == nil { 465 | slider.SetValue(f) 466 | } 467 | } 468 | return []*widget.FormItem{ 469 | widget.NewFormItem("Value", val)} 470 | }, 471 | gostring: func(obj fyne.CanvasObject) string { 472 | slider := obj.(*widget.Slider) 473 | return fmt.Sprintf("widget.NewSlider(Min:0, Max:100, Value:%f)", slider.Value) 474 | }, 475 | }, 476 | "*widget.Table": { 477 | name: "Table", 478 | create: func() fyne.CanvasObject { 479 | return widget.NewTable(func() (int, int) { return 3, 3 }, func() fyne.CanvasObject { 480 | return widget.NewLabel("Cell 000, 000") 481 | }, func(id widget.TableCellID, cell fyne.CanvasObject) { 482 | label := cell.(*widget.Label) 483 | switch id.Col { 484 | case 0: 485 | label.SetText(fmt.Sprintf("%d", id.Row+1)) 486 | case 1: 487 | label.SetText("A longer cell") 488 | default: 489 | label.SetText(fmt.Sprintf("Cell %d, %d", id.Row+1, id.Col+1)) 490 | } 491 | }) 492 | }, 493 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 494 | return []*widget.FormItem{} 495 | }, 496 | gostring: func(obj fyne.CanvasObject) string { 497 | return `widget.NewTable(func() (int, int) { return 3, 3 }, func() fyne.CanvasObject { 498 | return widget.NewLabel("Cell 000, 000") 499 | }, func(id widget.TableCellID, cell fyne.CanvasObject) { 500 | label := cell.(*widget.Label) 501 | switch id.Col { 502 | case 0: 503 | label.SetText(fmt.Sprintf("%d", id.Row+1)) 504 | case 1: 505 | label.SetText("A longer cell") 506 | default: 507 | label.SetText(fmt.Sprintf("Cell %d, %d", id.Row+1, id.Col+1)) 508 | } 509 | })` 510 | }, 511 | }, 512 | "*widget.TextGrid": { 513 | name: "Text Grid", 514 | create: func() fyne.CanvasObject { 515 | to := widget.NewTextGrid() 516 | to.SetText("ABCD \nEFGH") 517 | return to 518 | }, 519 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 520 | to := obj.(*widget.TextGrid) 521 | entry := widget.NewEntry() 522 | entry.SetText(to.Text()) 523 | entry.OnChanged = func(s string) { 524 | to.SetText(s) 525 | } 526 | return []*widget.FormItem{ 527 | widget.NewFormItem("Text", entry)} 528 | }, 529 | gostring: func(obj fyne.CanvasObject) string { 530 | to := obj.(*widget.TextGrid) 531 | return fmt.Sprintf("widget.NewTextGrid(\"%s\")", encodeDoubleQuote(to.Text())) 532 | }, 533 | }, 534 | "*widget.Toolbar": { 535 | name: "Toolbar", 536 | create: func() fyne.CanvasObject { 537 | return widget.NewToolbar( 538 | widget.NewToolbarAction(icons["FileIcon"], func() { fmt.Println("Clicked on FileIcon") }), 539 | widget.NewToolbarSeparator(), 540 | widget.NewToolbarAction(icons["HomeIcon"], func() { fmt.Println("Clicked on HomeIcon") }), 541 | widget.NewToolbarSeparator(), 542 | widget.NewToolbarAction(icons["DownloadIcon"], func() { fmt.Println("Clicked on DownloadIcon") }), 543 | widget.NewToolbarSeparator(), 544 | widget.NewToolbarAction(icons["ViewRefreshIcon"], func() { fmt.Println("Clicked on ViewRefreshIcon") }), 545 | widget.NewToolbarAction(icons["NavigateBackIcon"], func() { fmt.Println("Clicked on NavigateBackIcon") }), 546 | widget.NewToolbarAction(icons["NavigateNextIcon"], func() { fmt.Println("Clicked on NavigateNextIcon") }), 547 | widget.NewToolbarAction(icons["MailSendIcon"], func() { fmt.Println("Clicked on MailSendIcon") }), 548 | widget.NewToolbarSpacer(), 549 | widget.NewToolbarAction(icons["HelpIcon"], func() { fmt.Println("Clicked on HelpIcon") }), 550 | ) 551 | }, 552 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 553 | return []*widget.FormItem{} 554 | }, 555 | gostring: func(obj fyne.CanvasObject) string { 556 | return `widget.NewToolbar( 557 | widget.NewToolbarAction(theme.FileIcon(), func() {}), 558 | widget.NewToolbarSeparator(), 559 | widget.NewToolbarAction(theme.HomeIcon(), func() {}), 560 | widget.NewToolbarSeparator(), 561 | widget.NewToolbarAction(theme.DownloadIcon(), func() {}), 562 | widget.NewToolbarSeparator(), 563 | widget.NewToolbarAction(theme.ViewRefreshIcon(), func() {}), 564 | widget.NewToolbarAction(theme.NavigateBackIcon(), func() {}), 565 | widget.NewToolbarAction(theme.NavigateNextIcon(), func() {}), 566 | widget.NewToolbarAction(theme.MailSendIcon(), func() {}), 567 | widget.NewToolbarSpacer(), 568 | widget.NewToolbarAction(theme.HelpIcon(), func() {}), 569 | )` 570 | }, 571 | }, 572 | "*widget.Tree": { 573 | name: "Tree", 574 | create: func() fyne.CanvasObject { 575 | data := map[string][]string{ 576 | "": {"A"}, 577 | "A": {"B", "D", "H", "J", "L", "O", "P", "S", "V"}, 578 | "B": {"C"}, 579 | "C": {"abc"}, 580 | "D": {"E"}, 581 | "E": {"F", "G"}, 582 | "F": {"adef"}, 583 | "G": {"adeg"}, 584 | "H": {"I"}, 585 | "I": {"ahi"}, 586 | "O": {"ao"}, 587 | "P": {"Q"}, 588 | "Q": {"R"}, 589 | "R": {"apqr"}, 590 | "S": {"T"}, 591 | "T": {"U"}, 592 | "U": {"astu"}, 593 | "V": {"W"}, 594 | "W": {"X"}, 595 | "X": {"Y"}, 596 | "Y": {"Z"}, 597 | "Z": {"avwxyz"}, 598 | } 599 | 600 | tree := widget.NewTreeWithStrings(data) 601 | tree.OnSelected = func(id string) { 602 | fmt.Println("Tree node selected:", id) 603 | } 604 | tree.OnUnselected = func(id string) { 605 | fmt.Println("Tree node unselected:", id) 606 | } 607 | tree.OpenBranch("A") 608 | tree.OpenBranch("D") 609 | tree.OpenBranch("E") 610 | tree.OpenBranch("L") 611 | tree.OpenBranch("M") 612 | return tree 613 | }, 614 | edit: func(co fyne.CanvasObject) []*widget.FormItem { 615 | return []*widget.FormItem{} 616 | }, 617 | }, 618 | 619 | "*fyne.Container": { 620 | name: "Container", 621 | create: func() fyne.CanvasObject { 622 | return container.NewMax() 623 | }, 624 | edit: func(obj fyne.CanvasObject) []*widget.FormItem { 625 | c := obj.(*fyne.Container) 626 | props := layoutProps[c] 627 | 628 | var items []*widget.FormItem 629 | var choose *widget.FormItem 630 | // TODO figure out how to work Border... 631 | choose = widget.NewFormItem("Layout", widget.NewSelect(layoutNames, func(l string) { 632 | lay := layouts[l] 633 | props["layout"] = l 634 | c.Layout = lay.create(props) 635 | c.Refresh() 636 | choose.Widget.Hide() 637 | 638 | edit := lay.edit 639 | items = []*widget.FormItem{choose} 640 | if edit != nil { 641 | items = append(items, edit(c, props)...) 642 | } 643 | 644 | editForm = widget.NewForm(items...) 645 | paletteList.Objects = []fyne.CanvasObject{editForm} 646 | choose.Widget.Show() 647 | paletteList.Refresh() 648 | })) 649 | choose.Widget.(*widget.Select).SetSelected(props["layout"]) 650 | return items 651 | }, 652 | gostring: func(obj fyne.CanvasObject) string { 653 | c := obj.(*fyne.Container) 654 | l := layoutProps[c]["layout"] 655 | str := strings.Builder{} 656 | str.WriteString(fmt.Sprintf("container.New%s(", l)) 657 | for i, o := range c.Objects { 658 | if _, ok := o.(*overlayContainer); !ok { 659 | o = o.(*fyne.Container).Objects[1] 660 | } 661 | str.WriteString(fmt.Sprintf("\n\t\t%#v", o)) 662 | if i < len(c.Objects)-1 { 663 | str.WriteRune(',') 664 | } 665 | } 666 | str.WriteString(")\n") 667 | return str.String() 668 | }, 669 | }, 670 | } 671 | 672 | widgetNames = extractWidgetNames() 673 | } 674 | 675 | // extractWidgetNames returns all the list of names of all the widgets from our data 676 | func extractWidgetNames() []string { 677 | var widgetNamesFromData = make([]string, len(widgets)) 678 | i := 0 679 | for k := range widgets { 680 | widgetNamesFromData[i] = k 681 | i++ 682 | } 683 | 684 | sort.Strings(widgetNamesFromData) 685 | return widgetNamesFromData 686 | } 687 | -------------------------------------------------------------------------------- /wrap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "reflect" 7 | 8 | "fyne.io/fyne/v2" 9 | "fyne.io/fyne/v2/canvas" 10 | "fyne.io/fyne/v2/container" 11 | "fyne.io/fyne/v2/theme" 12 | "fyne.io/fyne/v2/widget" 13 | ) 14 | 15 | var current fyne.CanvasObject 16 | 17 | type jsonResource struct { 18 | fyne.Resource `json:"-"` 19 | } 20 | 21 | func (r *jsonResource) MarshalJSON() ([]byte, error) { 22 | icon := "\"" + iconReverse[fmt.Sprintf("%p", r.Resource)] + "\"" 23 | return []byte(icon), nil 24 | } 25 | 26 | type overlayContainer struct { 27 | widget.BaseWidget 28 | c, parent *fyne.Container 29 | } 30 | 31 | func (o *overlayContainer) CreateRenderer() fyne.WidgetRenderer { 32 | border := canvas.NewRectangle(color.Transparent) 33 | border.StrokeWidth = 4 34 | return &overRender{p: o, c: o.c, r: border} 35 | } 36 | 37 | func (o *overlayContainer) GoString() string { 38 | return widgets["*fyne.Container"].gostring(o.c) 39 | } 40 | 41 | func (o *overlayContainer) MinSize() fyne.Size { 42 | min := o.c.MinSize() 43 | if min.IsZero() { 44 | return fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()) 45 | } 46 | 47 | return min 48 | } 49 | 50 | func (o *overlayContainer) Move(p fyne.Position) { 51 | o.c.Move(p) 52 | o.BaseWidget.Move(p) 53 | } 54 | 55 | func (o *overlayContainer) Refresh() { 56 | o.BaseWidget.Refresh() 57 | o.c.Refresh() 58 | } 59 | 60 | func (o *overlayContainer) Resize(s fyne.Size) { 61 | o.c.Resize(s) 62 | o.BaseWidget.Resize(s) 63 | } 64 | 65 | func (o *overlayContainer) Tapped(e *fyne.PointEvent) { 66 | setCurrent(o) 67 | choose(o.c) 68 | } 69 | 70 | func (o *overlayContainer) Object() fyne.CanvasObject { 71 | return o.c 72 | } 73 | 74 | type overlayWidget struct { 75 | widget.BaseWidget 76 | child fyne.Widget 77 | parent *fyne.Container 78 | } 79 | 80 | func (w *overlayWidget) CreateRenderer() fyne.WidgetRenderer { 81 | border := canvas.NewRectangle(color.Transparent) 82 | border.StrokeWidth = 4 83 | 84 | return &overRender{p: w, r: border} 85 | } 86 | 87 | func (w *overlayWidget) GoString() string { 88 | name := reflect.TypeOf(w.child).String() 89 | if widgets[name].gostring != nil { 90 | return widgets[name].gostring(w.child) 91 | } 92 | 93 | return fmt.Sprintf("%#v", w.child) 94 | } 95 | 96 | func (w *overlayWidget) Object() fyne.CanvasObject { 97 | return w.child 98 | } 99 | 100 | func (w *overlayWidget) Packages() []string { 101 | name := reflect.TypeOf(w.child).String() 102 | if widgets[name].packages != nil { 103 | return widgets[name].packages(w.child) 104 | } 105 | 106 | return []string{"widget"} 107 | } 108 | 109 | func (w *overlayWidget) Refresh() { 110 | w.BaseWidget.Refresh() 111 | w.child.Refresh() 112 | } 113 | 114 | func (w *overlayWidget) Tapped(e *fyne.PointEvent) { 115 | setCurrent(w) 116 | choose(w.child) 117 | } 118 | 119 | type overRender struct { 120 | p fyne.CanvasObject 121 | c *fyne.Container 122 | r *canvas.Rectangle 123 | } 124 | 125 | func (o overRender) BackgroundColor() color.Color { 126 | return color.Transparent 127 | } 128 | 129 | func (o overRender) Destroy() { 130 | } 131 | 132 | func (o overRender) Layout(s fyne.Size) { 133 | o.r.Resize(s) 134 | } 135 | 136 | func (o overRender) MinSize() fyne.Size { 137 | return fyne.Size{} 138 | } 139 | 140 | func (o overRender) Objects() []fyne.CanvasObject { 141 | if o.c == nil { 142 | return []fyne.CanvasObject{o.r} 143 | } 144 | 145 | return append([]fyne.CanvasObject{o.r}, o.c.Objects...) 146 | } 147 | 148 | func (o overRender) Refresh() { 149 | if o.p == current { 150 | o.r.StrokeColor = theme.PrimaryColor() 151 | } else { 152 | o.r.StrokeColor = color.Transparent 153 | } 154 | o.r.Refresh() 155 | } 156 | 157 | func setCurrent(o fyne.CanvasObject) { 158 | old := current 159 | current = o 160 | if old != nil { 161 | old.Refresh() 162 | } 163 | current.Refresh() 164 | } 165 | 166 | func wrapContent(o fyne.CanvasObject, parent *fyne.Container) fyne.CanvasObject { 167 | switch obj := o.(type) { 168 | case *fyne.Container: 169 | var c *fyne.Container 170 | if obj.Layout == nil { 171 | c = container.NewWithoutLayout() 172 | } else { 173 | c = container.New(obj.Layout) 174 | } 175 | items := make([]fyne.CanvasObject, len(obj.Objects)) 176 | for i, child := range obj.Objects { 177 | items[i] = wrapContent(child, c) 178 | } 179 | c.Objects = items 180 | 181 | o := &overlayContainer{c: c, parent: parent} 182 | layoutProps[o.c] = map[string]string{"layout": "VBox"} 183 | o.ExtendBaseWidget(o) 184 | return o 185 | case fyne.Widget: 186 | return wrapWidget(obj, parent) 187 | } 188 | 189 | return nil //? 190 | } 191 | 192 | func wrapWidget(w fyne.Widget, parent *fyne.Container) fyne.CanvasObject { 193 | switch t := w.(type) { 194 | case *widget.Icon: 195 | t.Resource = wrapResource(t.Resource) 196 | case *widget.Button: 197 | if t.Icon != nil { 198 | t.Icon = wrapResource(t.Icon) 199 | } 200 | } 201 | o := &overlayWidget{child: w, parent: parent} 202 | o.ExtendBaseWidget(o) 203 | return container.NewMax(w, o) 204 | } 205 | 206 | func wrapResource(r fyne.Resource) fyne.Resource { 207 | return &jsonResource{r} 208 | } 209 | --------------------------------------------------------------------------------