256 lines
6.4 KiB
Go
256 lines
6.4 KiB
Go
package numeter
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"time"
|
|
|
|
"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
|
|
lastPeakTime time.Time
|
|
lastPeakVal float64
|
|
meter *vuMeter
|
|
}
|
|
|
|
func (v *vuRenderer) MinSize() fyne.Size {
|
|
if (v.meter.overriddenMinSize.Height >= 0) && (v.meter.overriddenMinSize.Height >= 0) {
|
|
return v.meter.overriddenMinSize
|
|
}
|
|
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 drawPeakBar(i *image.RGBA, w int, h int, o VUMeterDirectionEnum, val float64, colGrn color.Color, colAmber color.Color, colRed color.Color) {
|
|
valPixel := int(math.Round(float64(w) * val))
|
|
c := colGrn
|
|
if o == VUMeterVertical {
|
|
valPixel = int(math.Round(float64(h) * val))
|
|
if float64(valPixel)/float64(h) > 0.75 {
|
|
c = colAmber
|
|
}
|
|
if float64(valPixel)/float64(h) > 0.85 {
|
|
c = colRed
|
|
}
|
|
for x := 0; x < w; x++ {
|
|
i.Set(x, valPixel-h, c)
|
|
}
|
|
} else {
|
|
valPixel = int(math.Round(float64(w) * val))
|
|
if float64(valPixel)/float64(w) > 0.75 {
|
|
c = colAmber
|
|
}
|
|
if float64(valPixel)/float64(w) > 0.85 {
|
|
c = colRed
|
|
}
|
|
for y := 0; y < h; y++ {
|
|
i.Set(valPixel, y, 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)
|
|
if v.meter.Value > v.lastPeakVal {
|
|
//fmt.Printf("New peak: %f, expires at %s\n", v.meter.Value, time.Now().Add(v.meter.Peakhold))
|
|
v.lastPeakTime = time.Now()
|
|
v.lastPeakVal = v.meter.Value
|
|
}
|
|
if v.lastPeakTime.Add(v.meter.Peakhold).Before(time.Now()) {
|
|
//fmt.Printf("Previous peak good, bar at %f\n", v.lastPeakVal)
|
|
drawPeakBar(i, w, h, v.meter.VUMeterDirection, v.lastPeakVal, g, a, r)
|
|
}
|
|
if v.lastPeakTime.Add(v.meter.Peakhold).After(time.Now()) {
|
|
//fmt.Printf("Previous peak expired, holding at %f\n", v.meter.Value)
|
|
v.lastPeakVal = v.meter.Value
|
|
v.lastPeakTime = time.Now()
|
|
}
|
|
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
|
|
Peakhold time.Duration
|
|
VUMeterDirection VUMeterDirectionEnum
|
|
|
|
binder basicBinder
|
|
overriddenMinSize fyne.Size
|
|
}
|
|
|
|
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, time.Now(), 0, m}
|
|
c.Generator = renderer.Render
|
|
renderer.currentframe = c
|
|
renderer.lastPeakTime = time.Now()
|
|
renderer.lastPeakVal = 0
|
|
|
|
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()
|
|
}
|
|
|
|
func (m *vuMeter) SetMinSize(s fyne.Size) {
|
|
m.overriddenMinSize = s
|
|
m.Refresh()
|
|
}
|
|
|
|
// 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.overriddenMinSize = fyne.NewSize(-1, -1)
|
|
meter.VUMeterDirection = VUMeterHorizontal
|
|
meter.Peakhold = 0
|
|
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
|
|
}
|