Search works
This commit is contained in:
@@ -3,14 +3,17 @@ package page
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/go-gl/gl/v3.3-core/gl"
|
"github.com/go-gl/gl/v3.3-core/gl"
|
||||||
"github.com/go-gl/mathgl/mgl32"
|
"github.com/go-gl/mathgl/mgl32"
|
||||||
"github.com/veandco/go-sdl2/sdl"
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
"gorm.io/gorm" // Ensure correct GORM import
|
"gorm.io/gorm"
|
||||||
"rhemawavetech.com/rhemawave/internal/colors"
|
"rhemawavetech.com/rhemawave/internal/colors"
|
||||||
"rhemawavetech.com/rhemawave/internal/db"
|
"rhemawavetech.com/rhemawave/internal/db"
|
||||||
"rhemawavetech.com/rhemawave/internal/fontManager"
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
||||||
|
"rhemawavetech.com/rhemawave/internal/logger"
|
||||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@ type FlatItem struct {
|
|||||||
IsSubCategory bool
|
IsSubCategory bool
|
||||||
Depth int
|
Depth int
|
||||||
Expanded bool
|
Expanded bool
|
||||||
|
SearchText string // lowercase name + content, pre-built at load time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProclamationOptions struct {
|
type ProclamationOptions struct {
|
||||||
@@ -37,12 +41,12 @@ type ProclamationOptions struct {
|
|||||||
title StringItem
|
title StringItem
|
||||||
prompt StringItem
|
prompt StringItem
|
||||||
|
|
||||||
// Tree State
|
// Browse Tree State
|
||||||
allItems []*FlatItem
|
allItems []*FlatItem
|
||||||
activeMap map[string]bool
|
activeMap map[string]bool
|
||||||
highlightIndex int
|
highlightIndex int
|
||||||
|
|
||||||
// Scrolling State
|
// Browse Scrolling State
|
||||||
scrollOffset int
|
scrollOffset int
|
||||||
maxVisible int
|
maxVisible int
|
||||||
|
|
||||||
@@ -55,7 +59,16 @@ type ProclamationOptions struct {
|
|||||||
indentSize float32
|
indentSize float32
|
||||||
verticalMargin int32
|
verticalMargin int32
|
||||||
|
|
||||||
|
// Search State
|
||||||
isSearchActive bool
|
isSearchActive bool
|
||||||
|
searchQuery string
|
||||||
|
searchInput StringItem
|
||||||
|
searchItems []*FlatItem // filtered view — pointers into allProclamations
|
||||||
|
searchHighlight int
|
||||||
|
searchScrollOff int
|
||||||
|
|
||||||
|
// Full flat proclamation pool (pointers shared with allItems leaves)
|
||||||
|
allProclamations []*FlatItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||||
@@ -83,7 +96,7 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
|||||||
p.title, _ = NewStringItem("Proclamation Browser", p.font, colors.White)
|
p.title, _ = NewStringItem("Proclamation Browser", p.font, colors.White)
|
||||||
p.title.Scale(float32(64) / float32(ctx.CfgPtr.StandardFontSize))
|
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, _ = NewStringItem("Type to Search Space to Select Enter to Save Escape to Exit", p.font, colors.White)
|
||||||
p.prompt.Scale(itemScale * 0.8)
|
p.prompt.Scale(itemScale * 0.8)
|
||||||
|
|
||||||
// Calculation for visible area
|
// Calculation for visible area
|
||||||
@@ -97,17 +110,91 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
|||||||
p.activeMap[s.ID] = true
|
p.activeMap[s.ID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load ALL proclamations into a lookup map for SearchText population
|
||||||
|
var allProcs []db.Proclamation
|
||||||
|
if err := p.Context.DbPtr.Find(&allProcs).Error; err != nil {
|
||||||
|
logger.Error("Failed to fetch proclamations: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
procContentMap := make(map[string]string, len(allProcs))
|
||||||
|
for _, pr := range allProcs {
|
||||||
|
procContentMap[pr.ID] = pr.Content
|
||||||
|
}
|
||||||
|
p.allProclamations = []*FlatItem{}
|
||||||
|
|
||||||
// Load Root Categories
|
// Load Root Categories
|
||||||
var roots []db.ProclamationCategory
|
var parentCategories []db.ProclamationCategory
|
||||||
p.Context.DbPtr.Where("parent_id IS NULL OR parent_id = 0").Find(&roots)
|
p.Context.DbPtr.Where("parent_id IS NULL OR parent_id = 0").Find(&parentCategories)
|
||||||
for _, c := range roots {
|
for _, c := range parentCategories {
|
||||||
p.allItems = append(p.allItems, p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, 0))
|
p.allItems = append(p.allItems, p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expand all root categories so allProclamations can be populated
|
||||||
|
for i := 0; i < len(p.allItems); i++ {
|
||||||
|
if p.allItems[i].IsCategory && !p.allItems[i].IsSubCategory {
|
||||||
|
p.expandAndCollectProcs(i, procContentMap)
|
||||||
|
p.allItems[i].Expanded = true
|
||||||
|
itemScale2 := float32(32) / float32(ctx.CfgPtr.StandardFontSize)
|
||||||
|
pureName := p.allItems[i].Label.content[4:]
|
||||||
|
gl.DeleteTextures(1, &p.allItems[i].Label.ID)
|
||||||
|
p.allItems[i].Label, _ = NewStringItem("[-] "+pureName, p.font, colors.White)
|
||||||
|
p.allItems[i].Label.Scale(itemScale2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.layout()
|
p.layout()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expandAndCollectProcs is like expand() but also populates SearchText and allProclamations.
|
||||||
|
// Used only during Init to build the initial fully-expanded tree.
|
||||||
|
func (p *ProclamationOptions) expandAndCollectProcs(index int, procContentMap map[string]string) {
|
||||||
|
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
|
||||||
|
newItems = append(newItems, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
var procs []db.Proclamation
|
||||||
|
p.Context.DbPtr.Where("category_id = ?", parentID).Find(&procs)
|
||||||
|
for _, pr := range procs {
|
||||||
|
fi := p.createFlatItem(pr.Name, pr.ID, false, parent.Depth+1)
|
||||||
|
fi.SearchText = strings.ToLower(pr.Name + " " + pr.Content)
|
||||||
|
newItems = append(newItems, fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
tail := append([]*FlatItem{}, p.allItems[index+1:]...)
|
||||||
|
p.allItems = append(p.allItems[:index+1], append(newItems, tail...)...)
|
||||||
|
|
||||||
|
// Recurse into subcategories and collect their proclamations too
|
||||||
|
i := index + 1
|
||||||
|
for i < len(p.allItems) && p.allItems[i].Depth > parent.Depth {
|
||||||
|
item := p.allItems[i]
|
||||||
|
if item.IsCategory {
|
||||||
|
p.expandAndCollectProcs(i, procContentMap)
|
||||||
|
} else {
|
||||||
|
// It's a leaf — add to pool if not already present
|
||||||
|
alreadyIn := false
|
||||||
|
for _, existing := range p.allProclamations {
|
||||||
|
if existing.ID == item.ID {
|
||||||
|
alreadyIn = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyIn {
|
||||||
|
p.allProclamations = append(p.allProclamations, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ProclamationOptions) createFlatItem(name, id string, isCat bool, depth int) *FlatItem {
|
func (p *ProclamationOptions) createFlatItem(name, id string, isCat bool, depth int) *FlatItem {
|
||||||
text := name
|
text := name
|
||||||
if isCat && depth == 0 {
|
if isCat && depth == 0 {
|
||||||
@@ -135,14 +222,35 @@ func (p *ProclamationOptions) layout() {
|
|||||||
p.scrollTrackY = 130
|
p.scrollTrackY = 130
|
||||||
p.scrollTrackHeight = int32(startY) - 130 + 50
|
p.scrollTrackHeight = int32(startY) - 130 + 50
|
||||||
|
|
||||||
for i, item := range p.allItems {
|
if p.isSearchActive {
|
||||||
item.Label.X = -5000 // Hidden
|
// Position search bar label
|
||||||
|
p.searchInput.X = 60
|
||||||
|
p.searchInput.Y = 100
|
||||||
|
|
||||||
|
// Hide all browse items
|
||||||
|
for _, item := range p.allItems {
|
||||||
|
item.Label.X = -5000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position filtered search results
|
||||||
|
for i, item := range p.searchItems {
|
||||||
|
item.Label.X = -5000
|
||||||
|
if i >= p.searchScrollOff && i < p.searchScrollOff+p.maxVisible {
|
||||||
|
visibleIdx := i - p.searchScrollOff
|
||||||
|
item.Label.X = 60
|
||||||
|
item.Label.Y = int32(startY - float32(visibleIdx)*47.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse mode
|
||||||
|
for i, item := range p.allItems {
|
||||||
|
item.Label.X = -5000
|
||||||
if i >= p.scrollOffset && i < p.scrollOffset+p.maxVisible {
|
if i >= p.scrollOffset && i < p.scrollOffset+p.maxVisible {
|
||||||
visibleIdx := i - p.scrollOffset
|
visibleIdx := i - p.scrollOffset
|
||||||
// Cast to int32 for StringItem coordinates
|
item.Label.X = int32(60.0 + float32(item.Depth)*p.indentSize)
|
||||||
item.Label.X = int32(60.0 + (float32(item.Depth) * p.indentSize))
|
item.Label.Y = int32(startY - float32(visibleIdx)*47.0)
|
||||||
item.Label.Y = int32(startY - (float32(visibleIdx) * 47.0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +258,7 @@ func (p *ProclamationOptions) layout() {
|
|||||||
func (p *ProclamationOptions) toggleExpansion(index int) {
|
func (p *ProclamationOptions) toggleExpansion(index int) {
|
||||||
item := p.allItems[index]
|
item := p.allItems[index]
|
||||||
if !item.IsCategory || item.IsSubCategory {
|
if !item.IsCategory || item.IsSubCategory {
|
||||||
return // Subcategories are never manually toggled
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pureName := item.Label.content[4:]
|
pureName := item.Label.content[4:]
|
||||||
@@ -178,25 +286,37 @@ func (p *ProclamationOptions) expand(index int) {
|
|||||||
p.Context.DbPtr.Where("parent_id = ?", parentID).Find(&cats)
|
p.Context.DbPtr.Where("parent_id = ?", parentID).Find(&cats)
|
||||||
for _, c := range cats {
|
for _, c := range cats {
|
||||||
fi := p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, parent.Depth+1)
|
fi := p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, parent.Depth+1)
|
||||||
fi.Expanded = true // Mark expanded so collapse state is consistent
|
fi.Expanded = true
|
||||||
newItems = append(newItems, fi)
|
newItems = append(newItems, fi)
|
||||||
}
|
}
|
||||||
|
|
||||||
var procs []db.Proclamation
|
var procs []db.Proclamation
|
||||||
p.Context.DbPtr.Where("category_id = ?", parentID).Find(&procs)
|
p.Context.DbPtr.Where("category_id = ?", parentID).Find(&procs)
|
||||||
for _, pr := range procs {
|
for _, pr := range procs {
|
||||||
newItems = append(newItems, p.createFlatItem(pr.Name, pr.ID, false, parent.Depth+1))
|
// Reuse the existing FlatItem pointer from allProclamations if available,
|
||||||
|
// so search pool stays valid without duplication.
|
||||||
|
var fi *FlatItem
|
||||||
|
for _, existing := range p.allProclamations {
|
||||||
|
if existing.ID == pr.ID {
|
||||||
|
fi = existing
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fi == nil {
|
||||||
|
fi = p.createFlatItem(pr.Name, pr.ID, false, parent.Depth+1)
|
||||||
|
fi.SearchText = strings.ToLower(pr.Name + " " + pr.Content)
|
||||||
|
p.allProclamations = append(p.allProclamations, fi)
|
||||||
|
}
|
||||||
|
newItems = append(newItems, fi)
|
||||||
}
|
}
|
||||||
|
|
||||||
tail := append([]*FlatItem{}, p.allItems[index+1:]...)
|
tail := append([]*FlatItem{}, p.allItems[index+1:]...)
|
||||||
p.allItems = append(p.allItems[:index+1], append(newItems, tail...)...)
|
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
|
i := index + 1
|
||||||
for i < len(p.allItems) && p.allItems[i].Depth > parent.Depth {
|
for i < len(p.allItems) && p.allItems[i].Depth > parent.Depth {
|
||||||
if p.allItems[i].IsCategory && p.allItems[i].Depth > 0 {
|
if p.allItems[i].IsCategory && p.allItems[i].Depth > 0 {
|
||||||
p.expand(i) // Recurse — inserts children in-place, loop will walk into them
|
p.expand(i)
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
@@ -206,19 +326,175 @@ func (p *ProclamationOptions) collapse(index int) {
|
|||||||
depth := p.allItems[index].Depth
|
depth := p.allItems[index].Depth
|
||||||
i := index + 1
|
i := index + 1
|
||||||
for i < len(p.allItems) && p.allItems[i].Depth > depth {
|
for i < len(p.allItems) && p.allItems[i].Depth > depth {
|
||||||
|
// Do NOT delete textures here — leaves are shared with allProclamations.
|
||||||
|
// Only delete textures for category items that aren't in the proclamation pool.
|
||||||
|
if p.allItems[i].IsCategory {
|
||||||
gl.DeleteTextures(1, &p.allItems[i].Label.ID)
|
gl.DeleteTextures(1, &p.allItems[i].Label.ID)
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
p.allItems = append(p.allItems[:index+1], p.allItems[i:]...)
|
p.allItems = append(p.allItems[:index+1], p.allItems[i:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchesWordPrefix returns true if any word in text starts with query (case-insensitive).
|
||||||
|
func matchesWordPrefix(text, query string) bool {
|
||||||
|
q := strings.ToLower(query)
|
||||||
|
words := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
|
||||||
|
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
|
||||||
|
})
|
||||||
|
for _, w := range words {
|
||||||
|
if strings.HasPrefix(w, q) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProclamationOptions) buildSearchList() {
|
||||||
|
p.searchItems = nil
|
||||||
|
if p.searchQuery == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range p.allProclamations {
|
||||||
|
if matchesWordPrefix(item.SearchText, p.searchQuery) {
|
||||||
|
p.searchItems = append(p.searchItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.searchHighlight >= len(p.searchItems) {
|
||||||
|
p.searchHighlight = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProclamationOptions) rebuildSearchLabel() {
|
||||||
|
if p.searchInput.ID != 0 {
|
||||||
|
gl.DeleteTextures(1, &p.searchInput.ID)
|
||||||
|
p.searchInput.ID = 0
|
||||||
|
}
|
||||||
|
if p.searchQuery == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemScale := float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
|
||||||
|
p.searchInput, _ = NewStringItem("Search: "+p.searchQuery, p.font, colors.Yellow)
|
||||||
|
p.searchInput.Scale(itemScale * 1.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProclamationOptions) enterSearchMode(firstRune rune) {
|
||||||
|
p.searchQuery = string(firstRune)
|
||||||
|
p.isSearchActive = true
|
||||||
|
p.searchScrollOff = 0
|
||||||
|
p.searchHighlight = 0
|
||||||
|
p.buildSearchList()
|
||||||
|
p.rebuildSearchLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProclamationOptions) exitSearchMode() {
|
||||||
|
p.searchQuery = ""
|
||||||
|
p.isSearchActive = false
|
||||||
|
p.searchItems = nil
|
||||||
|
p.searchHighlight = 0
|
||||||
|
p.searchScrollOff = 0
|
||||||
|
if p.searchInput.ID != 0 {
|
||||||
|
gl.DeleteTextures(1, &p.searchInput.ID)
|
||||||
|
p.searchInput.ID = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
||||||
switch e := (*event).(type) {
|
switch e := (*event).(type) {
|
||||||
case *sdl.KeyboardEvent:
|
case *sdl.KeyboardEvent:
|
||||||
if e.Type != sdl.KEYDOWN {
|
if e.Type != sdl.KEYDOWN {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch e.Keysym.Sym {
|
|
||||||
|
sym := e.Keysym.Sym
|
||||||
|
|
||||||
|
// --- Backspace: always handled specially ---
|
||||||
|
if sym == sdl.K_BACKSPACE {
|
||||||
|
if p.isSearchActive {
|
||||||
|
if len(p.searchQuery) > 0 {
|
||||||
|
p.searchQuery = p.searchQuery[:len(p.searchQuery)-1]
|
||||||
|
}
|
||||||
|
if p.searchQuery == "" {
|
||||||
|
p.exitSearchMode()
|
||||||
|
} else {
|
||||||
|
p.buildSearchList()
|
||||||
|
p.rebuildSearchLabel()
|
||||||
|
}
|
||||||
|
p.layout()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Browse mode backspace — navigate back
|
||||||
|
p.Context.NavigateTo(SETTINGS)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Escape ---
|
||||||
|
if sym == sdl.K_ESCAPE {
|
||||||
|
if p.isSearchActive {
|
||||||
|
p.exitSearchMode()
|
||||||
|
p.layout()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.Context.NavigateTo(SETTINGS)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Search mode input handling ---
|
||||||
|
if p.isSearchActive {
|
||||||
|
switch sym {
|
||||||
|
case sdl.K_UP:
|
||||||
|
if p.searchHighlight > 0 {
|
||||||
|
p.searchHighlight--
|
||||||
|
if p.searchHighlight < p.searchScrollOff {
|
||||||
|
p.searchScrollOff--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case sdl.K_DOWN:
|
||||||
|
if p.searchHighlight < len(p.searchItems)-1 {
|
||||||
|
p.searchHighlight++
|
||||||
|
if p.searchHighlight >= p.searchScrollOff+p.maxVisible {
|
||||||
|
p.searchScrollOff++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case sdl.K_PAGEUP:
|
||||||
|
p.searchHighlight = p.searchScrollOff
|
||||||
|
p.searchScrollOff -= p.maxVisible - 1
|
||||||
|
if p.searchScrollOff < 0 {
|
||||||
|
p.searchScrollOff = 0
|
||||||
|
p.searchHighlight = 0
|
||||||
|
}
|
||||||
|
case sdl.K_PAGEDOWN:
|
||||||
|
bottom := p.searchScrollOff + p.maxVisible - 1
|
||||||
|
if bottom >= len(p.searchItems) {
|
||||||
|
bottom = len(p.searchItems) - 1
|
||||||
|
}
|
||||||
|
p.searchScrollOff = bottom
|
||||||
|
p.searchHighlight = bottom
|
||||||
|
case sdl.K_SPACE:
|
||||||
|
if len(p.searchItems) > 0 {
|
||||||
|
item := p.searchItems[p.searchHighlight]
|
||||||
|
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
||||||
|
}
|
||||||
|
case sdl.K_RETURN:
|
||||||
|
p.acceptChanges()
|
||||||
|
default:
|
||||||
|
// Append printable characters to search query
|
||||||
|
r := rune(sym)
|
||||||
|
if r >= 33 && r <= 126 {
|
||||||
|
if e.Keysym.Mod&sdl.KMOD_SHIFT != 0 {
|
||||||
|
r = unicode.ToUpper(r)
|
||||||
|
}
|
||||||
|
p.searchQuery += string(r)
|
||||||
|
p.buildSearchList()
|
||||||
|
p.rebuildSearchLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.layout()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Browse mode ---
|
||||||
|
switch sym {
|
||||||
case sdl.K_UP:
|
case sdl.K_UP:
|
||||||
if p.highlightIndex > 0 {
|
if p.highlightIndex > 0 {
|
||||||
p.highlightIndex--
|
p.highlightIndex--
|
||||||
@@ -235,7 +511,7 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
}
|
}
|
||||||
case sdl.K_PAGEUP:
|
case sdl.K_PAGEUP:
|
||||||
p.highlightIndex = p.scrollOffset
|
p.highlightIndex = p.scrollOffset
|
||||||
p.scrollOffset -= (p.maxVisible - 1)
|
p.scrollOffset -= p.maxVisible - 1
|
||||||
if p.scrollOffset < 0 {
|
if p.scrollOffset < 0 {
|
||||||
p.scrollOffset = 0
|
p.scrollOffset = 0
|
||||||
p.highlightIndex = 0
|
p.highlightIndex = 0
|
||||||
@@ -256,23 +532,22 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
} else if !item.IsSubCategory {
|
} else if !item.IsSubCategory {
|
||||||
p.toggleExpansion(p.highlightIndex)
|
p.toggleExpansion(p.highlightIndex)
|
||||||
}
|
}
|
||||||
case sdl.K_BACKSPACE, sdl.K_ESCAPE:
|
|
||||||
p.Context.NavigateTo(SETTINGS)
|
|
||||||
default:
|
default:
|
||||||
// Treat input as search intent, initiate search & filter
|
// Any printable character enters search mode
|
||||||
p.toggleTextInput()
|
r := rune(sym)
|
||||||
p.isSearchActive = true
|
if r >= 33 && r <= 126 {
|
||||||
|
if e.Keysym.Mod&sdl.KMOD_SHIFT != 0 {
|
||||||
|
r = unicode.ToUpper(r)
|
||||||
}
|
}
|
||||||
|
p.enterSearchMode(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p.layout()
|
p.layout()
|
||||||
}
|
}
|
||||||
return nil
|
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() {
|
func (p *ProclamationOptions) acceptChanges() {
|
||||||
p.Context.DbPtr.Transaction(func(tx *gorm.DB) error {
|
p.Context.DbPtr.Transaction(func(tx *gorm.DB) error {
|
||||||
tx.Model(&db.Proclamation{}).Where("1 = 1").Update("selected", false)
|
tx.Model(&db.Proclamation{}).Where("1 = 1").Update("selected", false)
|
||||||
@@ -294,11 +569,43 @@ 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.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)
|
p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color)
|
||||||
|
|
||||||
|
if p.isSearchActive {
|
||||||
|
// Search bar label
|
||||||
|
if p.searchInput.ID != 0 {
|
||||||
|
p.RenderTexture(p.textShader, p.searchInput.ID, p.searchInput.Position(), p.searchInput.RawDimensions(), p.searchInput.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.searchItems) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight box for search results
|
||||||
|
if p.searchHighlight >= p.searchScrollOff && p.searchHighlight < p.searchScrollOff+p.maxVisible {
|
||||||
|
target := p.searchItems[p.searchHighlight].Label
|
||||||
|
p.RenderSolidColorQuad(p.solidShader, target.Position(), target.RawDimensions(), colors.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := p.searchScrollOff; i < p.searchScrollOff+p.maxVisible && i < len(p.searchItems); i++ {
|
||||||
|
item := p.searchItems[i]
|
||||||
|
if i == p.searchHighlight {
|
||||||
|
item.Label.Color = colors.Black
|
||||||
|
} 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.renderSearchScrollbar()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse mode
|
||||||
if len(p.allItems) == 0 {
|
if len(p.allItems) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight Box
|
|
||||||
if p.highlightIndex >= p.scrollOffset && p.highlightIndex < p.scrollOffset+p.maxVisible {
|
if p.highlightIndex >= p.scrollOffset && p.highlightIndex < p.scrollOffset+p.maxVisible {
|
||||||
target := p.allItems[p.highlightIndex].Label
|
target := p.allItems[p.highlightIndex].Label
|
||||||
p.RenderSolidColorQuad(p.solidShader, target.Position(), target.RawDimensions(), colors.White)
|
p.RenderSolidColorQuad(p.solidShader, target.Position(), target.RawDimensions(), colors.White)
|
||||||
@@ -328,22 +635,44 @@ func (p *ProclamationOptions) renderScrollbar() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Render Track
|
|
||||||
p.RenderSolidColorQuad(p.solidShader,
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)},
|
mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)},
|
||||||
mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackHeight)},
|
mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackHeight)},
|
||||||
colorScrollTrack)
|
colorScrollTrack)
|
||||||
|
|
||||||
// Thumb Math
|
|
||||||
visibleRatio := float32(p.maxVisible) / float32(total)
|
visibleRatio := float32(p.maxVisible) / float32(total)
|
||||||
scrollRatio := float32(p.scrollOffset) / float32(total)
|
scrollRatio := float32(p.scrollOffset) / float32(total)
|
||||||
thumbHeight := float32(p.scrollTrackHeight) * visibleRatio
|
thumbHeight := float32(p.scrollTrackHeight) * visibleRatio
|
||||||
if thumbHeight < 20 {
|
if thumbHeight < 20 {
|
||||||
thumbHeight = 20
|
thumbHeight = 20
|
||||||
}
|
}
|
||||||
thumbY := float32(p.scrollTrackY) + (float32(p.scrollTrackHeight) * scrollRatio)
|
thumbY := float32(p.scrollTrackY) + float32(p.scrollTrackHeight)*scrollRatio
|
||||||
|
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY},
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight},
|
||||||
|
colorScrollThumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProclamationOptions) renderSearchScrollbar() {
|
||||||
|
total := len(p.searchItems)
|
||||||
|
if total <= p.maxVisible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)},
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackHeight)},
|
||||||
|
colorScrollTrack)
|
||||||
|
|
||||||
|
visibleRatio := float32(p.maxVisible) / float32(total)
|
||||||
|
scrollRatio := float32(p.searchScrollOff) / 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,
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY},
|
mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY},
|
||||||
mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight},
|
mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight},
|
||||||
@@ -353,7 +682,17 @@ func (p *ProclamationOptions) renderScrollbar() {
|
|||||||
func (p *ProclamationOptions) Destroy() error {
|
func (p *ProclamationOptions) Destroy() error {
|
||||||
gl.DeleteTextures(1, &p.title.ID)
|
gl.DeleteTextures(1, &p.title.ID)
|
||||||
gl.DeleteTextures(1, &p.prompt.ID)
|
gl.DeleteTextures(1, &p.prompt.ID)
|
||||||
|
if p.searchInput.ID != 0 {
|
||||||
|
gl.DeleteTextures(1, &p.searchInput.ID)
|
||||||
|
}
|
||||||
|
// Delete browse category textures
|
||||||
for _, item := range p.allItems {
|
for _, item := range p.allItems {
|
||||||
|
if item.IsCategory {
|
||||||
|
gl.DeleteTextures(1, &item.Label.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete proclamation textures via the pool (single source of truth)
|
||||||
|
for _, item := range p.allProclamations {
|
||||||
gl.DeleteTextures(1, &item.Label.ID)
|
gl.DeleteTextures(1, &item.Label.ID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user