Migrate scroller page to arrow pointer navigation
Change ENTER and SPACE to perform the same actions, and also save on every update.
This commit is contained in:
@@ -2,6 +2,7 @@ package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -71,6 +72,15 @@ type ProclamationOptions struct {
|
||||
|
||||
// Full flat proclamation pool (pointers shared with allItems leaves)
|
||||
allProclamations []*FlatItem
|
||||
|
||||
// Spinning triangle cursor — same shape/animation as AudioOptions —
|
||||
// that points at whichever row is currently highlighted.
|
||||
triVAO uint32
|
||||
triVBO uint32
|
||||
triX float32
|
||||
triCenterY float32
|
||||
triVisible bool
|
||||
spinElapsed float32
|
||||
}
|
||||
|
||||
func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||
@@ -147,6 +157,23 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||
p.allItems = append(p.allItems, p.createFlatItem(c.Name, fmt.Sprint(c.ID), true, 0))
|
||||
}
|
||||
|
||||
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords.
|
||||
// Mirrors the spinning cursor used in AudioOptions.
|
||||
triVerts := []float32{
|
||||
0.5, 0.0,
|
||||
-0.5, 0.5,
|
||||
-0.5, -0.5,
|
||||
}
|
||||
gl.GenVertexArrays(1, &p.triVAO)
|
||||
gl.GenBuffers(1, &p.triVBO)
|
||||
gl.BindVertexArray(p.triVAO)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, p.triVBO)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(triVerts)*4, gl.Ptr(triVerts), gl.STATIC_DRAW)
|
||||
gl.EnableVertexAttribArray(0)
|
||||
gl.VertexAttribPointerWithOffset(0, 2, gl.FLOAT, false, 2*4, 0)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
gl.BindVertexArray(0)
|
||||
|
||||
p.layout()
|
||||
return nil
|
||||
}
|
||||
@@ -178,6 +205,12 @@ func (p *ProclamationOptions) layout() {
|
||||
p.scrollTrackY = 130
|
||||
p.scrollTrackHeight = int32(startY) - 130 + 50
|
||||
|
||||
// Reserve room on the left of the list for the spinning cursor
|
||||
// (triangleSize wide, cursorGap away from the text it points at),
|
||||
// ahead of the original left margin used for list rows.
|
||||
cursorGap := int32(12)
|
||||
listBaseX := int32(60) + int32(triangleSize) + cursorGap
|
||||
|
||||
if p.isSearchActive {
|
||||
// Position search bar label
|
||||
p.searchInput.X = 60
|
||||
@@ -193,10 +226,21 @@ func (p *ProclamationOptions) layout() {
|
||||
item.Label.X = -5000
|
||||
if i >= p.searchScrollOff && i < p.searchScrollOff+p.maxVisible {
|
||||
visibleIdx := i - p.searchScrollOff
|
||||
item.Label.X = 60
|
||||
item.Label.X = listBaseX
|
||||
item.Label.Y = int32(startY - float32(visibleIdx)*47.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Point the cursor at the highlighted search result, if it's
|
||||
// currently scrolled into view.
|
||||
p.triVisible = false
|
||||
if len(p.searchItems) > 0 && p.searchHighlight >= 0 && p.searchHighlight < len(p.searchItems) &&
|
||||
p.searchHighlight >= p.searchScrollOff && p.searchHighlight < p.searchScrollOff+p.maxVisible {
|
||||
target := p.searchItems[p.searchHighlight].Label
|
||||
p.triX = float32(target.X) - triangleSize - float32(cursorGap)
|
||||
p.triCenterY = float32(target.Y) + float32(target.H)/2
|
||||
p.triVisible = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,10 +249,23 @@ func (p *ProclamationOptions) layout() {
|
||||
item.Label.X = -5000
|
||||
if i >= p.scrollOffset && i < p.scrollOffset+p.maxVisible {
|
||||
visibleIdx := i - p.scrollOffset
|
||||
item.Label.X = int32(60.0 + float32(item.Depth)*p.indentSize)
|
||||
item.Label.X = listBaseX + int32(float32(item.Depth)*p.indentSize)
|
||||
item.Label.Y = int32(startY - float32(visibleIdx)*47.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Point the cursor at the highlighted browse row, if it's currently
|
||||
// scrolled into view. Since target.X already includes the row's depth
|
||||
// indentation, the cursor shifts in and out with it automatically —
|
||||
// the gap between cursor and text stays constant regardless of depth.
|
||||
p.triVisible = false
|
||||
if len(p.allItems) > 0 && p.highlightIndex >= 0 && p.highlightIndex < len(p.allItems) &&
|
||||
p.highlightIndex >= p.scrollOffset && p.highlightIndex < p.scrollOffset+p.maxVisible {
|
||||
target := p.allItems[p.highlightIndex].Label
|
||||
p.triX = float32(target.X) - triangleSize - float32(cursorGap)
|
||||
p.triCenterY = float32(target.Y) + float32(target.H)/2
|
||||
p.triVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProclamationOptions) toggleExpansion(index int) {
|
||||
@@ -445,7 +502,12 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
||||
item := p.searchItems[p.searchHighlight]
|
||||
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
||||
}
|
||||
p.acceptChanges()
|
||||
case sdl.K_RETURN:
|
||||
if len(p.searchItems) > 0 {
|
||||
item := p.searchItems[p.searchHighlight]
|
||||
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
||||
}
|
||||
p.acceptChanges()
|
||||
default:
|
||||
// Append printable characters to search query
|
||||
@@ -494,11 +556,18 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
||||
p.scrollOffset = bottomIdx
|
||||
p.highlightIndex = bottomIdx
|
||||
case sdl.K_RETURN:
|
||||
p.acceptChanges()
|
||||
item := p.allItems[p.highlightIndex]
|
||||
if !item.IsCategory { // If the item is NOT a Category
|
||||
p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list
|
||||
p.acceptChanges()
|
||||
} else if !item.IsSubCategory {
|
||||
p.toggleExpansion(p.highlightIndex)
|
||||
}
|
||||
case sdl.K_SPACE:
|
||||
item := p.allItems[p.highlightIndex]
|
||||
if !item.IsCategory { // If the item is NOT a Category
|
||||
p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list
|
||||
p.acceptChanges()
|
||||
} else if !item.IsSubCategory {
|
||||
p.toggleExpansion(p.highlightIndex)
|
||||
}
|
||||
@@ -529,10 +598,21 @@ func (p *ProclamationOptions) acceptChanges() {
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
tx.Model(&db.Proclamation{}).Where("id IN ?", ids).Update("selected", true)
|
||||
logger.Debug("Saving selected state for %s\n", ids)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
p.Context.NavigateTo(SCROLLER)
|
||||
}
|
||||
|
||||
// Update advances the spinning-cursor animation. The cursor's *position* is
|
||||
// recomputed in layout() whenever the highlighted row changes; this only
|
||||
// drives the spin itself, frame to frame.
|
||||
func (p *ProclamationOptions) Update(dt float32) error {
|
||||
p.spinElapsed += dt
|
||||
if p.spinElapsed >= 1.0 {
|
||||
p.spinElapsed -= 1.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProclamationOptions) Render() error {
|
||||
@@ -549,17 +629,9 @@ func (p *ProclamationOptions) Render() error {
|
||||
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] {
|
||||
if p.activeMap[item.ID] {
|
||||
item.Label.Color = colors.Orange
|
||||
} else {
|
||||
item.Label.Color = colors.White
|
||||
@@ -567,6 +639,9 @@ func (p *ProclamationOptions) Render() error {
|
||||
p.RenderTexture(p.textShader, item.Label.ID, item.Label.Position(), item.Label.RawDimensions(), item.Label.Color)
|
||||
}
|
||||
|
||||
// Spinning triangle cursor — always on top, points at the highlighted result
|
||||
p.renderCursor()
|
||||
|
||||
p.renderSearchScrollbar()
|
||||
return nil
|
||||
}
|
||||
@@ -576,16 +651,9 @@ func (p *ProclamationOptions) Render() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
if item.IsCategory {
|
||||
item.Label.Color = colors.DodgerBlue
|
||||
} else if p.activeMap[item.ID] {
|
||||
item.Label.Color = colors.Orange
|
||||
@@ -595,10 +663,46 @@ func (p *ProclamationOptions) Render() error {
|
||||
p.RenderTexture(p.textShader, item.Label.ID, item.Label.Position(), item.Label.RawDimensions(), item.Label.Color)
|
||||
}
|
||||
|
||||
// Spinning triangle cursor — always on top, points at the highlighted row
|
||||
p.renderCursor()
|
||||
|
||||
p.renderScrollbar()
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCursor draws the spinning triangle cursor pointing at whichever row
|
||||
// is currently highlighted, in either browse or search mode. Identical
|
||||
// shape/animation to the cursor in AudioOptions.
|
||||
func (p *ProclamationOptions) renderCursor() error {
|
||||
if !p.triVisible {
|
||||
return nil
|
||||
}
|
||||
|
||||
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
||||
|
||||
model := mgl32.Ident4()
|
||||
model = model.Mul4(mgl32.Translate3D(p.triX+triangleSize/2, p.triCenterY, 0))
|
||||
model = model.Mul4(mgl32.Scale3D(triangleSize, triangleSize*scaleY, 1))
|
||||
mvp := p.Projection.Mul4(model)
|
||||
|
||||
gl.UseProgram(p.solidShader.ProgramID)
|
||||
gl.UniformMatrix4fv(p.solidShader.LocMVPMatrix, 1, false, &mvp[0])
|
||||
gl.Uniform4f(
|
||||
p.solidShader.LocColor,
|
||||
float32(colors.White.R)/255.0,
|
||||
float32(colors.White.G)/255.0,
|
||||
float32(colors.White.B)/255.0,
|
||||
float32(colors.White.A)/255.0,
|
||||
)
|
||||
|
||||
gl.BindVertexArray(p.triVAO)
|
||||
gl.DrawArrays(gl.TRIANGLES, 0, 3)
|
||||
gl.BindVertexArray(0)
|
||||
gl.UseProgram(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProclamationOptions) renderScrollbar() {
|
||||
total := len(p.allItems)
|
||||
if total <= p.maxVisible {
|
||||
@@ -665,5 +769,7 @@ func (p *ProclamationOptions) Destroy() error {
|
||||
for _, item := range p.allProclamations {
|
||||
gl.DeleteTextures(1, &item.Label.ID)
|
||||
}
|
||||
gl.DeleteBuffers(1, &p.triVBO)
|
||||
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user