initial commit
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// focusToggle is the sentinel focusState value meaning the audio on/off toggle
|
||||
// row has keyboard focus. Device indices (0..n-1) are used for everything below.
|
||||
const focusToggle = -1
|
||||
|
||||
// AudioFileList is kept as the canonical seed data for the audio_files table.
|
||||
// Track selection UI no longer lives on this page, but other parts of the
|
||||
// app still read these rows from the database.
|
||||
var AudioFileList = []db.AudioFile{
|
||||
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
||||
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
||||
{ID: 2, Name: "396 Hz", Path: "396Hz.wav", Selected: false, Order: 3},
|
||||
{ID: 3, Name: "417 Hz", Path: "417Hz.wav", Selected: false, Order: 4},
|
||||
{ID: 4, Name: "432 Hz", Path: "432Hz.wav", Selected: false, Order: 5},
|
||||
{ID: 5, Name: "639 Hz", Path: "639Hz.wav", Selected: false, Order: 6},
|
||||
{ID: 6, Name: "741 Hz", Path: "741Hz.wav", Selected: false, Order: 7},
|
||||
{ID: 7, Name: "852 Hz", Path: "852Hz.wav", Selected: false, Order: 8},
|
||||
{ID: 8, Name: "963 Hz", Path: "963Hz.wav", Selected: false, Order: 9},
|
||||
{ID: 9, Name: "1111 Hz", Path: "1111Hz.wav", Selected: false, Order: 10},
|
||||
{ID: 10, Name: "14 Hz Binaural Beat", Path: "14Hz_Binaural.wav", Selected: false, Order: 11},
|
||||
{ID: 11, Name: "40 Hz Binaural Beat", Path: "40Hz_Binaural.wav", Selected: false, Order: 12},
|
||||
{ID: 12, Name: "Alpha Waves", Path: "AlphaWaves.wav", Selected: false, Order: 13},
|
||||
{ID: 13, Name: "Pineal Gland", Path: "PinealGland.wav", Selected: false, Order: 14},
|
||||
}
|
||||
|
||||
const triangleSize = float32(16)
|
||||
|
||||
type AudioDevice struct {
|
||||
Label string
|
||||
ID uint32
|
||||
index int
|
||||
}
|
||||
|
||||
type AudioOptions struct {
|
||||
// Page Attributes
|
||||
Base
|
||||
font *fontManager.FontEntry
|
||||
textShader *shaderManager.Shader
|
||||
solidShader *shaderManager.Shader
|
||||
CfgPtr *db.AppConfig
|
||||
|
||||
// Page Textures
|
||||
title StringItem
|
||||
prompt StringItem
|
||||
|
||||
// Layout
|
||||
verticalMargin int32
|
||||
ItemScale float32
|
||||
|
||||
// Audio toggle
|
||||
AudioStatusToggle StringItem
|
||||
|
||||
// Audio Devices
|
||||
activeDeviceIndex int
|
||||
NumAudioDevices int
|
||||
AudioDevices []AudioDevice
|
||||
AudioDeviceTextures []StringItem
|
||||
|
||||
// Volume row
|
||||
VolumeLabel StringItem
|
||||
volumePercent StringItem
|
||||
volumeBarX int32
|
||||
volumeBarY int32
|
||||
volumeBarW int32
|
||||
volumeBarH int32
|
||||
|
||||
// Triangle cursor
|
||||
triVAO uint32
|
||||
triVBO uint32
|
||||
triX float32
|
||||
triCenterY float32
|
||||
spinElapsed float32
|
||||
|
||||
// Focus: focusToggle (-1) = toggle row, 0..n-1 = device at that index
|
||||
focusState int
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||
var err error
|
||||
p.verticalMargin = int32(20)
|
||||
p.focusState = focusToggle
|
||||
|
||||
// Font
|
||||
fm := fontManager.Get()
|
||||
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get font: %w", sdl.GetError())
|
||||
}
|
||||
p.font = font
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
if err = p.Base.Init(ctx); err != nil {
|
||||
return fmt.Errorf("base init failed: %w", err)
|
||||
}
|
||||
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
baseFontSize := config.StandardFontSize
|
||||
titleScale := float32(64) / float32(baseFontSize)
|
||||
p.ItemScale = float32(32) / float32(baseFontSize)
|
||||
|
||||
// Title
|
||||
p.title, err = NewStringItem("Audio Options", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating title: %w", err)
|
||||
}
|
||||
p.title.Scale(titleScale)
|
||||
|
||||
// Prompt
|
||||
p.prompt, err = NewStringItem(
|
||||
"Enter to select -- Up/Down to navigate -- Left/Right to change volume -- Backspace to go back",
|
||||
p.font, colors.White,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating prompt: %w", err)
|
||||
}
|
||||
p.prompt.Scale(p.ItemScale)
|
||||
|
||||
// Volume row
|
||||
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)
|
||||
if err = p.CreateVolumePercentItem(); err != nil {
|
||||
logger.Error("failed to create VolumePercentItem in AudioOptions.Init()")
|
||||
}
|
||||
|
||||
// Audio toggle
|
||||
if err = p.CreateAudioStatusItem(); err != nil {
|
||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
||||
}
|
||||
|
||||
// Audio output devices
|
||||
if err = p.GetAudioDevices(); err != nil {
|
||||
logger.Error("failed to enumerate audio devices: %v", err)
|
||||
}
|
||||
|
||||
// Set active device index based on mixer's current device
|
||||
currentName := mixer.CurrentDevice()
|
||||
config, err = db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p.activeDeviceIndex = -1 // default to not found
|
||||
for i, dev := range p.AudioDevices {
|
||||
if dev.Label == currentName {
|
||||
p.activeDeviceIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// If not found and current device is default (""), check saved config
|
||||
if p.activeDeviceIndex == -1 && currentName == "" && config.SelectedAudioDevice == "default" {
|
||||
// Default device is selected - first device in SDL list is typically the default
|
||||
if len(p.AudioDevices) > 0 {
|
||||
p.activeDeviceIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the audio_files table is populated.
|
||||
if err = p.seedAudioFiles(); err != nil {
|
||||
logger.Error("failed to seed audio files: %v", err)
|
||||
}
|
||||
|
||||
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
|
||||
triVerts := []float32{
|
||||
0.5, 0.0,
|
||||
-0.5, 0.5,
|
||||
-0.5, -0.5,
|
||||
}
|
||||
gl.GenVertexArrays(1, &p.triVAO)
|
||||
gl.GenBuffers(1, &p.triVBO)
|
||||
gl.BindVertexArray(p.triVAO)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, p.triVBO)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(triVerts)*4, gl.Ptr(triVerts), gl.STATIC_DRAW)
|
||||
gl.EnableVertexAttribArray(0)
|
||||
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 2*4, 0)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
gl.BindVertexArray(0)
|
||||
|
||||
p.layout()
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedAudioFiles ensures the audio_files table has its default rows.
|
||||
func (p *AudioOptions) seedAudioFiles() error {
|
||||
var existing []db.AudioFile
|
||||
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
return p.Context.DbPtr.Save(&AudioFileList).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Audio status toggle ───────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) CreateAudioStatusItem() error {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
on := config.AudioEnabled
|
||||
statusString := "Turn Audio On"
|
||||
color := colors.Green
|
||||
if on {
|
||||
statusString = "Turn Audio Off"
|
||||
color = colors.Red
|
||||
}
|
||||
item, err := NewStringItem(statusString, p.font, color)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create '%s' string item: %v", statusString, err)
|
||||
}
|
||||
item.Scale(p.ItemScale)
|
||||
p.AudioStatusToggle = item
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Audio Output Devices ──────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) GetAudioDevices() error {
|
||||
numAudioDevices := sdl.GetNumAudioDevices(false)
|
||||
if numAudioDevices == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre-allocate with zero length so append doesn't produce leading zero-value
|
||||
// elements (the old code used make([]AudioDevice, n) then appended, which
|
||||
// doubled the slice length with n zero-value entries at the front).
|
||||
audioDevices := make([]AudioDevice, 0, numAudioDevices)
|
||||
for i := range numAudioDevices {
|
||||
label := sdl.GetAudioDeviceName(i, false)
|
||||
audioDevices = append(audioDevices, AudioDevice{Label: label, ID: uint32(i)})
|
||||
}
|
||||
p.AudioDevices = audioDevices
|
||||
|
||||
for i := range audioDevices {
|
||||
item, err := NewStringItem(p.AudioDevices[i].Label, p.font, colors.White)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.Scale(p.ItemScale)
|
||||
p.AudioDeviceTextures = append(p.AudioDeviceTextures, item)
|
||||
}
|
||||
p.NumAudioDevices = numAudioDevices
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectAudioDevice is called when the user confirms a device from the list.
|
||||
// The device-switching implementation goes here; for now we just log the choice.
|
||||
func (p *AudioOptions) SelectAudioDevice(deviceIndex int) error {
|
||||
if deviceIndex < 0 || deviceIndex >= len(p.AudioDevices) {
|
||||
return fmt.Errorf("invalid device index: %d", deviceIndex)
|
||||
}
|
||||
device := p.AudioDevices[deviceIndex]
|
||||
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config for device switch: %w", err)
|
||||
}
|
||||
|
||||
if err := mixer.SwitchDevice(device.Label, config); err != nil {
|
||||
return fmt.Errorf("failed to switch to device %s: %w", device.Label, err)
|
||||
}
|
||||
|
||||
// Persist the selected device to the database
|
||||
config.SelectedAudioDevice = device.Label
|
||||
if err := p.Context.DbPtr.Save(config).Error; err != nil {
|
||||
logger.Error("failed to persist device selection: %v", err)
|
||||
}
|
||||
|
||||
p.activeDeviceIndex = deviceIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Volume ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) CreateVolumePercentItem() error {
|
||||
if p.volumePercent.ID != 0 {
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
}
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pct := int(config.SavedVolume * 100)
|
||||
item, err := NewStringItem(fmt.Sprintf("%d%%", pct), 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) UpdateVolume(level int) {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newVol := mixer.ConvertVolume(level) + config.SavedVolume
|
||||
if newVol < 0.0 || newVol > 1.0 {
|
||||
return
|
||||
}
|
||||
if newVol > .99 {
|
||||
newVol = 1.0
|
||||
}
|
||||
config.SavedVolume = newVol
|
||||
p.Context.DbPtr.Save(&config)
|
||||
if err := mixer.SetAllSinkVolumes(newVol); err != nil {
|
||||
logger.Error("Error setting allSinkVolumes: %v", err)
|
||||
}
|
||||
if err := p.CreateVolumePercentItem(); err != nil {
|
||||
logger.Error("failed to refresh volume percent item: %v", err)
|
||||
}
|
||||
gap := int32(12)
|
||||
p.volumePercent.X = p.volumeBarX + p.volumeBarW + gap
|
||||
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
|
||||
}
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) layout() {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Title
|
||||
p.title.X = p.ScreenCenterX - p.title.W/2
|
||||
p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2
|
||||
|
||||
// Prompt
|
||||
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
||||
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
|
||||
|
||||
// Volume row
|
||||
barH := int32(float32(config.StandardFontSize) * p.ItemScale)
|
||||
barW := int32(float32(p.ScreenWidth) * 0.30)
|
||||
barX := p.ScreenCenterX - barW/2
|
||||
barY := p.prompt.Y - p.verticalMargin*2 - barH
|
||||
|
||||
p.volumeBarX = barX
|
||||
p.volumeBarY = barY
|
||||
p.volumeBarW = barW
|
||||
p.volumeBarH = barH
|
||||
|
||||
p.VolumeLabel.X = barX - p.VolumeLabel.W - 20
|
||||
p.VolumeLabel.Y = barY + (barH-p.VolumeLabel.H)/2
|
||||
|
||||
gap := int32(12)
|
||||
p.volumePercent.X = barX + barW + gap
|
||||
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
||||
|
||||
// Audio toggle — centred below the volume row
|
||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
||||
|
||||
// Device list — each item centred and stacked below the toggle
|
||||
nextY := p.AudioStatusToggle.Y - p.verticalMargin
|
||||
for i := range p.AudioDeviceTextures {
|
||||
item := &p.AudioDeviceTextures[i]
|
||||
item.X = p.ScreenCenterX - item.W/2
|
||||
item.Y = nextY - item.H
|
||||
nextY = item.Y - p.verticalMargin
|
||||
}
|
||||
|
||||
// Reposition the triangle cursor for whatever row currently has focus
|
||||
p.updateCursorPosition()
|
||||
}
|
||||
|
||||
// updateCursorPosition moves triX / triCenterY to sit just left of the
|
||||
// focused row. Call this any time focusState changes or layout() reruns.
|
||||
func (p *AudioOptions) updateCursorPosition() {
|
||||
cursorGap := int32(12)
|
||||
|
||||
var item *StringItem
|
||||
switch {
|
||||
case p.focusState == focusToggle:
|
||||
item = &p.AudioStatusToggle
|
||||
case p.focusState >= 0 && p.focusState < len(p.AudioDeviceTextures):
|
||||
item = &p.AudioDeviceTextures[p.focusState]
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
p.triX = float32(item.X) - triangleSize - float32(cursorGap)
|
||||
p.triCenterY = float32(item.Y) + float32(item.H)/2
|
||||
}
|
||||
|
||||
// ── Toggle ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) ToggleAudio() {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
logger.Error("panic! Cannot query database for configuration")
|
||||
panic(err)
|
||||
}
|
||||
config.AudioEnabled = !config.AudioEnabled
|
||||
p.Context.DbPtr.Save(config)
|
||||
p.CreateAudioStatusItem()
|
||||
p.layout()
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────────
|
||||
|
||||
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:
|
||||
if p.focusState == focusToggle {
|
||||
p.ToggleAudio()
|
||||
} else if p.focusState >= 0 && p.focusState < len(p.AudioDevices) {
|
||||
if err := p.SelectAudioDevice(p.focusState); err != nil {
|
||||
logger.Error("SelectAudioDevice: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case sdl.K_DOWN:
|
||||
if p.focusState == focusToggle {
|
||||
if len(p.AudioDeviceTextures) > 0 {
|
||||
p.focusState = 0
|
||||
p.updateCursorPosition()
|
||||
}
|
||||
} else if p.focusState < len(p.AudioDeviceTextures)-1 {
|
||||
p.focusState++
|
||||
p.updateCursorPosition()
|
||||
}
|
||||
|
||||
case sdl.K_UP:
|
||||
if p.focusState == 0 {
|
||||
p.focusState = focusToggle
|
||||
p.updateCursorPosition()
|
||||
} else if p.focusState > 0 {
|
||||
p.focusState--
|
||||
p.updateCursorPosition()
|
||||
}
|
||||
|
||||
case sdl.K_RIGHT:
|
||||
p.UpdateVolume(10)
|
||||
|
||||
case sdl.K_LEFT:
|
||||
p.UpdateVolume(-10)
|
||||
|
||||
case sdl.K_BACKSPACE:
|
||||
p.Context.NavigateTo(SETTINGS)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) Update(dt float32) error {
|
||||
p.spinElapsed += dt
|
||||
if p.spinElapsed >= 1.0 {
|
||||
p.spinElapsed -= 1.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Render helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) renderVolumeBar() error {
|
||||
config, err := db.GetConfig(p.Context.DbPtr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vol := config.SavedVolume
|
||||
|
||||
fillW := float32(p.volumeBarW) * float32(vol)
|
||||
if fillW > 0 {
|
||||
if err := p.RenderSolidColorQuad(
|
||||
p.solidShader,
|
||||
mgl32.Vec2{float32(p.volumeBarX), float32(p.volumeBarY)},
|
||||
mgl32.Vec2{fillW, float32(p.volumeBarH)},
|
||||
colors.White,
|
||||
); err != nil {
|
||||
return fmt.Errorf("rendering volume fill: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
thickness := float32(2)
|
||||
bx := float32(p.volumeBarX)
|
||||
by := float32(p.volumeBarY)
|
||||
bw := float32(p.volumeBarW)
|
||||
bh := float32(p.volumeBarH)
|
||||
|
||||
outlineRects := [4][2]mgl32.Vec2{
|
||||
{{bx, by}, {bw, thickness}},
|
||||
{{bx, by + bh - thickness}, {bw, thickness}},
|
||||
{{bx, by}, {thickness, bh}},
|
||||
{{bx + bw - thickness, by}, {thickness, bh}}, // fix: was missing `by` as Y component
|
||||
}
|
||||
for _, r := range outlineRects {
|
||||
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
|
||||
return fmt.Errorf("rendering volume outline: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCursor draws the spinning triangle cursor next to the focused row.
|
||||
func (p *AudioOptions) renderCursor() error {
|
||||
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
||||
|
||||
model := mgl32.Ident4()
|
||||
model = model.Mul4(mgl32.Translate3D(p.triX+triangleSize/2, p.triCenterY, 0))
|
||||
model = model.Mul4(mgl32.Scale3D(triangleSize, triangleSize*scaleY, 1))
|
||||
mvp := p.Projection.Mul4(model)
|
||||
|
||||
gl.UseProgram(p.solidShader.ProgramID)
|
||||
gl.UniformMatrix4fv(p.solidShader.LocMVPMatrix, 1, false, &mvp[0])
|
||||
gl.Uniform4f(
|
||||
p.solidShader.LocColor,
|
||||
float32(colors.White.R)/255.0,
|
||||
float32(colors.White.G)/255.0,
|
||||
float32(colors.White.B)/255.0,
|
||||
float32(colors.White.A)/255.0,
|
||||
)
|
||||
|
||||
gl.BindVertexArray(p.triVAO)
|
||||
gl.DrawArrays(gl.TRIANGLES, 0, 3)
|
||||
gl.BindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("render title: %w", err)
|
||||
}
|
||||
if err := p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color); err != nil {
|
||||
return fmt.Errorf("render prompt: %w", err)
|
||||
}
|
||||
|
||||
// Volume row
|
||||
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
|
||||
return fmt.Errorf("render volume label: %w", err)
|
||||
}
|
||||
if err := p.renderVolumeBar(); err != nil {
|
||||
return fmt.Errorf("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 {
|
||||
return fmt.Errorf("render volume percent: %w", err)
|
||||
}
|
||||
|
||||
// Toggle
|
||||
if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil {
|
||||
return fmt.Errorf("render audio toggle: %w", err)
|
||||
}
|
||||
|
||||
// Device list
|
||||
for i := range p.AudioDeviceTextures {
|
||||
item := &p.AudioDeviceTextures[i]
|
||||
color := colors.White
|
||||
if i == p.activeDeviceIndex {
|
||||
color = colors.Green
|
||||
}
|
||||
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), color); err != nil {
|
||||
return fmt.Errorf("render audio device %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Spinning triangle cursor — always on top
|
||||
if err := p.renderCursor(); err != nil {
|
||||
return fmt.Errorf("render cursor: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── Destroy ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AudioOptions) Destroy() error {
|
||||
gl.DeleteTextures(1, &p.title.ID)
|
||||
gl.DeleteTextures(1, &p.prompt.ID)
|
||||
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
||||
for i := range p.AudioDeviceTextures {
|
||||
gl.DeleteTextures(1, &p.AudioDeviceTextures[i].ID)
|
||||
}
|
||||
gl.DeleteBuffers(1, &p.triVBO)
|
||||
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user