diff --git a/internal/page/AudioOptions.go b/internal/page/AudioOptions.go index e6c218c..82acc6e 100644 --- a/internal/page/AudioOptions.go +++ b/internal/page/AudioOptions.go @@ -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