Remove track list from AudioOptions

This commit is contained in:
NeilClack
2026-06-18 23:01:11 -05:00
parent 1c814f4abb
commit 9ee7e8021a
+25 -350
View File
@@ -1,16 +1,12 @@
package page
import (
"errors"
"fmt"
"math"
"path/filepath"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/mathgl/mgl32"
"github.com/veandco/go-sdl2/sdl"
"gorm.io/gorm"
"rhemawavetech.com/rhemawave/internal/colors"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/fontManager"
@@ -19,6 +15,9 @@ import (
"rhemawavetech.com/rhemawave/internal/shaderManager"
)
// 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},
@@ -36,13 +35,7 @@ var AudioFileList = []db.AudioFile{
{ID: 13, Name: "Pineal Gland", Path: "PinealGland.wav", Selected: false, Order: 14},
}
const (
triangleSize = float32(16)
visibleCount = 9
listShiftDown = int32(50)
scrollbarWidth = int32(4)
scrollbarGap = int32(16) // gap between right edge of list and scrollbar
)
const triangleSize = float32(16)
type AudioOptions struct {
Base
@@ -58,7 +51,7 @@ type AudioOptions struct {
// Audio toggle
AudioStatusToggle StringItem
// Volume row (excluded from focus)
// Volume row
VolumeLabel StringItem
volumePercent StringItem
volumeBarX int32
@@ -66,33 +59,6 @@ type AudioOptions struct {
volumeBarW int32
volumeBarH int32
// Separator + track section
separatorY int32
tracksHeading StringItem
trackItems []StringItem
// Selected track (-1 = none)
selectedTrack int
// Navigation
// focusedIndex: 0 = AudioStatusToggle, 1..N = trackItems[focusedIndex-1]
// Volume is intentionally excluded from focus.
focusedIndex int
// Scrolling window
windowStart int
listTopY int32
listLeftX int32
itemHeight int32
trackSpacing int32
// Scrollbar geometry (computed once in layout, thumb Y updated on scroll)
scrollbarX int32
scrollbarTrackY int32 // bottom of the scrollbar track
scrollbarTrackH int32 // total height of the track
scrollbarThumbH int32 // height of the thumb
scrollbarThumbY int32 // current bottom Y of the thumb (updated on scroll)
// Triangle cursor
triVAO uint32
triVBO uint32
@@ -106,10 +72,6 @@ type AudioOptions struct {
func (p *AudioOptions) Init(ctx *PageCtx) error {
var err error
p.verticalMargin = int32(20)
p.focusedIndex = 0
p.selectedTrack = -1
p.windowStart = 0
p.trackSpacing = p.verticalMargin / 2
// Font
fm := fontManager.Get()
@@ -146,7 +108,7 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
p.title.Scale(titleScale)
// Prompt
p.prompt, err = NewStringItem("Up/Down to navigate -- Enter to select -- Left/Right to change volume -- Backspace to go back", p.font, colors.White)
p.prompt, err = NewStringItem("Enter to toggle audio -- Left/Right to change volume -- Backspace to go back", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating prompt: %w", err)
}
@@ -167,17 +129,11 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
}
// "Available Tracks" heading
headingScale := float32(40) / float32(baseFontSize)
p.tracksHeading, err = NewStringItem("Available Tracks", p.font, colors.White)
if err != nil {
return fmt.Errorf("creating tracks heading: %w", err)
}
p.tracksHeading.Scale(headingScale)
// Track items
if err = p.buildTrackItems(); err != nil {
return fmt.Errorf("building track items: %w", err)
// Make sure the audio_files table is populated. This page no longer
// presents any track selection UI, but other parts of the app still
// read from this table.
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
@@ -200,84 +156,18 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
return nil
}
// ── Track items ───────────────────────────────────────────────────────────────
func (p *AudioOptions) buildTrackItems() error {
var audioFileList = []db.AudioFile{}
if err := p.Context.DbPtr.Find(&audioFileList).Error; err != 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(audioFileList) == 0 {
audioFileList = AudioFileList
p.Context.DbPtr.Save(&audioFileList)
}
for i := range p.trackItems {
if p.trackItems[i].ID != 0 {
gl.DeleteTextures(1, &p.trackItems[i].ID)
}
}
p.trackItems = make([]StringItem, len(AudioFileList))
for i, entry := range AudioFileList {
color := colors.White
if i == p.selectedTrack {
color = colors.Green
}
item, err := NewStringItem(entry.Name, p.font, color)
if err != nil {
return fmt.Errorf("creating track item %d: %w", i, err)
}
item.Scale(p.ItemScale)
p.trackItems[i] = item
if len(existing) == 0 {
return p.Context.DbPtr.Save(&AudioFileList).Error
}
return nil
}
// positionTrackItems places the visible window of items and parks the rest off-screen.
func (p *AudioOptions) positionTrackItems() {
peekCount := visibleCount + 1
for i := range p.trackItems {
slot := i - p.windowStart
if slot < 0 || slot >= peekCount {
p.trackItems[i].X = p.listLeftX
p.trackItems[i].Y = -9999
continue
}
itemY := p.listTopY - int32(slot)*(p.itemHeight+p.trackSpacing)
p.trackItems[i].X = p.listLeftX
p.trackItems[i].Y = itemY
}
}
// ── Scrollbar ─────────────────────────────────────────────────────────────────
// updateScrollbarThumb recalculates the thumb Y position based on windowStart.
// Called whenever windowStart changes.
func (p *AudioOptions) updateScrollbarThumb() {
total := len(AudioFileList)
if total <= visibleCount {
// Entire list fits — thumb fills the track, nothing to scroll
p.scrollbarThumbY = p.scrollbarTrackY
return
}
// How far through the scrollable range are we?
// maxWindowStart is the furthest windowStart can go
maxWindowStart := total - visibleCount
fraction := float32(p.windowStart) / float32(maxWindowStart)
// Thumb travels from the top of the track down to (trackTop - thumbH)
// "top" in our Y-up projection means the larger Y value
trackTop := p.scrollbarTrackY + p.scrollbarTrackH
travelRange := p.scrollbarTrackH - p.scrollbarThumbH
p.scrollbarThumbY = trackTop - p.scrollbarThumbH - int32(fraction*float32(travelRange))
}
// ── Audio status toggle ───────────────────────────────────────────────────────
func (p *AudioOptions) CreateAudioStatusItem() error {
@@ -345,7 +235,7 @@ func (p *AudioOptions) layout() {
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
p.prompt.Y = p.title.Y - p.verticalMargin - p.prompt.H
// Volume row (not focusable)
// Volume row
barH := int32(float32(p.Context.CfgPtr.StandardFontSize) * p.ItemScale)
barW := int32(float32(p.ScreenWidth) * 0.30)
barX := p.ScreenCenterX - barW/2
@@ -363,100 +253,14 @@ func (p *AudioOptions) layout() {
p.volumePercent.X = barX + barW + gap
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
// "Available Tracks" heading — centred, left edge becomes the list anchor
p.tracksHeading.X = p.ScreenCenterX - p.tracksHeading.W/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
// Toggle — left-aligned to the heading
p.AudioStatusToggle.X = p.tracksHeading.X
p.AudioStatusToggle.Y = barY - p.verticalMargin*2 - p.AudioStatusToggle.H
// Separator
p.separatorY = p.AudioStatusToggle.Y - p.verticalMargin*3
// Heading Y — below separator
p.tracksHeading.Y = p.separatorY - p.verticalMargin - p.tracksHeading.H
// Track list geometry — shifted down by listShiftDown
if len(p.trackItems) > 0 {
p.itemHeight = p.trackItems[0].H
}
p.listLeftX = p.tracksHeading.X
p.listTopY = p.tracksHeading.Y - p.verticalMargin - listShiftDown
// ── Scrollbar geometry ────────────────────────────────────────────────────
// The scrollbar track spans the full visible window height.
// It sits to the right of the list with a small gap.
windowH := int32(visibleCount)*(p.itemHeight+p.trackSpacing) - p.trackSpacing
scrollbarBottom := p.listTopY - windowH
p.scrollbarTrackH = windowH
p.scrollbarTrackY = scrollbarBottom // bottom Y of the track (Y-up coords)
// Right edge of the list: use the widest track name as an approximation.
// Since items are left-aligned and widths vary, we use a fixed width based
// on the heading width which is a reasonable proxy for the list column width.
listRightX := p.listLeftX + p.tracksHeading.W
p.scrollbarX = listRightX + scrollbarGap
// Thumb height is proportional: visibleCount / total items
total := len(AudioFileList)
if total > 0 {
thumbFraction := float32(visibleCount) / float32(total)
p.scrollbarThumbH = int32(thumbFraction * float32(p.scrollbarTrackH))
if p.scrollbarThumbH < 8 {
p.scrollbarThumbH = 8 // minimum thumb size so it's always clickable
}
}
// Triangle cursor X — just outside the left edge of the list
// Triangle cursor — just outside the left edge of the toggle, vertically centred on it
cursorGap := int32(12)
p.triX = float32(p.listLeftX) - float32(triangleSize) - float32(cursorGap)
p.positionTrackItems()
p.updateScrollbarThumb()
p.repositionCursor()
}
// repositionCursor vertically centres the triangle on the focused item.
func (p *AudioOptions) repositionCursor() {
var targetY, targetH int32
if p.focusedIndex == 0 {
targetY = p.AudioStatusToggle.Y
targetH = p.AudioStatusToggle.H
} else {
trackIdx := p.focusedIndex - 1
slot := trackIdx - p.windowStart
targetY = p.listTopY - int32(slot)*(p.itemHeight+p.trackSpacing)
targetH = p.itemHeight
}
p.triCenterY = float32(targetY) + float32(targetH)/2
}
// ── Scroll ────────────────────────────────────────────────────────────────────
func (p *AudioOptions) scrollToFocus() {
if p.focusedIndex == 0 {
p.windowStart = 0
p.positionTrackItems()
p.updateScrollbarThumb()
p.repositionCursor()
return
}
trackIdx := p.focusedIndex - 1
if trackIdx >= p.windowStart+visibleCount {
p.windowStart = trackIdx - visibleCount + 1
}
if trackIdx < p.windowStart {
p.windowStart = trackIdx
}
p.positionTrackItems()
p.updateScrollbarThumb()
p.repositionCursor()
p.triX = float32(p.AudioStatusToggle.X) - float32(triangleSize) - float32(cursorGap)
p.triCenterY = float32(p.AudioStatusToggle.Y) + float32(p.AudioStatusToggle.H)/2
}
// ── Toggle ────────────────────────────────────────────────────────────────────
@@ -469,46 +273,6 @@ func (p *AudioOptions) ToggleAudio() {
p.layout()
}
// ── Track selection ───────────────────────────────────────────────────────────
func (p *AudioOptions) onTrackSelected(index int, entry db.AudioFile) {
p.selectedTrack = index
if err := p.buildTrackItems(); err != nil {
logger.Error("onTrackSelected: failed to rebuild track items: %v", err)
return
}
p.layout()
// Create file names and paths
// Set newly selected entry as Selected
entry.Selected = true
// Set path
if !strings.Contains(entry.Path, ".local/") {
entry.Path = filepath.Join(p.Context.CfgPtr.AssetsDir, "audio/", entry.Path)
}
// Clear previously selected.
var previouslySelectedAudio db.AudioFile
if err := p.Context.DbPtr.Where("Selected = ?", true).First(&previouslySelectedAudio).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.Info("No audio previously selected")
} else {
logger.Error("Unable to fetch previously selected audio from database %s", err)
}
} else {
previouslySelectedAudio.Selected = false
if err = p.Context.DbPtr.Save(&previouslySelectedAudio).Error; err != nil {
logger.Error("Unable to update selected audio: %v", err)
return
}
}
// Save it to the database
if err := p.Context.DbPtr.Save(&entry).Error; err != nil {
logger.Error("Error saving to database: %v", err)
return
}
logger.Info("Updated audio selection: %s", entry.Name)
}
// ── Events ────────────────────────────────────────────────────────────────────
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
@@ -518,25 +282,8 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
}
switch e.Keysym.Sym {
case sdl.K_UP:
if p.focusedIndex > 0 {
p.focusedIndex--
p.scrollToFocus()
}
case sdl.K_DOWN:
if p.focusedIndex < len(AudioFileList) {
p.focusedIndex++
p.scrollToFocus()
}
case sdl.K_RETURN:
if p.focusedIndex == 0 {
p.ToggleAudio()
} else {
trackIdx := p.focusedIndex - 1
p.onTrackSelected(trackIdx, AudioFileList[trackIdx])
}
p.ToggleAudio()
case sdl.K_RIGHT:
p.UpdateVolume(10)
@@ -563,17 +310,6 @@ func (p *AudioOptions) Update(dt float32) error {
// ── Render helpers ────────────────────────────────────────────────────────────
func (p *AudioOptions) renderSeparator() error {
lineW := float32(p.ScreenWidth) * 0.80
lineX := float32(p.ScreenCenterX) - lineW/2
return p.RenderSolidColorQuad(
p.solidShader,
mgl32.Vec2{lineX, float32(p.separatorY)},
mgl32.Vec2{lineW, 2},
colors.White,
)
}
func (p *AudioOptions) renderVolumeBar() error {
vol := p.Context.CfgPtr.SavedVolume
@@ -609,37 +345,6 @@ func (p *AudioOptions) renderVolumeBar() error {
return nil
}
// renderScrollbar draws the scrollbar track (dim) and thumb (white).
// Only rendered when the list is longer than the visible window.
func (p *AudioOptions) renderScrollbar() error {
if len(AudioFileList) <= visibleCount {
return nil
}
// Track — a thin dim line the full height of the window
trackColor := sdl.Color{R: 80, G: 80, B: 80, A: 255}
if err := p.RenderSolidColorQuad(
p.solidShader,
mgl32.Vec2{float32(p.scrollbarX), float32(p.scrollbarTrackY)},
mgl32.Vec2{float32(scrollbarWidth), float32(p.scrollbarTrackH)},
trackColor,
); err != nil {
return fmt.Errorf("rendering scrollbar track: %w", err)
}
// Thumb — white, positioned proportionally within the track
if err := p.RenderSolidColorQuad(
p.solidShader,
mgl32.Vec2{float32(p.scrollbarX), float32(p.scrollbarThumbY)},
mgl32.Vec2{float32(scrollbarWidth), float32(p.scrollbarThumbH)},
colors.White,
); err != nil {
return fmt.Errorf("rendering scrollbar thumb: %w", err)
}
return nil
}
// renderCursor draws the spinning triangle cursor.
func (p *AudioOptions) renderCursor() error {
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
@@ -693,32 +398,6 @@ func (p *AudioOptions) Render() error {
return fmt.Errorf("render audio toggle: %w", err)
}
// Separator
if err := p.renderSeparator(); err != nil {
return fmt.Errorf("render separator: %w", err)
}
// "Available Tracks" heading — fixed, never moves
if err := p.RenderTexture(p.textShader, p.tracksHeading.ID, p.tracksHeading.Position(), p.tracksHeading.RawDimensions(), p.tracksHeading.Color); err != nil {
return fmt.Errorf("render tracks heading: %w", err)
}
// Track list
for i := range p.trackItems {
item := &p.trackItems[i]
if item.Y < 0 {
continue
}
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color); err != nil {
return fmt.Errorf("render track item %d: %w", i, err)
}
}
// Scrollbar
if err := p.renderScrollbar(); err != nil {
return fmt.Errorf("render scrollbar: %w", err)
}
// Spinning triangle cursor — always on top
if err := p.renderCursor(); err != nil {
return fmt.Errorf("render cursor: %w", err)
@@ -735,10 +414,6 @@ func (p *AudioOptions) Destroy() error {
gl.DeleteTextures(1, &p.VolumeLabel.ID)
gl.DeleteTextures(1, &p.volumePercent.ID)
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
gl.DeleteTextures(1, &p.tracksHeading.ID)
for i := range p.trackItems {
gl.DeleteTextures(1, &p.trackItems[i].ID)
}
gl.DeleteBuffers(1, &p.triVBO)
gl.DeleteVertexArrays(1, &p.triVAO)
return nil