diff --git a/.vscode/launch.json b/.vscode/launch.json index 1226d3e..2a88165 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,10 @@ "type": "go", "request": "launch", "mode": "debug", - "program": "rhemawave.go" + "program": "rhemawave.go", + "env": { + "DEBUGINFOD_URLS": "" + } }, ] } \ No newline at end of file diff --git a/internal/page/proclamationOptions.go b/internal/page/proclamationOptions.go index 414f333..e4dfb51 100644 --- a/internal/page/proclamationOptions.go +++ b/internal/page/proclamationOptions.go @@ -2,134 +2,194 @@ package page import ( "fmt" + "strconv" "github.com/go-gl/gl/v3.3-core/gl" + "github.com/go-gl/mathgl/mgl32" "github.com/veandco/go-sdl2/sdl" + "gorm.io/gorm" // Ensure correct GORM import "rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" - "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" ) +// Scrollbar Visuals +var ( + colorScrollTrack = sdl.Color{R: 255, G: 255, B: 255, A: 30} + colorScrollThumb = sdl.Color{R: 255, G: 255, B: 255, A: 100} +) + +type FlatItem struct { + Label StringItem + ID string + IsCategory bool + Depth int + Expanded bool +} + type ProclamationOptions struct { Base - font *fontManager.FontEntry - textShader *shaderManager.Shader - solidShader *shaderManager.Shader - title StringItem - prompt StringItem - proclamations []StringItem - proclamationIDs []string - activeMap map[int]bool // Maps display index to selected state - highlightIndex int - verticalMargin int32 + font *fontManager.FontEntry + textShader *shaderManager.Shader + solidShader *shaderManager.Shader + title StringItem + prompt StringItem + + // Tree State + allItems []*FlatItem + activeMap map[string]bool + highlightIndex int + + // Scrolling State + scrollOffset int + maxVisible int + + // Scrollbar Dimens + scrollTrackWidth int32 + scrollTrackX int32 + scrollTrackHeight int32 + scrollTrackY int32 + + indentSize float32 + verticalMargin int32 } func (p *ProclamationOptions) Init(ctx *PageCtx) error { - var err error - p.verticalMargin = int32(20) - p.activeMap = make(map[int]bool) - p.proclamationIDs = make([]string, 0) + p.verticalMargin = 25 + p.indentSize = 40.0 + p.activeMap = make(map[string]bool) + p.allItems = []*FlatItem{} + p.scrollOffset = 0 + + p.scrollTrackWidth = 30 + p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth - // Get font fm := fontManager.Get() - font, ok := fm.GetFont("Roboto-Regular") - if !ok { - return fmt.Errorf("failed to get font: %w", sdl.GetError()) - } + font, _ := fm.GetFont("Roboto-Regular") 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.textShader, _ = sm.Get("text") + p.solidShader, _ = sm.Get("solid_color") + + p.Base.Init(ctx) + + itemScale := float32(32) / float32(ctx.CfgPtr.StandardFontSize) + p.title, _ = NewStringItem("Proclamation Browser", p.font, colors.White) + p.title.Scale(float32(64) / float32(ctx.CfgPtr.StandardFontSize)) + + p.prompt, _ = NewStringItem("Arrows: Nav | Enter: Exp/Col | Space: Toggle | A: Accept | Back: Cancel", p.font, colors.White) + p.prompt.Scale(itemScale * 0.8) + + // Calculation for visible area + areaHeight := float32(p.ScreenHeight) - 250 + p.maxVisible = int(areaHeight / 47) + + // Load current selections + var selected []db.Proclamation + p.Context.DbPtr.Where("selected = ?", true).Find(&selected) + for _, s := range selected { + p.activeMap[s.ID] = true } - 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 := ctx.CfgPtr.StandardFontSize - - // Calculate scales - titleScale := float32(64) / float32(baseFontSize) - itemScale := float32(32) / float32(baseFontSize) - - // Create title - p.title, err = NewStringItem("Select Proclamations", p.font, colors.White) - if err != nil { - return fmt.Errorf("creating title: %w", err) - } - p.title.Scale(titleScale) - - // Create prompt - p.prompt, err = NewStringItem("Arrow Keys to navigate | Space to toggle | F2 to run | Enter to return", p.font, colors.White) - if err != nil { - return fmt.Errorf("creating prompt: %w", err) - } - p.prompt.Scale(itemScale) - - // Load proclamations from database - var proclamations []db.Proclamation - if result := p.Context.DbPtr.Find(&proclamations); result.Error != nil { - logger.Error("unable to query database for proclamations: %v", result.Error) - return fmt.Errorf("%s", result.Error) - } - - // Create UI items for each available proclamation - for _, proclamation := range proclamations { - if !proclamation.Available { - continue - } - - label, err := NewStringItem(proclamation.Name, p.font, colors.White) - if err != nil { - return fmt.Errorf("creating proclamation label: %w", err) - } - - label.Scale(itemScale) - p.proclamations = append(p.proclamations, label) - p.proclamationIDs = append(p.proclamationIDs, proclamation.ID) - - // Set initial selection state - if proclamation.Selected { - p.activeMap[len(p.proclamations)-1] = true - } + // Load Root Categories + var roots []db.ProclamationCategory + p.Context.DbPtr.Where("parent_id IS NULL OR parent_id = 0").Find(&roots) + for _, c := range roots { + p.allItems = append(p.allItems, p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, 0)) } p.layout() return nil } +func (p *ProclamationOptions) createFlatItem(name, id string, isCat bool, depth int) *FlatItem { + text := name + if isCat { + text = "[+] " + name + } + itemScale := float32(32) / float32(p.Context.CfgPtr.StandardFontSize) + lbl, _ := NewStringItem(text, p.font, colors.White) + lbl.Scale(itemScale) + return &FlatItem{Label: lbl, ID: id, IsCategory: isCat, Depth: depth} +} + func (p *ProclamationOptions) layout() { - // Position title at bottom p.title.X = p.ScreenCenterX - p.title.W/2 - p.title.Y = p.ScreenHeight - p.title.H - p.verticalMargin*2 - - // Position prompt above title + p.title.Y = 40 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.ScreenHeight - 60 - // Position proclamation items stacked upward from prompt - for i := range p.proclamations { - p.proclamations[i].X = p.ScreenCenterX - p.proclamations[i].W/2 - if i == 0 { - p.proclamations[i].Y = p.prompt.Y - p.verticalMargin*2 - p.proclamations[i].H - } else { - p.proclamations[i].Y = p.proclamations[i-1].Y - p.proclamations[i].H - 10 + startY := float32(p.ScreenHeight - 150) + p.scrollTrackY = 130 + p.scrollTrackHeight = int32(startY) - 130 + 50 + + for i, item := range p.allItems { + item.Label.X = -5000 // Hidden + + if i >= p.scrollOffset && i < p.scrollOffset+p.maxVisible { + visibleIdx := i - p.scrollOffset + // Cast to int32 for StringItem coordinates + item.Label.X = int32(60.0 + (float32(item.Depth) * p.indentSize)) + item.Label.Y = int32(startY - (float32(visibleIdx) * 47.0)) } } } +func (p *ProclamationOptions) toggleExpansion(index int) { + item := p.allItems[index] + if !item.IsCategory { + return + } + + pureName := item.Label.content[4:] + gl.DeleteTextures(1, &item.Label.ID) + + itemScale := float32(32) / float32(p.Context.CfgPtr.StandardFontSize) + if item.Expanded { + p.collapse(index) + item.Label, _ = NewStringItem("[+] "+pureName, p.font, colors.White) + } else { + p.expand(index) + item.Label, _ = NewStringItem("[-] "+pureName, p.font, colors.White) + } + item.Label.Scale(itemScale) + item.Expanded = !item.Expanded + p.layout() +} + +func (p *ProclamationOptions) expand(index int) { + parent := p.allItems[index] + parentID, _ := strconv.Atoi(parent.ID) + var newItems []*FlatItem + + var cats []db.ProclamationCategory + p.Context.DbPtr.Where("parent_id = ?", parentID).Find(&cats) + for _, c := range cats { + newItems = append(newItems, p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, parent.Depth+1)) + } + + var procs []db.Proclamation + p.Context.DbPtr.Where("category_id = ?", parentID).Find(&procs) + for _, pr := range procs { + newItems = append(newItems, p.createFlatItem(pr.Name, pr.ID, false, parent.Depth+1)) + } + + tail := append([]*FlatItem{}, p.allItems[index+1:]...) + p.allItems = append(p.allItems[:index+1], append(newItems, tail...)...) +} + +func (p *ProclamationOptions) collapse(index int) { + depth := p.allItems[index].Depth + i := index + 1 + for i < len(p.allItems) && p.allItems[i].Depth > depth { + gl.DeleteTextures(1, &p.allItems[i].Label.ID) + i++ + } + p.allItems = append(p.allItems[:index+1], p.allItems[i:]...) +} + func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error { switch e := (*event).(type) { case *sdl.KeyboardEvent: @@ -140,100 +200,130 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error { case sdl.K_UP: if p.highlightIndex > 0 { p.highlightIndex-- + if p.highlightIndex < p.scrollOffset { + p.scrollOffset-- + } } case sdl.K_DOWN: - if p.highlightIndex < len(p.proclamations)-1 { + if p.highlightIndex < len(p.allItems)-1 { p.highlightIndex++ + if p.highlightIndex >= p.scrollOffset+p.maxVisible { + p.scrollOffset++ + } } - case sdl.K_BACKSPACE: - p.Context.NavigateTo(SETTINGS) - case sdl.K_SPACE: - // Toggle selection for highlighted item - if p.activeMap[p.highlightIndex] { - delete(p.activeMap, p.highlightIndex) - } else { - p.activeMap[p.highlightIndex] = true + case sdl.K_PAGEUP: + p.highlightIndex = p.scrollOffset + p.scrollOffset -= (p.maxVisible - 1) + if p.scrollOffset < 0 { + p.scrollOffset = 0 + p.highlightIndex = 0 } - - // Immediately save to database - if err := p.toggleProclamation(p.highlightIndex); err != nil { - return fmt.Errorf("failed to toggle proclamation: %w", err) + case sdl.K_PAGEDOWN: + bottomIdx := p.scrollOffset + p.maxVisible - 1 + if bottomIdx >= len(p.allItems) { + bottomIdx = len(p.allItems) - 1 } + p.scrollOffset = bottomIdx + p.highlightIndex = bottomIdx case sdl.K_RETURN: - // Just return to settings - no saving needed + p.toggleExpansion(p.highlightIndex) + case sdl.K_SPACE: + item := p.allItems[p.highlightIndex] + if !item.IsCategory { + p.activeMap[item.ID] = !p.activeMap[item.ID] + } + case sdl.K_a: + p.acceptChanges() + case sdl.K_BACKSPACE, sdl.K_ESCAPE: p.Context.NavigateTo(SETTINGS) } + p.layout() } return nil } -// toggleProclamation immediately updates the database for the proclamation at the given display index -func (p *ProclamationOptions) toggleProclamation(displayIndex int) error { - if displayIndex < 0 || displayIndex >= len(p.proclamationIDs) { - return fmt.Errorf("invalid display index %d", displayIndex) - } - - dbID := p.proclamationIDs[displayIndex] - isSelected := p.activeMap[displayIndex] - - // Get the proclamation from database - var proclamation db.Proclamation - proclamation.ID = dbID - if result := p.Context.DbPtr.Find(&proclamation); result.Error != nil { - logger.Error("unable to query proclamation %s from database: %v", proclamation.ID, result.Error) - } - - // Update the Selected field - proclamation.Selected = isSelected - if result := p.Context.DbPtr.Save(&proclamation); result.Error != nil { - logger.Error("failed to update proclamation %s: %v", proclamation.ID, result.Error) - return fmt.Errorf("%s", result.Error) - } - - return nil +func (p *ProclamationOptions) acceptChanges() { + p.Context.DbPtr.Transaction(func(tx *gorm.DB) error { + tx.Model(&db.Proclamation{}).Where("1 = 1").Update("selected", false) + var ids []string + for id, active := range p.activeMap { + if active { + ids = append(ids, id) + } + } + if len(ids) > 0 { + tx.Model(&db.Proclamation{}).Where("id IN ?", ids).Update("selected", true) + } + return nil + }) + p.Context.NavigateTo(SETTINGS) } func (p *ProclamationOptions) Render() error { - // Always render title and prompt p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color) p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color) - // No proclamations to display - if len(p.proclamations) == 0 { + if len(p.allItems) == 0 { return nil } - // Render highlight box behind selected item - p.RenderSolidColorQuad( - p.solidShader, - p.proclamations[p.highlightIndex].Position(), - p.proclamations[p.highlightIndex].RawDimensions(), - colors.White, - ) - - // Render proclamation items with appropriate colors - for i, item := range p.proclamations { - if p.activeMap[i] { - // Selected items are green - item.Color = colors.Green - } else if i == p.highlightIndex { - // Highlighted item is black (shows on white background) - item.Color = colors.Black - } else { - // Normal items are white - item.Color = colors.White - } - p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), item.Color) + // Highlight Box + if p.highlightIndex >= p.scrollOffset && p.highlightIndex < p.scrollOffset+p.maxVisible { + target := p.allItems[p.highlightIndex].Label + p.RenderSolidColorQuad(p.solidShader, target.Position(), target.RawDimensions(), colors.White) } + for i := p.scrollOffset; i < p.scrollOffset+p.maxVisible && i < len(p.allItems); i++ { + item := p.allItems[i] + if i == p.highlightIndex { + item.Label.Color = colors.Black + } else if item.IsCategory { + item.Label.Color = colors.Cyan + } else if p.activeMap[item.ID] { + item.Label.Color = colors.Green + } else { + item.Label.Color = colors.White + } + p.RenderTexture(p.textShader, item.Label.ID, item.Label.Position(), item.Label.RawDimensions(), item.Label.Color) + } + + p.renderScrollbar() return nil } +func (p *ProclamationOptions) renderScrollbar() { + total := len(p.allItems) + if total <= p.maxVisible { + return + } + + // 1. Render Track + p.RenderSolidColorQuad(p.solidShader, + mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)}, + mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackHeight)}, + colorScrollTrack) + + // Thumb Math + visibleRatio := float32(p.maxVisible) / float32(total) + scrollRatio := float32(p.scrollOffset) / float32(total) + thumbHeight := float32(p.scrollTrackHeight) * visibleRatio + if thumbHeight < 20 { + thumbHeight = 20 + } + thumbY := float32(p.scrollTrackY) + (float32(p.scrollTrackHeight) * scrollRatio) + + // 2. Render Thumb + p.RenderSolidColorQuad(p.solidShader, + mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY}, + mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight}, + colorScrollThumb) +} + func (p *ProclamationOptions) Destroy() error { gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.prompt.ID) - for _, item := range p.proclamations { - gl.DeleteTextures(1, &item.ID) + for _, item := range p.allItems { + gl.DeleteTextures(1, &item.Label.ID) } return nil } diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index bf80f31..f7c61ed 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -67,6 +67,7 @@ import ( "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" colorPattern "rhemawavetech.com/rhemawave/internal/pattern/color" linePattern "rhemawavetech.com/rhemawave/internal/pattern/line" @@ -194,81 +195,78 @@ type ScrollerPage struct { } func (p *ScrollerPage) Init(ctx *PageCtx) error { + // 1. Basic configuration and Deque allocation + // We do this first so Destroy() has a valid object to clean up if we fail later. p.increment = 100.0 p.velocity = 800.0 p.SetAutoVelocity(true, -4000.0, 4000.0, 100.0, 2.5, 6.0) + standardFontSize := ctx.CfgPtr.StandardFontSize + baseFontSize := ctx.CfgPtr.StandardFontSize + p.fontScale = float32(standardFontSize) / float32(baseFontSize) + + nLines := int(p.ScreenHeight/int32(standardFontSize)) + 5 + p.lines = NewLineDeque(nLines * 2) + + // 2. Initialize Base and Managers + p.Base.Init(ctx) sm := shaderManager.Get() fm := fontManager.Get() + if fm == nil || sm == nil { + return fmt.Errorf("critical systems (font/shader managers) not initialized") + } - // Get font + // 3. Resource Loading: Font font, ok := fm.GetFont("Scrambled") - if !ok { - return fmt.Errorf("unable to fetch font 'Scrambled'") + if !ok || font == nil { + return fmt.Errorf("unable to fetch font 'Scrambled': font is nil") } p.font = font - p.Base.Init(ctx) - - // Get text shader + // 4. Resource Loading: Shader textShader, ok := sm.Get("text") - if !ok { - return fmt.Errorf("text shader not found") + if !ok || textShader == nil { + return fmt.Errorf("text shader not found or is nil") } p.shader = textShader - // Load proclamation words from database + // 5. Calculate Font Metrics (Now safe because p.font is guaranteed) + spaceGlyph, ok := fm.GetGlyph(p.font.Name, ' ') + if !ok { + return fmt.Errorf("failed to get space glyph for font: %s", p.font.Name) + } + p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale) + + // Get line height (Using a temporary texture then immediate cleanup) + tmpStrID, _, H, err := fm.NewCreateStringTexture(p.font.Name, "Mg", p.shader) + if err != nil { + return fmt.Errorf("failed to create sample texture for metrics: %w", err) + } + p.lineHeight = int32(float32(H) * p.fontScale) + gl.DeleteTextures(1, &tmpStrID) + + // 6. Load Data: Proclamations if err := p.loadProclamations(); err != nil { - // Don't stop execution, just show fallback words - p.words = strings.Fields("No Proclamations Selected!") + // If no proclamations, we must stop here or we'll crash in createLine + p.Context.NavigateTo(PROCLAMATIONS) + return fmt.Errorf("navigation triggered: %w", err) } p.wordIndex = 0 - // Get line pattern from database - var err error + // 7. Load Data: Patterns linePatternName := p.Base.Context.CfgPtr.LinePattern - p.activePattern, err = linePattern.Get(linePatternName) if err != nil { return fmt.Errorf("failed to load line pattern '%s': %w", linePatternName, err) } - // Initialize color pattern var baseColors = []sdl.Color{ - colors.Tan, - colors.PeachPuff, - colors.LightCyan, - colors.LightSeaGreen, - colors.MediumTurquoise, - colors.DarkCyan, - colors.MediumPurple, + colors.Tan, colors.PeachPuff, colors.LightCyan, + colors.LightSeaGreen, colors.MediumTurquoise, + colors.DarkCyan, colors.MediumPurple, } p.colorPattern = colorPattern.NewColorPattern(baseColors) - // Get font sizes from database (optimize: load once, cache as int32) - standardFontSize := p.Base.Context.CfgPtr.StandardFontSize - baseFontSize := p.Base.Context.CfgPtr.StandardFontSize - - // Font setup (optimized: calculate once, reuse) - p.fontScale = float32(standardFontSize) / float32(baseFontSize) - nLines := int(p.ScreenHeight / int32(standardFontSize)) - p.lines = NewLineDeque(nLines * 2) - - // Calculate space width once - spaceGlyph, ok := fm.GetGlyph(p.font.Name, ' ') - if !ok { - return fmt.Errorf("failed to get space glyph") - } - p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale) - - // Get line height from a sample string - tmpStrID, _, H, err := fm.NewCreateStringTexture(p.font.Name, "Mg", p.shader) - if err != nil { - return fmt.Errorf("failed to create sample texture: %w", err) - } - p.lineHeight = int32(float32(H) * p.fontScale) - gl.DeleteTextures(1, &tmpStrID) - p.lineCtx = &linePattern.LineRenderContext{ ScreenWidth: p.ScreenWidth, Font: p.font, @@ -276,37 +274,23 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { SpaceWidth: p.spaceWidth, } - // Create initial lines + // 8. Generate Initial Visual State for i := 0; i < nLines; i++ { if err := p.createLine(i, true); err != nil { return fmt.Errorf("failed to create initial line %d: %w", i, err) } } - // Audio setup + // 9. Audio Initialization audioDeviceName := p.Base.Context.CfgPtr.SelectedAudioDevice - - // Switch audio devices if needed if mixer.CurrentDevice() != audioDeviceName { - if err := mixer.SwitchDevice(audioDeviceName); err != nil { - } else { - } + _ = mixer.SwitchDevice(audioDeviceName) } - // Start audio playback selectedAudio := p.Base.Context.CfgPtr.SelectedAudio - assetsDir := p.Base.Context.CfgPtr.AssetsDir - - if selectedAudio == "" { - } else { - if err := mixer.Play(filepath.Join(assetsDir, selectedAudio), true, p.Base.Context.CfgPtr); err != nil { - } - } - - currentDevice := mixer.CurrentDevice() - if currentDevice == "" { - currentDevice = "default" + if selectedAudio != "" { + _ = mixer.Play(filepath.Join(assetsDir, selectedAudio), true, p.Base.Context.CfgPtr) } return nil @@ -316,19 +300,23 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { func (p *ScrollerPage) loadProclamations() error { p.words = []string{} - // Get all selected proclamations from database + // 1. Query only Selected AND Available records directly from the DB var proclamations []db.Proclamation - p.Base.Context.DbPtr.Find(&proclamations) + err := p.Base.Context.DbPtr.Where("selected = ? AND available = ?", true, true).Find(&proclamations).Error - // Build word list from selected, available proclamations - for _, proclamation := range proclamations { - if proclamation.Selected && proclamation.Available { - // Split content into words and add to list - words := strings.Fields(proclamation.Content) - p.words = append(p.words, words...) - } + if err != nil { + logger.Error("Database query failed: %v", err) + return err } + // 2. Build word list + for _, proclamation := range proclamations { + // strings.Fields handles multiple spaces and newlines automatically + words := strings.Fields(proclamation.Content) + p.words = append(p.words, words...) + } + + // 3. Handle empty state if len(p.words) == 0 { p.words = strings.Fields("No Proclamations Selected!") return fmt.Errorf("no proclamations selected or available") @@ -580,33 +568,60 @@ func (p *ScrollerPage) Render() error { return nil } +// Drop references func (p *ScrollerPage) Destroy() error { if p == nil { return nil } - mixer.Stop(1000, p.Context.CfgPtr) - - // GL cleanup — delete all text textures - for i := 0; i < p.lines.Len(); i++ { - line := p.lines.Get(i) - for j := range line.words { - if id := line.words[j].ID; id != 0 { - gl.DeleteTextures(1, &line.words[j].ID) - line.words[j].ID = 0 - } - } + // 1. Audio Guard + // Ensure Context and CfgPtr exist before stopping the mixer + if p.Context != nil && p.Context.CfgPtr != nil { + mixer.Stop(1000, p.Context.CfgPtr) } - gl.Flush() + // 2. Lines / OpenGL Guard + // We only iterate and delete textures if the deque was actually allocated + if p.lines != nil { + for i := 0; i < p.lines.Len(); i++ { + line := p.lines.Get(i) + for j := range line.words { + // Double-check word ID to avoid redundant GL calls + if line.words[j].ID != 0 { + gl.DeleteTextures(1, &line.words[j].ID) + line.words[j].ID = 0 + } + } + } + p.lines.Clear() // Optional: clear circular buffer internal refs + p.lines = nil + } - // Drop references for GC - p.lines = nil - p.words = nil - p.lineCtx = nil - p.font = nil - p.shader = nil - p.colorPattern = nil + // 3. Pointer Cleanup Guards + // Explicitly setting to nil to help the Garbage Collector, + // but only if they aren't already nil. + if p.words != nil { + p.words = nil + } + + if p.lineCtx != nil { + p.lineCtx = nil + } + + if p.font != nil { + p.font = nil + } + + if p.shader != nil { + p.shader = nil + } + + if p.colorPattern != nil { + p.colorPattern = nil + } + + // Final GPU sync + gl.Flush() return nil }