Rename audio devices to audio options
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"rhemawavetech.com/rhemawave/internal/colors"
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
"rhemawavetech.com/rhemawave/internal/fontManager"
|
||||
"rhemawavetech.com/rhemawave/internal/logger"
|
||||
"rhemawavetech.com/rhemawave/internal/mixer"
|
||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||
)
|
||||
|
||||
type AudioOptions struct {
|
||||
Base
|
||||
font *fontManager.FontEntry
|
||||
textShader *shaderManager.Shader
|
||||
solidShader *shaderManager.Shader
|
||||
title StringItem
|
||||
prompt StringItem
|
||||
AudioStatusToggle StringItem
|
||||
volumePercent StringItem
|
||||
verticalMargin int32
|
||||
ItemScale float32
|
||||
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 *AudioOptions) Init(ctx *PageCtx) error {
|
||||
var err error
|
||||
p.verticalMargin = int32(20)
|
||||
|
||||
// Get font
|
||||
fm := fontManager.Get()
|
||||
font, ok := fm.GetFont("Roboto-Regular")
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get font: %w", sdl.GetError())
|
||||
}
|
||||
p.font = font
|
||||
|
||||
// Get shaders
|
||||
sm := shaderManager.Get()
|
||||
p.textShader, ok = sm.Get("text")
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get text shader: %w", sdl.GetError())
|
||||
}
|
||||
p.solidShader, ok = sm.Get("solid_color")
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get solid_color shader: %w", sdl.GetError())
|
||||
}
|
||||
|
||||
// Initialize base
|
||||
if err := p.Base.Init(ctx); err != nil {
|
||||
return fmt.Errorf("base init failed: %w", err)
|
||||
}
|
||||
|
||||
// Get font size from database
|
||||
baseFontSize := p.Context.CfgPtr.StandardFontSize
|
||||
|
||||
// Calculate scales
|
||||
titleScale := float32(64) / float32(baseFontSize)
|
||||
p.ItemScale = float32(32) / float32(baseFontSize)
|
||||
|
||||
// Create title
|
||||
p.title, err = NewStringItem("Toggle Audio", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating title: %w", err)
|
||||
}
|
||||
p.title.Scale(titleScale)
|
||||
|
||||
// Create prompt
|
||||
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 {
|
||||
return fmt.Errorf("creating prompt: %w", err)
|
||||
}
|
||||
p.prompt.Scale(p.ItemScale)
|
||||
|
||||
if err = p.CreateAudioStatusItem(); err != nil {
|
||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
||||
}
|
||||
|
||||
if err = p.CreateVolumePercentItem(); err != nil {
|
||||
logger.Error("failed to create VolumePercentItem in AudioOptions.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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AudioOptions) CreateAudioStatusItem() error {
|
||||
on := p.Context.CfgPtr.AudioEnabled
|
||||
statusString := "Turn Audio Off" // The audio is ON
|
||||
color := colors.Green
|
||||
if !on {
|
||||
statusString = "Turn Audio on"
|
||||
color = colors.Red
|
||||
}
|
||||
audioStatusStringItem, err := NewStringItem(statusString, p.font, color)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create '%s' string item: %v", statusString, err)
|
||||
}
|
||||
fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID)
|
||||
audioStatusStringItem.Scale(p.ItemScale)
|
||||
p.AudioStatusToggle = audioStatusStringItem
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateVolumePercentItem creates (or recreates) the percentage label beside the volume bar.
|
||||
func (p *AudioOptions) CreateVolumePercentItem() error {
|
||||
// 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
|
||||
}
|
||||
|
||||
func (p *AudioOptions) layout() {
|
||||
// Position title at the top
|
||||
p.title.X = p.ScreenCenterX - p.title.W/2
|
||||
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
|
||||
|
||||
// Position prompt below the title
|
||||
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
||||
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 *AudioOptions) ToggleAudio() {
|
||||
// Toggle current setting
|
||||
p.Context.CfgPtr.AudioEnabled = !p.Context.CfgPtr.AudioEnabled
|
||||
|
||||
// Update the database
|
||||
p.Context.DbPtr.Save(p.Context.CfgPtr)
|
||||
|
||||
// Update the configuration
|
||||
p.Context.RefreshConfig()
|
||||
|
||||
// Update AudioEnabled UI element
|
||||
p.CreateAudioStatusItem()
|
||||
p.layout()
|
||||
}
|
||||
|
||||
// UpdateVolume increments or decrements system volume by level (percent change)
|
||||
func (p *AudioOptions) UpdateVolume(level int) {
|
||||
// Calculate the new volume
|
||||
newVol := mixer.ConvertVolume(level) + p.Context.CfgPtr.SavedVolume
|
||||
// Check that it's within bounds
|
||||
if newVol < 0.0 || newVol > 1.0 {
|
||||
return
|
||||
}
|
||||
// Force 1.0 rather than 0.99999999999999989
|
||||
if newVol > .99 {
|
||||
newVol = 1.0
|
||||
}
|
||||
// Update the config
|
||||
p.Context.CfgPtr.SavedVolume = newVol
|
||||
fmt.Printf("New Volume: %f", newVol)
|
||||
// Update the database
|
||||
p.Context.DbPtr.Save(&p.Context.CfgPtr)
|
||||
// Set the new volume
|
||||
err := mixer.SetAllSinkVolumes(newVol)
|
||||
if err != nil {
|
||||
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 *AudioOptions) HandleEvent(event *sdl.Event) error {
|
||||
e, ok := (*event).(*sdl.KeyboardEvent)
|
||||
if !ok || e.Type != sdl.KEYDOWN {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch e.Keysym.Sym {
|
||||
case sdl.K_RETURN:
|
||||
p.ToggleAudio()
|
||||
case sdl.K_RIGHT: // Right increase
|
||||
p.UpdateVolume(10)
|
||||
case sdl.K_LEFT: // Left decrease
|
||||
p.UpdateVolume(-10)
|
||||
case sdl.K_BACKSPACE:
|
||||
p.Context.NavigateTo(SETTINGS)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderVolumeBar draws the green outline and the white filled portion.
|
||||
func (p *AudioOptions) 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 *AudioOptions) Render() error {
|
||||
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)
|
||||
return fmt.Errorf("unable to render title texture: %w", err)
|
||||
}
|
||||
|
||||
if err := p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color); err != nil {
|
||||
logger.Error("unable to render prompt texture: %v", err)
|
||||
return fmt.Errorf("unable to render prompt texture: %w", err)
|
||||
}
|
||||
|
||||
if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil {
|
||||
logger.Error("unable to render audioStatusToggle texture: %v", 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
|
||||
}
|
||||
|
||||
func (p *AudioOptions) Destroy() error {
|
||||
gl.DeleteTextures(1, &p.title.ID)
|
||||
gl.DeleteTextures(1, &p.prompt.ID)
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user