diff --git a/examples/vumeter/main.go b/examples/vumeter/main.go index 6558dc6..436857f 100644 --- a/examples/vumeter/main.go +++ b/examples/vumeter/main.go @@ -6,15 +6,24 @@ import ( "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" + "git.martyn.berlin/martyn/fyne-widgets/pkg/vumeter" ) func main() { a := app.New() - w := a.NewWindow("Diagonal") + w := a.NewWindow("VUMeter") level := binding.NewFloat() - levelPB := widget.NewProgressBarWithData(level) + levelPB := vumeter.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) + } - w.SetContent(container.New(layout.NewHBoxLayout(), levelPB)) + w.SetContent(container.New(layout.NewVBoxLayout(), levelPB, progress, slider)) w.ShowAndRun() } diff --git a/internal/cache/widget.go b/internal/cache/widget.go deleted file mode 100644 index e705df6..0000000 --- a/internal/cache/widget.go +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 2b6ea8a..0000000 --- a/internal/color/color.go +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 7f9c34d..0000000 --- a/pkg/vumeter/base_renderer.go +++ /dev/null @@ -1,32 +0,0 @@ -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/vumeter.go b/pkg/vumeter/vumeter.go index cfe2923..6dae69d 100644 --- a/pkg/vumeter/vumeter.go +++ b/pkg/vumeter/vumeter.go @@ -1,12 +1,10 @@ package vumeter import ( + "fmt" "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" @@ -14,140 +12,172 @@ import ( "fyne.io/fyne/v2/widget" ) -type VUMeter struct { - widget.BaseWidget - Min, Max, Value float64 - TextFormatter func() string +type vuRenderer struct { + label *canvas.Text + background, bar, + optimumBar, peakBar, + lowalphaGreen, + lowalphaAmber, + lowalphaRed *canvas.Rectangle - binder basicBinder + objects []fyne.CanvasObject + meter *vuMeter } -type vuMeterRenderer struct { - objects []fyne.CanvasObject - background, bar *canvas.Rectangle - label *canvas.Text - meter *VUMeter -} - -func (p *vuMeterRenderer) MinSize() fyne.Size { +// 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 := p.meter.TextFormatter; text != nil { - tsize = fyne.MeasureText(text(), p.label.TextSize, p.label.TextStyle) + if text := v.meter.TextFormatter; text != nil { + tsize = fyne.MeasureText(text(), v.label.TextSize, v.label.TextStyle) } else { - tsize = fyne.MeasureText("100%", p.label.TextSize, p.label.TextStyle) + tsize = fyne.MeasureText("100%", v.label.TextSize, v.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 +func (v *vuRenderer) updateBars() { + if v.meter.Value < v.meter.Min { + v.meter.Value = v.meter.Min } - if p.meter.Value > p.meter.Max { - p.meter.Value = p.meter.Max + if v.meter.Value > v.meter.Max { + v.meter.Value = v.meter.Max } - delta := float32(p.meter.Max - p.meter.Min) - ratio := float32(p.meter.Value-p.meter.Min) / delta + delta := float64(v.meter.Max - v.meter.Min) + ratio := float64(v.meter.Value-v.meter.Min) / delta - if text := p.meter.TextFormatter; text != nil { - p.label.Text = text() + if text := v.meter.TextFormatter; text != nil { + v.label.Text = text() } else { - p.label.Text = strconv.Itoa(int(ratio*100)) + "%" + v.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 + size := v.meter.Size() + greenWidth := 0.0 + amberWidth := 0.0 + redWidth := 0.0 + if ratio > (v.meter.OptimumValueMin / 100) { + if ratio > (v.meter.OptimumValueMax / 100) { + greenWidth = float64(size.Width) * v.meter.OptimumValueMin / 100 + amberWidth = float64(size.Width) * v.meter.OptimumValueMax / 100 + redWidth = float64(size.Width) * ratio + } else { + greenWidth = float64(size.Width) * v.meter.OptimumValueMin / 100 + amberWidth = float64(size.Width) * ratio + } + } else { + greenWidth = float64(size.Width) * ratio + } + redWidth = redWidth - amberWidth + if redWidth < 0 { + redWidth = 0 + } + amberWidth = amberWidth - greenWidth + if amberWidth < 0 { + amberWidth = 0 } - background := canvas.NewRectangle(vuMeterBackgroundColor()) - bar := canvas.NewRectangle(theme.PrimaryColor()) + lowalphaGreenWidth := float64(size.Width) * v.meter.OptimumValueMin / 100 + lowalphaAmberWidth := float64(size.Width) * v.meter.OptimumValueMax / 100 + lowalphaRedWidth := float64(size.Width) + lowalphaRedWidth = lowalphaRedWidth - lowalphaAmberWidth + lowalphaAmberWidth = lowalphaAmberWidth - lowalphaGreenWidth + + v.lowalphaGreen.Resize(fyne.NewSize(float32(lowalphaGreenWidth), size.Height)) + v.lowalphaAmber.Resize(fyne.NewSize(float32(lowalphaAmberWidth), size.Height)) + v.lowalphaAmber.Move(fyne.NewPos(float32(lowalphaGreenWidth), 0)) + v.lowalphaRed.Resize(fyne.NewSize(float32(lowalphaRedWidth), size.Height)) + v.lowalphaRed.Move(fyne.NewPos(float32(lowalphaAmberWidth+lowalphaGreenWidth), 0)) + + v.bar.Resize(fyne.NewSize(float32(greenWidth), size.Height)) + v.optimumBar.Resize(fyne.NewSize(float32(amberWidth), size.Height)) + v.optimumBar.Move(fyne.NewPos(float32(greenWidth), 0)) + v.peakBar.Resize(fyne.NewSize(float32(redWidth), size.Height)) + v.peakBar.Move(fyne.NewPos(float32(greenWidth+amberWidth), 0)) +} + +// 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() { +} + +// 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 + + 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 - return &vuMeterRenderer{[]fyne.CanvasObject{background, bar, label}, background, bar, label, p} + objects := []fyne.CanvasObject{ + background, + lowalphaGreen, + lowalphaAmber, + lowalphaRed, + bar, + optimumBar, + peakBar, + label, + } + + return &vuRenderer{label, background, bar, optimumBar, peakBar, + lowalphaGreen, + lowalphaAmber, + lowalphaRed, objects, m} } -func (p *VUMeter) Unbind() { - p.binder.Unbind() +// 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 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) { +func (m *vuMeter) updateFromData(data binding.DataItem) { if data == nil { return } @@ -161,5 +191,38 @@ func (p *VUMeter) updateFromData(data binding.DataItem) { fyne.LogError("Error getting current data value", err) return } - p.SetValue(val) + 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) + 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 }