361 lines
9.8 KiB
Go
361 lines
9.8 KiB
Go
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/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
|
|
IsSubCategory bool
|
|
Depth int
|
|
Expanded bool
|
|
}
|
|
|
|
type ProclamationOptions struct {
|
|
Base
|
|
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
|
|
|
|
isSearchActive bool
|
|
}
|
|
|
|
func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
|
p.verticalMargin = 25
|
|
p.indentSize = 40.0
|
|
p.activeMap = make(map[string]bool)
|
|
p.allItems = []*FlatItem{}
|
|
p.scrollOffset = 0
|
|
p.isSearchActive = false
|
|
|
|
p.scrollTrackWidth = 30
|
|
p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth
|
|
|
|
fm := fontManager.Get()
|
|
font, _ := fm.GetFont("Roboto-Regular")
|
|
p.font = font
|
|
|
|
sm := shaderManager.Get()
|
|
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 Space: Select/Expand Enter: Save Changes 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
|
|
}
|
|
|
|
// 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 && depth == 0 {
|
|
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,
|
|
IsSubCategory: isCat && depth > 0,
|
|
Depth: depth,
|
|
}
|
|
}
|
|
|
|
func (p *ProclamationOptions) layout() {
|
|
p.title.X = p.ScreenCenterX - p.title.W/2
|
|
p.title.Y = 40
|
|
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
|
p.prompt.Y = p.ScreenHeight - 60
|
|
|
|
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 || item.IsSubCategory {
|
|
return // Subcategories are never manually toggled
|
|
}
|
|
|
|
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 {
|
|
fi := p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, parent.Depth+1)
|
|
fi.Expanded = true // Mark expanded so collapse state is consistent
|
|
newItems = append(newItems, fi)
|
|
}
|
|
|
|
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...)...)
|
|
|
|
// Auto-expand any subcategories just inserted
|
|
// Walk forward: for each newly inserted category, expand it in-place
|
|
i := index + 1
|
|
for i < len(p.allItems) && p.allItems[i].Depth > parent.Depth {
|
|
if p.allItems[i].IsCategory && p.allItems[i].Depth > 0 {
|
|
p.expand(i) // Recurse — inserts children in-place, loop will walk into them
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
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:
|
|
if e.Type != sdl.KEYDOWN {
|
|
return nil
|
|
}
|
|
switch e.Keysym.Sym {
|
|
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.allItems)-1 {
|
|
p.highlightIndex++
|
|
if p.highlightIndex >= p.scrollOffset+p.maxVisible {
|
|
p.scrollOffset++
|
|
}
|
|
}
|
|
case sdl.K_PAGEUP:
|
|
p.highlightIndex = p.scrollOffset
|
|
p.scrollOffset -= (p.maxVisible - 1)
|
|
if p.scrollOffset < 0 {
|
|
p.scrollOffset = 0
|
|
p.highlightIndex = 0
|
|
}
|
|
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:
|
|
p.acceptChanges()
|
|
case sdl.K_SPACE:
|
|
item := p.allItems[p.highlightIndex]
|
|
if !item.IsCategory {
|
|
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
|
} else if !item.IsSubCategory {
|
|
p.toggleExpansion(p.highlightIndex)
|
|
}
|
|
case sdl.K_BACKSPACE, sdl.K_ESCAPE:
|
|
p.Context.NavigateTo(SETTINGS)
|
|
default:
|
|
// Treat input as search intent, initiate search & filter
|
|
p.toggleTextInput()
|
|
p.isSearchActive = true
|
|
}
|
|
p.layout()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Turn on and off the text input capture.
|
|
// Don't forget to capture the current event, so that you can
|
|
// determine which character to start the search with.
|
|
func (p *ProclamationOptions) toggleTextInput() {}
|
|
|
|
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(SCROLLER)
|
|
}
|
|
|
|
func (p *ProclamationOptions) Render() error {
|
|
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)
|
|
|
|
if len(p.allItems) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// 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.DodgerBlue
|
|
} else if p.activeMap[item.ID] {
|
|
item.Label.Color = colors.Orange
|
|
} 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.allItems {
|
|
gl.DeleteTextures(1, &item.Label.ID)
|
|
}
|
|
return nil
|
|
}
|