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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -71,6 +72,15 @@ type ProclamationOptions struct {
|
|||||||
|
|
||||||
// Full flat proclamation pool (pointers shared with allItems leaves)
|
// Full flat proclamation pool (pointers shared with allItems leaves)
|
||||||
allProclamations []*FlatItem
|
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 {
|
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))
|
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()
|
p.layout()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -178,6 +205,12 @@ func (p *ProclamationOptions) layout() {
|
|||||||
p.scrollTrackY = 130
|
p.scrollTrackY = 130
|
||||||
p.scrollTrackHeight = int32(startY) - 130 + 50
|
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 {
|
if p.isSearchActive {
|
||||||
// Position search bar label
|
// Position search bar label
|
||||||
p.searchInput.X = 60
|
p.searchInput.X = 60
|
||||||
@@ -193,10 +226,21 @@ func (p *ProclamationOptions) layout() {
|
|||||||
item.Label.X = -5000
|
item.Label.X = -5000
|
||||||
if i >= p.searchScrollOff && i < p.searchScrollOff+p.maxVisible {
|
if i >= p.searchScrollOff && i < p.searchScrollOff+p.maxVisible {
|
||||||
visibleIdx := i - p.searchScrollOff
|
visibleIdx := i - p.searchScrollOff
|
||||||
item.Label.X = 60
|
item.Label.X = listBaseX
|
||||||
item.Label.Y = int32(startY - float32(visibleIdx)*47.0)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +249,23 @@ func (p *ProclamationOptions) layout() {
|
|||||||
item.Label.X = -5000
|
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
|
||||||
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)
|
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) {
|
func (p *ProclamationOptions) toggleExpansion(index int) {
|
||||||
@@ -445,7 +502,12 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
item := p.searchItems[p.searchHighlight]
|
item := p.searchItems[p.searchHighlight]
|
||||||
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
||||||
}
|
}
|
||||||
|
p.acceptChanges()
|
||||||
case sdl.K_RETURN:
|
case sdl.K_RETURN:
|
||||||
|
if len(p.searchItems) > 0 {
|
||||||
|
item := p.searchItems[p.searchHighlight]
|
||||||
|
p.activeMap[item.ID] = !p.activeMap[item.ID]
|
||||||
|
}
|
||||||
p.acceptChanges()
|
p.acceptChanges()
|
||||||
default:
|
default:
|
||||||
// Append printable characters to search query
|
// Append printable characters to search query
|
||||||
@@ -494,11 +556,18 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
p.scrollOffset = bottomIdx
|
p.scrollOffset = bottomIdx
|
||||||
p.highlightIndex = bottomIdx
|
p.highlightIndex = bottomIdx
|
||||||
case sdl.K_RETURN:
|
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:
|
case sdl.K_SPACE:
|
||||||
item := p.allItems[p.highlightIndex]
|
item := p.allItems[p.highlightIndex]
|
||||||
if !item.IsCategory { // If the item is NOT a Category
|
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.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list
|
||||||
|
p.acceptChanges()
|
||||||
} else if !item.IsSubCategory {
|
} else if !item.IsSubCategory {
|
||||||
p.toggleExpansion(p.highlightIndex)
|
p.toggleExpansion(p.highlightIndex)
|
||||||
}
|
}
|
||||||
@@ -529,10 +598,21 @@ func (p *ProclamationOptions) acceptChanges() {
|
|||||||
}
|
}
|
||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
tx.Model(&db.Proclamation{}).Where("id IN ?", ids).Update("selected", true)
|
tx.Model(&db.Proclamation{}).Where("id IN ?", ids).Update("selected", true)
|
||||||
|
logger.Debug("Saving selected state for %s\n", ids)
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func (p *ProclamationOptions) Render() error {
|
||||||
@@ -549,17 +629,9 @@ func (p *ProclamationOptions) Render() error {
|
|||||||
return nil
|
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++ {
|
for i := p.searchScrollOff; i < p.searchScrollOff+p.maxVisible && i < len(p.searchItems); i++ {
|
||||||
item := p.searchItems[i]
|
item := p.searchItems[i]
|
||||||
if i == p.searchHighlight {
|
if p.activeMap[item.ID] {
|
||||||
item.Label.Color = colors.Black
|
|
||||||
} else if p.activeMap[item.ID] {
|
|
||||||
item.Label.Color = colors.Orange
|
item.Label.Color = colors.Orange
|
||||||
} else {
|
} else {
|
||||||
item.Label.Color = colors.White
|
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)
|
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()
|
p.renderSearchScrollbar()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -576,16 +651,9 @@ func (p *ProclamationOptions) Render() error {
|
|||||||
return nil
|
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++ {
|
for i := p.scrollOffset; i < p.scrollOffset+p.maxVisible && i < len(p.allItems); i++ {
|
||||||
item := p.allItems[i]
|
item := p.allItems[i]
|
||||||
if i == p.highlightIndex {
|
if item.IsCategory {
|
||||||
item.Label.Color = colors.Black
|
|
||||||
} else if item.IsCategory {
|
|
||||||
item.Label.Color = colors.DodgerBlue
|
item.Label.Color = colors.DodgerBlue
|
||||||
} else if p.activeMap[item.ID] {
|
} else if p.activeMap[item.ID] {
|
||||||
item.Label.Color = colors.Orange
|
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)
|
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()
|
p.renderScrollbar()
|
||||||
return nil
|
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() {
|
func (p *ProclamationOptions) renderScrollbar() {
|
||||||
total := len(p.allItems)
|
total := len(p.allItems)
|
||||||
if total <= p.maxVisible {
|
if total <= p.maxVisible {
|
||||||
@@ -665,5 +769,7 @@ func (p *ProclamationOptions) Destroy() error {
|
|||||||
for _, item := range p.allProclamations {
|
for _, item := range p.allProclamations {
|
||||||
gl.DeleteTextures(1, &item.Label.ID)
|
gl.DeleteTextures(1, &item.Label.ID)
|
||||||
}
|
}
|
||||||
|
gl.DeleteBuffers(1, &p.triVBO)
|
||||||
|
gl.DeleteVertexArrays(1, &p.triVAO)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user