create waveform seekbar widget; still more to do

This commit is contained in:
Drew Weymouth
2025-07-24 08:51:25 -07:00
parent ae7cb55c8c
commit 43f6d501c9
5 changed files with 140 additions and 29 deletions
+14 -1
View File
@@ -1,6 +1,7 @@
package widgets
import (
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/ui/util"
"fyne.io/fyne/v2"
@@ -82,6 +83,7 @@ type PlayerControls struct {
widget.BaseWidget
slider *TrackPosSlider
waveform *WaveformSeekbar
curTimeLabel *labelMinSize
totalTimeLabel *labelMinSize
prev *IconButton
@@ -116,6 +118,8 @@ func NewPlayerControls() *PlayerControls {
pc.slider = NewTrackPosSlider()
pc.slider.Disable()
pc.slider.Hide()
pc.waveform = NewWaveformSeekbar()
pc.curTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55)
pc.curTimeLabel.Alignment = fyne.TextAlignTrailing
pc.totalTimeLabel = NewLabelMinSize(util.SecondsToMMSS(0), 55)
@@ -138,7 +142,11 @@ func NewPlayerControls() *PlayerControls {
buttons := container.NewHBox(layout.NewSpacer(), pc.prev, pc.playpause, pc.next, layout.NewSpacer())
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider)
seekCtrl := container.NewStack(
pc.slider,
pc.waveform,
)
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, seekCtrl)
pc.container = container.New(layout.NewCustomPaddedVBoxLayout(0), c, buttons)
return pc
@@ -203,10 +211,15 @@ func (pc *PlayerControls) UpdatePlayTime(curTime, totalTime float64) {
if updated {
// Only update slider once a second when time label changes
pc.slider.SetValue(v)
pc.waveform.SetProgress(v)
}
}
}
func (p *PlayerControls) UpdateWaveformImg(img *backend.WaveformImage) {
p.waveform.UpdateImage(img)
}
func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(p.container)
}
+109
View File
@@ -0,0 +1,109 @@
package widgets
import (
"image"
"image/color"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend"
)
type WaveformSeekbar struct {
widget.BaseWidget
imgColorL color.Color
imgColorR color.Color
imgProgress float64
img *canvas.Image
}
func NewWaveformSeekbar() *WaveformSeekbar {
w := &WaveformSeekbar{
img: canvas.NewImageFromImage(nil),
}
w.ExtendBaseWidget(w)
return w
}
func (w *WaveformSeekbar) UpdateImage(img *backend.WaveformImage) {
w.img.Image = img
w.Refresh()
}
func (w *WaveformSeekbar) Refresh() {
prm, fg := w.getThemeColors()
w.recolorImage(prm, fg, w.imgProgress)
w.img.Refresh()
//w.BaseWidget.Refresh()
}
func (w *WaveformSeekbar) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(
container.New(layout.NewCustomPaddedLayout(4, 4, 0, 0), w.img),
)
}
// SetProgress sets how much of the seekbar has been played
// (ratio from 0 to 1)
func (w *WaveformSeekbar) SetProgress(v float64) {
prm, fg := w.getThemeColors()
w.recolorImage(prm, fg, v)
w.Refresh()
}
func (w *WaveformSeekbar) getThemeColors() (primary, foreground color.Color) {
th := w.Theme()
vnt := fyne.CurrentApp().Settings().ThemeVariant()
primary = th.Color(theme.ColorNamePrimary, vnt)
foreground = th.Color(theme.ColorNameForeground, vnt)
return primary, foreground
}
func (w *WaveformSeekbar) recolorImage(cL, cR color.Color, progress float64) {
if w.img.Image == nil {
return
}
if w.imgColorL == cL && w.imgColorR == cR && w.imgProgress == progress {
return
}
_r, _g, _b, _ := cL.RGBA()
rL, gL, bL := byte(_r>>8), byte(_g>>8), byte(_b>>8)
_r, _g, _b, _ = cR.RGBA()
rR, gR, bR := byte(_r>>8), byte(_g>>8), byte(_b>>8)
// TODO- smartly figure out which pixels we need
// to update for different scenarios (e.g. progress update only)
// and not iterate the whole thing every time
img := w.img.Image.(*image.NRGBA)
bnds := img.Rect.Bounds()
thresholdPixel := int(float64(bnds.Dx()) * progress)
log.Println("progress = ", progress, " bounds Dx = ", bnds.Dx())
log.Println("thresholdPixel = ", thresholdPixel)
for x := 0; x < bnds.Dx(); x++ {
for y := 0; y < bnds.Dy(); y++ {
if x < thresholdPixel {
setPixelRGB(img, x, y, rL, gL, bL)
} else {
setPixelRGB(img, x, y, rR, gR, bR)
}
}
}
w.imgColorL, w.imgColorR = cL, cR
w.imgProgress = progress
}
func setPixelRGB(img *image.NRGBA, x, y int, r, g, b byte) {
offset := img.PixOffset(x, y)
img.Pix[offset+0] = r
img.Pix[offset+1] = g
img.Pix[offset+2] = b
}