Moved selection keybinds: - Enter now saves - Space selects - Removed A Updated colors to be more readable and colorblind friendly. Cleaned up instructional text to be easier and more simple to read.
331 lines
8.8 KiB
Go
331 lines
8.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
|
|
}
|
|
|
|
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.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 {
|
|
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() {
|
|
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 {
|
|
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:
|
|
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 {
|
|
p.toggleExpansion(p.highlightIndex)
|
|
}
|
|
case sdl.K_BACKSPACE, sdl.K_ESCAPE:
|
|
p.Context.NavigateTo(SETTINGS)
|
|
}
|
|
p.layout()
|
|
}
|
|
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(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
|
|
}
|