Add working volume bar in audioDevices page

This commit is contained in:
NeilClack
2026-05-14 10:21:11 -05:00
parent 3ea32e0eaa
commit 5188c90350
+128 -4
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
"rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/db"
@@ -21,9 +22,16 @@ type AudioDevices struct {
title StringItem title StringItem
prompt StringItem prompt StringItem
AudioStatusToggle StringItem AudioStatusToggle StringItem
volumePercent StringItem
verticalMargin int32 verticalMargin int32
ItemScale float32 ItemScale float32
CfgPtr *db.AppConfig CfgPtr *db.AppConfig
VolumeLabel StringItem
// Volume bar geometry (computed once in layout, reused in render)
volumeBarX int32
volumeBarY int32
volumeBarW int32
volumeBarH int32
} }
func (p *AudioDevices) Init(ctx *PageCtx) error { func (p *AudioDevices) Init(ctx *PageCtx) error {
@@ -69,7 +77,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
p.title.Scale(titleScale) p.title.Scale(titleScale)
// Create prompt // Create prompt
p.prompt, err = NewStringItem("Switch On/Off with Enter. Setting saves immediately.", p.font, colors.White) p.prompt, err = NewStringItem("Switch On/Off with Enter. Change volume with Left/Right (arrow keys). Setting saves immediately.", p.font, colors.White)
if err != nil { if err != nil {
return fmt.Errorf("creating prompt: %w", err) return fmt.Errorf("creating prompt: %w", err)
} }
@@ -79,6 +87,16 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
logger.Error("failed to create AudioStatusItem in AudioDevices.Init()") logger.Error("failed to create AudioStatusItem in AudioDevices.Init()")
} }
if err = p.CreateVolumePercentItem(); err != nil {
logger.Error("failed to create VolumePercentItem in AudioDevices.Init()")
}
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating volume label: %w", err)
}
p.VolumeLabel.Scale(p.ItemScale)
p.layout() p.layout()
return nil return nil
} }
@@ -98,10 +116,25 @@ func (p *AudioDevices) CreateAudioStatusItem() error {
fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID) fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID)
audioStatusStringItem.Scale(p.ItemScale) audioStatusStringItem.Scale(p.ItemScale)
p.AudioStatusToggle = audioStatusStringItem p.AudioStatusToggle = audioStatusStringItem
return nil
}
// Position the audio toggle // CreateVolumePercentItem creates (or recreates) the percentage label beside the volume bar.
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2 // Horizontal Position func (p *AudioDevices) CreateVolumePercentItem() error {
p.AudioStatusToggle.Y = p.ScreenCenterY + (p.AudioStatusToggle.H / 2) // Vertical Position // Free the old texture if one exists
if p.volumePercent.ID != 0 {
gl.DeleteTextures(1, &p.volumePercent.ID)
}
pct := int(p.Context.CfgPtr.SavedVolume * 100)
label := fmt.Sprintf("%d%%", pct)
item, err := NewStringItem(label, p.font, colors.White)
if err != nil {
return fmt.Errorf("failed to create volume percent item: %v", err)
}
item.Scale(p.ItemScale)
p.volumePercent = item
return nil return nil
} }
@@ -113,6 +146,30 @@ func (p *AudioDevices) layout() {
// Position prompt below the title // Position prompt below the title
p.prompt.X = p.ScreenCenterX - p.prompt.W/2 p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
// Position the audio toggle below the prompt, centred
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
p.AudioStatusToggle.Y = p.prompt.Y - p.verticalMargin*2 - p.AudioStatusToggle.H
// Volume bar: narrower (30% of screen width) so the right edge stays visible
barH := p.AudioStatusToggle.H
barW := int32(float32(p.ScreenWidth) * 0.30)
barX := p.ScreenCenterX - barW/2
barY := p.AudioStatusToggle.Y - p.verticalMargin*2 - barH
p.volumeBarX = barX
p.volumeBarY = barY
p.volumeBarW = barW
p.volumeBarH = barH
// "Volume" label: to the LEFT of the bar, vertically centred on it
p.VolumeLabel.X = barX - p.VolumeLabel.W - 20
p.VolumeLabel.Y = barY + (barH-p.VolumeLabel.H)/2
// Percentage label: vertically centred on the bar, small gap to the right
gap := int32(12)
p.volumePercent.X = barX + barW + gap
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
} }
func (p *AudioDevices) ToggleAudio() { func (p *AudioDevices) ToggleAudio() {
@@ -127,6 +184,7 @@ func (p *AudioDevices) ToggleAudio() {
// Update AudioEnabled UI element // Update AudioEnabled UI element
p.CreateAudioStatusItem() p.CreateAudioStatusItem()
p.layout()
} }
// UpdateVolume increments or decrements system volume by level (percent change) // UpdateVolume increments or decrements system volume by level (percent change)
@@ -152,6 +210,14 @@ func (p *AudioDevices) UpdateVolume(level int) {
fmt.Printf("Error setting allSinkVolumes: %v", err) fmt.Printf("Error setting allSinkVolumes: %v", err)
} }
// Refresh volume label
if err := p.CreateVolumePercentItem(); err != nil {
logger.Error("failed to refresh volume percent item: %v", err)
}
// Re-position the label (bar geometry is unchanged, but label width may differ)
gap := int32(12)
p.volumePercent.X = p.volumeBarX + p.volumeBarW + gap
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
} }
func (p *AudioDevices) HandleEvent(event *sdl.Event) error { func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
@@ -174,6 +240,47 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
return nil return nil
} }
// renderVolumeBar draws the green outline and the white filled portion.
func (p *AudioDevices) renderVolumeBar() error {
vol := p.Context.CfgPtr.SavedVolume
// --- White fill (clipped to current volume fraction) ---
fillW := float32(p.volumeBarW) * float32(vol)
if fillW > 0 {
fillPos := mgl32.Vec2{float32(p.volumeBarX), float32(p.volumeBarY)}
fillDim := mgl32.Vec2{fillW, float32(p.volumeBarH)}
if err := p.RenderSolidColorQuad(p.solidShader, fillPos, fillDim, colors.White); err != nil {
return fmt.Errorf("rendering volume fill: %w", err)
}
}
// --- Green outline (4 x thin quads: top, bottom, left, right) ---
thickness := float32(2)
bx := float32(p.volumeBarX)
by := float32(p.volumeBarY)
bw := float32(p.volumeBarW)
bh := float32(p.volumeBarH)
outlineRects := [4][2]mgl32.Vec2{
// bottom edge
{{bx, by}, {bw, thickness}},
// top edge
{{bx, by + bh - thickness}, {bw, thickness}},
// left edge
{{bx, by}, {thickness, bh}},
// right edge
{{bx + bw - thickness}, {thickness, bh}},
}
for _, r := range outlineRects {
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
return fmt.Errorf("rendering volume outline edge: %w", err)
}
}
return nil
}
func (p *AudioDevices) Render() error { func (p *AudioDevices) Render() error {
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil { if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
logger.Error("unable to render title texture: %v", err) logger.Error("unable to render title texture: %v", err)
@@ -190,11 +297,28 @@ func (p *AudioDevices) Render() error {
return fmt.Errorf("unable to render audioStatusToggle texture: %w", err) return fmt.Errorf("unable to render audioStatusToggle texture: %w", err)
} }
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
logger.Error("unable to render volume label texture: %v", err)
return fmt.Errorf("unable to render volume label texture: %w", err)
}
if err := p.renderVolumeBar(); err != nil {
logger.Error("unable to render volume bar: %v", err)
return fmt.Errorf("unable to render volume bar: %w", err)
}
if err := p.RenderTexture(p.textShader, p.volumePercent.ID, p.volumePercent.Position(), p.volumePercent.RawDimensions(), p.volumePercent.Color); err != nil {
logger.Error("unable to render volume percent texture: %v", err)
return fmt.Errorf("unable to render volume percent texture: %w", err)
}
return nil return nil
} }
func (p *AudioDevices) Destroy() error { func (p *AudioDevices) Destroy() error {
gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.title.ID)
gl.DeleteTextures(1, &p.prompt.ID) gl.DeleteTextures(1, &p.prompt.ID)
gl.DeleteTextures(1, &p.volumePercent.ID)
gl.DeleteTextures(1, &p.VolumeLabel.ID)
return nil return nil
} }