New vumeter (numeter) based on Raster instead of boxes with z-level problems.
This commit is contained in:
parent
635dc6e56e
commit
9209012be3
|
@ -0,0 +1,33 @@
|
|||
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"
|
||||
"git.martyn.berlin/martyn/fyne-widgets/pkg/numeter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := app.New()
|
||||
w := a.NewWindow("VUMeter")
|
||||
level := binding.NewFloat()
|
||||
levelPB := numeter.NewVUMeterWithData(level)
|
||||
level.Set(0.95)
|
||||
levelPB.TextFormatter = func() string { return " " }
|
||||
progress := widget.NewProgressBarWithData(level)
|
||||
setLevel := binding.NewFloat()
|
||||
setLevel.Set(75.0)
|
||||
slider := widget.NewSliderWithData(0.0, 100.0, setLevel)
|
||||
slider.OnChanged = func(data float64) {
|
||||
level.Set(data / 100)
|
||||
}
|
||||
|
||||
vLevelPB := numeter.NewVUMeterWithData(level)
|
||||
vLevelPB.VUMeterDirection = numeter.VUMeterVertical
|
||||
levelPB.TextFormatter = func() string { return " " }
|
||||
contain := container.New(layout.NewVBoxLayout(), levelPB, progress, slider)
|
||||
w.SetContent(container.New(layout.NewHBoxLayout(), contain, vLevelPB))
|
||||
w.ShowAndRun()
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package numeter
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package numeter
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"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 vuRenderer struct {
|
||||
currentframe *canvas.Raster
|
||||
meter *vuMeter
|
||||
}
|
||||
|
||||
func (v *vuRenderer) MinSize() fyne.Size {
|
||||
var tsize fyne.Size
|
||||
if text := v.meter.TextFormatter; text != nil {
|
||||
tsize = fyne.MeasureText(text(), theme.TextSize(), fyne.TextStyle{false, false, false, 4})
|
||||
} else {
|
||||
tsize = fyne.MeasureText("100%", theme.TextSize(), fyne.TextStyle{false, false, false, 4})
|
||||
}
|
||||
if v.meter.VUMeterDirection == VUMeterVertical {
|
||||
oldsize := tsize
|
||||
tsize.Width = oldsize.Height
|
||||
tsize.Height = oldsize.Width
|
||||
}
|
||||
return fyne.NewSize(tsize.Width+theme.Padding()*4, tsize.Height+theme.Padding()*2)
|
||||
}
|
||||
|
||||
func vuSet(i *image.RGBA, w int, h int, o VUMeterDirectionEnum, val float64, colGrn color.Color, colAmber color.Color, colRed color.Color) {
|
||||
valPixels := int(math.Round(float64(w) * val))
|
||||
long := w
|
||||
short := h
|
||||
if o == VUMeterVertical {
|
||||
valPixels = int(math.Round(float64(h) * val))
|
||||
long = h
|
||||
short = w
|
||||
}
|
||||
for l := 0; l < valPixels; l++ {
|
||||
c := colGrn
|
||||
if float64(l)/float64(long) > 0.75 {
|
||||
c = colAmber
|
||||
}
|
||||
if float64(l)/float64(long) > 0.85 {
|
||||
c = colRed
|
||||
}
|
||||
for s := 0; s < short; s++ {
|
||||
if o == VUMeterVertical {
|
||||
i.Set(s, h-l, c)
|
||||
} else {
|
||||
i.Set(l, s, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vuRenderer) Render(w int, h int) image.Image {
|
||||
i := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
g := color.RGBA{0, 0x1f, 0, 0xff}
|
||||
a := color.RGBA{0x1f, 0x1f, 0, 0xff}
|
||||
r := color.RGBA{0x1f, 0, 0, 0xff}
|
||||
vuSet(i, w, h, v.meter.VUMeterDirection, 1, g, a, r)
|
||||
g = color.RGBA{0, 0xff, 0, 0xff}
|
||||
a = color.RGBA{0xff, 0xff, 0, 0xff}
|
||||
r = color.RGBA{0xff, 0, 0, 0xff}
|
||||
val := (v.meter.Value - v.meter.Min) / (v.meter.Max - v.meter.Min) * 100
|
||||
vuSet(i, w, h, v.meter.VUMeterDirection, val, g, a, r)
|
||||
return i
|
||||
}
|
||||
|
||||
// Layout the components of the widget
|
||||
func (v *vuRenderer) Layout(size fyne.Size) {
|
||||
v.currentframe.Resize(size)
|
||||
v.currentframe.Refresh()
|
||||
}
|
||||
|
||||
// ApplyTheme is called when the vuMeter may need to update it's look
|
||||
func (v *vuRenderer) ApplyTheme() {
|
||||
v.Refresh()
|
||||
}
|
||||
|
||||
func (v *vuRenderer) BackgroundColor() color.Color {
|
||||
return theme.ButtonColor()
|
||||
}
|
||||
|
||||
func (v *vuRenderer) Refresh() {
|
||||
v.Layout(v.meter.Size())
|
||||
canvas.Refresh(v.meter)
|
||||
}
|
||||
|
||||
func (v *vuRenderer) Objects() []fyne.CanvasObject {
|
||||
return []fyne.CanvasObject{v.currentframe}
|
||||
}
|
||||
|
||||
func (v *vuRenderer) Destroy() {
|
||||
}
|
||||
|
||||
type VUMeterDirectionEnum uint32
|
||||
|
||||
const (
|
||||
VUMeterVertical = iota
|
||||
VUMeterHorizontal
|
||||
)
|
||||
|
||||
// vuMeter widget is a kind of custom progressbar but has "zones" of different color for peaking.
|
||||
type vuMeter struct {
|
||||
widget.BaseWidget
|
||||
TextFormatter func() string
|
||||
Value, Min, Max,
|
||||
OptimumValueMin, OptimumValueMax float64
|
||||
VUMeterDirection VUMeterDirectionEnum
|
||||
|
||||
binder basicBinder
|
||||
}
|
||||
|
||||
func NewVUMeterRenderer(m *vuMeter) *vuRenderer {
|
||||
c := canvas.NewRaster(func(w int, h int) image.Image { return image.NewNRGBA(image.Rect(0, 0, 200, 200)) })
|
||||
renderer := vuRenderer{c, m}
|
||||
c.Generator = renderer.Render
|
||||
renderer.currentframe = c
|
||||
|
||||
return &renderer
|
||||
}
|
||||
|
||||
func (m *vuMeter) Resize(s fyne.Size) {
|
||||
m.BaseWidget.Resize(s)
|
||||
}
|
||||
|
||||
func (m *vuMeter) CreateRenderer() fyne.WidgetRenderer {
|
||||
return NewVUMeterRenderer(m)
|
||||
}
|
||||
|
||||
// SetValue changes the current value of this progress bar (from p.Min to p.Max).
|
||||
// The widget will be refreshed to indicate the change.
|
||||
func (m *vuMeter) SetValue(v float64) {
|
||||
m.Value = v
|
||||
m.Refresh()
|
||||
}
|
||||
|
||||
func (m *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
|
||||
}
|
||||
m.SetValue(val)
|
||||
}
|
||||
|
||||
func (m *vuMeter) MinSize() fyne.Size {
|
||||
m.ExtendBaseWidget(m)
|
||||
return m.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
func (m *vuMeter) Bind(data binding.Float) {
|
||||
m.binder.SetCallback(m.updateFromData)
|
||||
m.binder.Bind(data)
|
||||
}
|
||||
|
||||
func (m *vuMeter) Unbind() {
|
||||
m.binder.Unbind()
|
||||
}
|
||||
|
||||
// NewVUMeter creates a new meter widget with the specified value
|
||||
func NewVUMeter(value float64) *vuMeter {
|
||||
meter := &vuMeter{Value: value}
|
||||
meter.OptimumValueMin = 75
|
||||
meter.OptimumValueMax = 85
|
||||
meter.Min = 0
|
||||
meter.Max = 100
|
||||
meter.ExtendBaseWidget(meter)
|
||||
meter.VUMeterDirection = VUMeterHorizontal
|
||||
return meter
|
||||
}
|
||||
|
||||
func NewVUMeterWithData(data binding.Float) *vuMeter {
|
||||
f, err := data.Get()
|
||||
if err != nil {
|
||||
f = 25.0
|
||||
}
|
||||
m := NewVUMeter(f)
|
||||
m.Bind(data)
|
||||
return m
|
||||
}
|
Loading…
Reference in New Issue