package vumeter import ( "fmt" "image/color" "strconv" "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 { label *canvas.Text background, bar, optimumBar, peakBar, lowalphaGreen, lowalphaAmber, lowalphaRed *canvas.Rectangle objects []fyne.CanvasObject meter *vuMeter } // MinSize calculates the minimum size of the VU meter. Code shamelessly stolen from progressbar for now. func (v *vuRenderer) MinSize() fyne.Size { var tsize fyne.Size if text := v.meter.TextFormatter; text != nil { tsize = fyne.MeasureText(text(), v.label.TextSize, v.label.TextStyle) } else { tsize = fyne.MeasureText("100%", v.label.TextSize, v.label.TextStyle) } return fyne.NewSize(tsize.Width+theme.Padding()*4, tsize.Height+theme.Padding()*2) } func (v *vuRenderer) updateBars() { if v.meter.Value < v.meter.Min { v.meter.Value = v.meter.Min } if v.meter.Value > v.meter.Max { v.meter.Value = v.meter.Max } delta := float64(v.meter.Max - v.meter.Min) ratio := float64(v.meter.Value-v.meter.Min) / delta if text := v.meter.TextFormatter; text != nil { v.label.Text = text() } else { v.label.Text = strconv.Itoa(int(ratio*100)) + "%" } size := v.meter.Size() var ratioSize float64 if v.meter.VUMeterDirection == VUMeterHorizontal { ratioSize = float64(size.Width) } else { ratioSize = float64(size.Height) } greenSize := 0.0 amberSize := 0.0 redSize := 0.0 if ratio > (v.meter.OptimumValueMin / 100) { if ratio > (v.meter.OptimumValueMax / 100) { greenSize = ratioSize * v.meter.OptimumValueMin / 100 amberSize = ratioSize * v.meter.OptimumValueMax / 100 redSize = ratioSize * ratio } else { greenSize = ratioSize * v.meter.OptimumValueMin / 100 amberSize = ratioSize * ratio } } else { greenSize = ratioSize * ratio } redSize = redSize - amberSize if redSize < 0 { redSize = 0 } amberSize = amberSize - greenSize if amberSize < 0 { amberSize = 0 } lowalphaGreenSize := ratioSize * v.meter.OptimumValueMin / 100 lowalphaAmberSize := ratioSize * v.meter.OptimumValueMax / 100 lowalphaRedSize := ratioSize lowalphaRedSize = lowalphaRedSize - lowalphaAmberSize lowalphaAmberSize = lowalphaAmberSize - lowalphaGreenSize if v.meter.VUMeterDirection == VUMeterHorizontal { v.lowalphaGreen.Move(fyne.NewPos(0, 0)) v.lowalphaGreen.Resize(fyne.NewSize(float32(lowalphaGreenSize), size.Height)) v.lowalphaAmber.Resize(fyne.NewSize(float32(lowalphaAmberSize), size.Height)) v.lowalphaAmber.Move(fyne.NewPos(float32(lowalphaGreenSize), 0)) v.lowalphaRed.Resize(fyne.NewSize(float32(lowalphaRedSize), size.Height)) v.lowalphaRed.Move(fyne.NewPos(float32(lowalphaAmberSize+lowalphaGreenSize), 0)) } else { v.lowalphaGreen.Move(fyne.NewPos(0, size.Height-float32(lowalphaGreenSize))) v.lowalphaGreen.Resize(fyne.NewSize(size.Width, float32(lowalphaGreenSize))) v.lowalphaAmber.Resize(fyne.NewSize(size.Width, float32(lowalphaAmberSize))) v.lowalphaAmber.Move(fyne.NewPos(0, size.Height-float32(lowalphaGreenSize+lowalphaAmberSize))) v.lowalphaRed.Resize(fyne.NewSize(size.Width, float32(lowalphaRedSize))) v.lowalphaRed.Move(fyne.NewPos(0, size.Height-float32(lowalphaGreenSize+lowalphaAmberSize+lowalphaRedSize))) } if v.meter.VUMeterDirection == VUMeterHorizontal { v.bar.Move(fyne.NewPos(0, 0)) v.bar.Resize(fyne.NewSize(float32(greenSize), size.Height)) v.optimumBar.Resize(fyne.NewSize(float32(amberSize), size.Height)) v.optimumBar.Move(fyne.NewPos(float32(greenSize), 0)) v.peakBar.Resize(fyne.NewSize(float32(redSize), size.Height)) v.peakBar.Move(fyne.NewPos(float32(greenSize+amberSize), 0)) } else { v.bar.Resize(fyne.NewSize(size.Width, float32(greenSize))) v.bar.Move(fyne.NewPos(0, size.Height-float32(greenSize))) v.optimumBar.Resize(fyne.NewSize(size.Width, float32(amberSize))) v.optimumBar.Move(fyne.NewPos(0, size.Height-float32(greenSize+amberSize))) v.peakBar.Resize(fyne.NewSize(size.Width, float32(redSize))) v.peakBar.Move(fyne.NewPos(0, size.Height-float32(greenSize+amberSize+redSize))) } } // Layout the components of the widget func (v *vuRenderer) Layout(size fyne.Size) { v.background.Resize(size) v.label.Resize(size) v.updateBars() } // ApplyTheme is called when the vuMeter may need to update it's look func (v *vuRenderer) ApplyTheme() { v.label.Color = theme.ForegroundColor() v.Refresh() } func (v *vuRenderer) BackgroundColor() color.Color { return theme.ButtonColor() } func (v *vuRenderer) Refresh() { v.label.Text = fmt.Sprintf("%f %%", v.meter.Value) fmt.Printf("%f %%\n", v.meter.Value) v.Layout(v.meter.Size()) canvas.Refresh(v.meter) } func (v *vuRenderer) Objects() []fyne.CanvasObject { return v.objects } 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 (m *vuMeter) CreateRenderer() fyne.WidgetRenderer { m.ExtendBaseWidget(m) if m.Min == 0 && m.Max == 0 { m.Max = 1.0 } background := canvas.NewRectangle(theme.BackgroundColor()) lowalphaGreen := canvas.NewRectangle(color.RGBA{0, 255, 0, 64}) lowalphaAmber := canvas.NewRectangle(color.RGBA{255, 200, 0, 64}) lowalphaRed := canvas.NewRectangle(color.RGBA{255, 0, 0, 64}) bar := canvas.NewRectangle(color.RGBA{0, 255, 0, 255}) optimumBar := canvas.NewRectangle(color.RGBA{255, 200, 0, 255}) peakBar := canvas.NewRectangle(color.RGBA{255, 0, 0, 255}) label := canvas.NewText("0%", theme.ForegroundColor()) label.Alignment = fyne.TextAlignCenter objects := []fyne.CanvasObject{ background, lowalphaGreen, lowalphaAmber, lowalphaRed, bar, optimumBar, peakBar, label, } return &vuRenderer{label, background, bar, optimumBar, peakBar, lowalphaGreen, lowalphaAmber, lowalphaRed, objects, 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.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 }