List proclamations inside of categories
F2 also redirects when no proclamations are selected (the default)
This commit is contained in:
Vendored
+4
-1
@@ -9,7 +9,10 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "rhemawave.go"
|
||||
"program": "rhemawave.go",
|
||||
"env": {
|
||||
"DEBUGINFOD_URLS": ""
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+108
-93
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user