From f4e556aa5651f9bcaedfdd84c3baad2190daaaaf Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 10 Dec 2021 21:09:01 +0100 Subject: [PATCH] Initial attempt at VUMeter --- examples/vumeter/main.go | 20 +++++ go.mod | 26 ++++++ go.sum | 105 ++++++++++++++++++++++ internal/cache/widget.go | 52 +++++++++++ internal/color/color.go | 97 ++++++++++++++++++++ pkg/vumeter/base_renderer.go | 32 +++++++ pkg/vumeter/bind_helper.go | 78 +++++++++++++++++ pkg/vumeter/vumeter.go | 165 +++++++++++++++++++++++++++++++++++ 8 files changed, 575 insertions(+) create mode 100644 examples/vumeter/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cache/widget.go create mode 100644 internal/color/color.go create mode 100644 pkg/vumeter/base_renderer.go create mode 100644 pkg/vumeter/bind_helper.go create mode 100644 pkg/vumeter/vumeter.go diff --git a/examples/vumeter/main.go b/examples/vumeter/main.go new file mode 100644 index 0000000..6558dc6 --- /dev/null +++ b/examples/vumeter/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" +) + +func main() { + a := app.New() + w := a.NewWindow("Diagonal") + level := binding.NewFloat() + levelPB := widget.NewProgressBarWithData(level) + levelPB.TextFormatter = func() string { return " " } + + w.SetContent(container.New(layout.NewHBoxLayout(), levelPB)) + w.ShowAndRun() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..28ff07d --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module git.martyn.berlin/martyn/fyne-widgets + +go 1.17 + +require fyne.io/fyne/v2 v2.1.2 + +require ( + fyne.io/fyne v1.4.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be // indirect + github.com/godbus/dbus/v5 v5.0.4 // indirect + github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect + github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect + github.com/stretchr/testify v1.5.1 // indirect + github.com/yuin/goldmark v1.3.8 // indirect + golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.3 // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9f46c36 --- /dev/null +++ b/go.sum @@ -0,0 +1,105 @@ +fyne.io/fyne v1.4.3 h1:356CnXCiYrrfaLGsB7qLK3c6ktzyh8WR05v/2RBu51I= +fyne.io/fyne v1.4.3/go.mod h1:8kiPBNSDmuplxs9WnKCkaWYqbcXFy0DeAzwa6PBO9Z8= +fyne.io/fyne/v2 v2.1.2 h1:avp9CvLAUdvE7fDMtH1tVKyjxEWHWcpow6aI6L7Kvvw= +fyne.io/fyne/v2 v2.1.2/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU= +github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= +github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E= +github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/cache/widget.go b/internal/cache/widget.go new file mode 100644 index 0000000..e705df6 --- /dev/null +++ b/internal/cache/widget.go @@ -0,0 +1,52 @@ +package cache + +import ( + "sync" + + "fyne.io/fyne/v2" +) + +var renderers sync.Map + +type isBaseWidget interface { + ExtendBaseWidget(fyne.Widget) + super() fyne.Widget +} + +// Renderer looks up the render implementation for a widget +func Renderer(wid fyne.Widget) fyne.WidgetRenderer { + if wid == nil { + return nil + } + + if wd, ok := wid.(isBaseWidget); ok { + if wd.super() != nil { + wid = wd.super() + } + } + renderer, ok := renderers.Load(wid) + if !ok { + renderer = wid.CreateRenderer() + renderers.Store(wid, renderer) + } + + if renderer == nil { + return nil + } + return renderer.(fyne.WidgetRenderer) +} + +// DestroyRenderer frees a render implementation for a widget. +// This is typically for internal use only. +func DestroyRenderer(wid fyne.Widget) { + Renderer(wid).Destroy() + + renderers.Delete(wid) +} + +// IsRendered returns true of the widget currently has a renderer. +// One will be created the first time a widget is shown but may be removed after it is hidden. +func IsRendered(wid fyne.Widget) bool { + _, found := renderers.Load(wid) + return found +} diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 0000000..2b6ea8a --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,97 @@ +package color + +import ( + "image/color" +) + +// ToNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA(). +func ToNRGBA(c color.Color) (r, g, b, a int) { + // We use UnmultiplyAlpha with RGBA, RGBA64, and unrecognized implementations of Color. + // It works for all Colors whose RGBA() method is implemented according to spec, but is only necessary for those. + // Only RGBA and RGBA64 have components which are already premultiplied. + switch col := c.(type) { + // NRGBA and NRGBA64 are not premultiplied + case color.NRGBA: + r = int(col.R) + g = int(col.G) + b = int(col.B) + a = int(col.A) + case *color.NRGBA: + r = int(col.R) + g = int(col.G) + b = int(col.B) + a = int(col.A) + case color.NRGBA64: + r = int(col.R) >> 8 + g = int(col.G) >> 8 + b = int(col.B) >> 8 + a = int(col.A) >> 8 + case *color.NRGBA64: + r = int(col.R) >> 8 + g = int(col.G) >> 8 + b = int(col.B) >> 8 + a = int(col.A) >> 8 + // Gray and Gray16 have no alpha component + case *color.Gray: + r = int(col.Y) + g = int(col.Y) + b = int(col.Y) + a = 0xff + case color.Gray: + r = int(col.Y) + g = int(col.Y) + b = int(col.Y) + a = 0xff + case *color.Gray16: + r = int(col.Y) >> 8 + g = int(col.Y) >> 8 + b = int(col.Y) >> 8 + a = 0xff + case color.Gray16: + r = int(col.Y) >> 8 + g = int(col.Y) >> 8 + b = int(col.Y) >> 8 + a = 0xff + // Alpha and Alpha16 contain only an alpha component. + case color.Alpha: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) + case *color.Alpha: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) + case color.Alpha16: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) >> 8 + case *color.Alpha16: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) >> 8 + default: // RGBA, RGBA64, and unknown implementations of Color + r, g, b, a = unmultiplyAlpha(c) + } + return +} + +// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication. +// It is only used by ToRGBA. +func unmultiplyAlpha(c color.Color) (r, g, b, a int) { + red, green, blue, alpha := c.RGBA() + if alpha != 0 && alpha != 0xffff { + red = (red * 0xffff) / alpha + green = (green * 0xffff) / alpha + blue = (blue * 0xffff) / alpha + } + // Convert from range 0-65535 to range 0-255 + r = int(red >> 8) + g = int(green >> 8) + b = int(blue >> 8) + a = int(alpha >> 8) + return +} diff --git a/pkg/vumeter/base_renderer.go b/pkg/vumeter/base_renderer.go new file mode 100644 index 0000000..7f9c34d --- /dev/null +++ b/pkg/vumeter/base_renderer.go @@ -0,0 +1,32 @@ +package vumeter + +import "fyne.io/fyne/v2" + +// BaseRenderer is a renderer base providing the most common implementations of a part of the +// widget.Renderer interface. +type BaseRenderer struct { + objects []fyne.CanvasObject +} + +// NewBaseRenderer creates a new BaseRenderer. +func NewBaseRenderer(objects []fyne.CanvasObject) BaseRenderer { + return BaseRenderer{objects} +} + +// Destroy does nothing in the base implementation. +// +// Implements: fyne.WidgetRenderer +func (r *BaseRenderer) Destroy() { +} + +// Objects returns the objects that should be rendered. +// +// Implements: fyne.WidgetRenderer +func (r *BaseRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// SetObjects updates the objects of the renderer. +func (r *BaseRenderer) SetObjects(objects []fyne.CanvasObject) { + r.objects = objects +} diff --git a/pkg/vumeter/bind_helper.go b/pkg/vumeter/bind_helper.go new file mode 100644 index 0000000..82c8457 --- /dev/null +++ b/pkg/vumeter/bind_helper.go @@ -0,0 +1,78 @@ +package vumeter + +import ( + "sync" + + "fyne.io/fyne/v2/data/binding" +) + +// basicBinder stores a DataItem and a function to be called when it changes. +// It provides a convenient way to replace data and callback independently. +type basicBinder struct { + callbackLock sync.RWMutex + callback func(binding.DataItem) // access guarded by callbackLock + dataListenerPairLock sync.RWMutex + dataListenerPair annotatedListener // access guarded by dataListenerPairLock +} + +// Bind replaces the data item whose changes are tracked by the callback function. +func (binder *basicBinder) Bind(data binding.DataItem) { + listener := binding.NewDataListener(func() { // NB: listener captures `data` but always calls the up-to-date callback + binder.callbackLock.RLock() + f := binder.callback + binder.callbackLock.RUnlock() + if f != nil { + f(data) + } + }) + data.AddListener(listener) + listenerInfo := annotatedListener{ + data: data, + listener: listener, + } + + binder.dataListenerPairLock.Lock() + binder.unbindLocked() + binder.dataListenerPair = listenerInfo + binder.dataListenerPairLock.Unlock() +} + +// CallWithData passes the currently bound data item as an argument to the +// provided function. +func (binder *basicBinder) CallWithData(f func(data binding.DataItem)) { + binder.dataListenerPairLock.RLock() + data := binder.dataListenerPair.data + binder.dataListenerPairLock.RUnlock() + f(data) +} + +// SetCallback replaces the function to be called when the data changes. +func (binder *basicBinder) SetCallback(f func(data binding.DataItem)) { + binder.callbackLock.Lock() + binder.callback = f + binder.callbackLock.Unlock() +} + +// Unbind requests the callback to be no longer called when the previously bound +// data item changes. +func (binder *basicBinder) Unbind() { + binder.dataListenerPairLock.Lock() + binder.unbindLocked() + binder.dataListenerPairLock.Unlock() +} + +// unbindLocked expects the caller to hold dataListenerPairLock. +func (binder *basicBinder) unbindLocked() { + previousListener := binder.dataListenerPair + binder.dataListenerPair = annotatedListener{nil, nil} + + if previousListener.listener == nil || previousListener.data == nil { + return + } + previousListener.data.RemoveListener(previousListener.listener) +} + +type annotatedListener struct { + data binding.DataItem + listener binding.DataListener +} diff --git a/pkg/vumeter/vumeter.go b/pkg/vumeter/vumeter.go new file mode 100644 index 0000000..cfe2923 --- /dev/null +++ b/pkg/vumeter/vumeter.go @@ -0,0 +1,165 @@ +package vumeter + +import ( + "image/color" + "strconv" + + "git.martyn.berlin/martyn/fyne-widgets/internal/cache" + col "git.martyn.berlin/martyn/fyne-widgets/internal/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type VUMeter struct { + widget.BaseWidget + Min, Max, Value float64 + TextFormatter func() string + + binder basicBinder +} + +type vuMeterRenderer struct { + objects []fyne.CanvasObject + background, bar *canvas.Rectangle + label *canvas.Text + meter *VUMeter +} + +func (p *vuMeterRenderer) MinSize() fyne.Size { + var tsize fyne.Size + if text := p.meter.TextFormatter; text != nil { + tsize = fyne.MeasureText(text(), p.label.TextSize, p.label.TextStyle) + } else { + tsize = fyne.MeasureText("100%", p.label.TextSize, p.label.TextStyle) + } + + return fyne.NewSize(tsize.Width+theme.Padding()*4, tsize.Height+theme.Padding()*2) +} + +func (p *vuMeterRenderer) updateBar() { + if p.meter.Value < p.meter.Min { + p.meter.Value = p.meter.Min + } + if p.meter.Value > p.meter.Max { + p.meter.Value = p.meter.Max + } + + delta := float32(p.meter.Max - p.meter.Min) + ratio := float32(p.meter.Value-p.meter.Min) / delta + + if text := p.meter.TextFormatter; text != nil { + p.label.Text = text() + } else { + p.label.Text = strconv.Itoa(int(ratio*100)) + "%" + } + + size := p.meter.Size() + p.bar.Resize(fyne.NewSize(size.Width*ratio, size.Height)) +} + +func (p *vuMeterRenderer) Layout(size fyne.Size) { + p.background.Resize(size) + p.label.Resize(size) + p.updateBar() +} + +func (p *vuMeterRenderer) applyTheme() { + p.background.FillColor = vuMeterBackgroundColor() + p.bar.FillColor = theme.PrimaryColor() + p.label.Color = theme.ForegroundColor() + p.label.TextSize = theme.TextSize() +} + +func (p *vuMeterRenderer) Refresh() { + p.applyTheme() + p.updateBar() + p.background.Refresh() + p.bar.Refresh() + p.meter.super() +} + +func (r *vuMeterRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// SetObjects updates the objects of the renderer. +func (r *vuMeterRenderer) SetObjects(objects []fyne.CanvasObject) { + r.objects = objects +} + +func (r *vuMeterRenderer) Destroy() { + +} + +func (p *VUMeter) Bind(data binding.Float) { + p.binder.SetCallback(p.updateFromData) + p.binder.Bind(data) +} + +func (p *VUMeter) SetValue(v float64) { + p.Value = v + p.Refresh() +} + +func (p *VUMeter) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +func (p *VUMeter) CreateRenderer() fyne.WidgetRenderer { + p.ExtendBaseWidget(p) + if p.Min == 0 && p.Max == 0 { + p.Max = 1.0 + } + + background := canvas.NewRectangle(vuMeterBackgroundColor()) + bar := canvas.NewRectangle(theme.PrimaryColor()) + label := canvas.NewText("0%", theme.ForegroundColor()) + label.Alignment = fyne.TextAlignCenter + return &vuMeterRenderer{[]fyne.CanvasObject{background, bar, label}, background, bar, label, p} +} + +func (p *VUMeter) Unbind() { + p.binder.Unbind() +} + +func NewVUMeter() *VUMeter { + p := &VUMeter{Min: 0, Max: 1} + + cache.Renderer(p).Layout(p.MinSize()) + return p +} + +func NewVUMeterWithData(data binding.Float) *VUMeter { + p := NewVUMeter() + p.Bind(data) + + return p +} + +func vuMeterBackgroundColor() color.Color { + r, g, b, a := col.ToNRGBA(theme.PrimaryColor()) + faded := uint8(a) / 3 + return &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: faded} +} + +func (p *VUMeter) updateFromData(data binding.DataItem) { + if data == nil { + return + } + floatSource, ok := data.(binding.Float) + if !ok { + return + } + + val, err := floatSource.Get() + if err != nil { + fyne.LogError("Error getting current data value", err) + return + } + p.SetValue(val) +}