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 }