Database is more usable now. All pages have been updated to use the database, and share an application configuration. Page Stack has been removed, no more pages hanging around in memory. Replaced with a 'Router' which uses an Enum to determine which page should be displayed, destroys the old page and initializes the new page all in one go. No more push, pop, rollback, etc. No more holding instances of pages. Logger has been updated with proper timestamps and sorting now works as expected."
251 lines
6.7 KiB
Go
251 lines
6.7 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/go-gl/mathgl/mgl32"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/logger"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
type LogViewerPage struct {
|
|
Base
|
|
|
|
titleTextureID uint32
|
|
titleTextureWidth int32
|
|
titleTextureHeight int32
|
|
|
|
lines []string
|
|
lineTextureIDs []uint32
|
|
lineTextureWidths []int32
|
|
lineTextureHeights []int32
|
|
|
|
hintTextureID uint32
|
|
hintTextureWidth int32
|
|
hintTextureHeight int32
|
|
|
|
scrollOffset int32
|
|
maxScrollOffset int32
|
|
lineHeight int32
|
|
visibleHeight int32
|
|
}
|
|
|
|
func (p *LogViewerPage) Init(ctx *PageCtx) error {
|
|
var logs []db.Log
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
if result := p.Base.Context.DbPtr.Order("timestamp desc").Limit(250).Find(&logs); result.Error != nil {
|
|
logger.Error("error querying database for logs: %s", result.Error)
|
|
return fmt.Errorf("%s", result.Error)
|
|
}
|
|
|
|
// Format and word-wrap each log entry
|
|
// for _, entry := range logs {
|
|
// formatted := fmt.Sprintf("[%s] [%s] %s", entry.Timestamp, entry.Level, entry.Message)
|
|
// wrapped := wrapText(formatted, 100)
|
|
// p.lines = append(p.lines, strings.Split(wrapped, "\n")...)
|
|
// }
|
|
|
|
for _, entry := range logs {
|
|
logTime := time.UnixMilli(entry.Timestamp)
|
|
timeStr := logTime.Format("2006-01-02 15:04:05")
|
|
line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message)
|
|
p.lines = append(p.lines, line)
|
|
}
|
|
|
|
applicationFont := "Roboto-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
textShader, ok := shaderManager.Get().Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch text shader")
|
|
}
|
|
|
|
// Title texture
|
|
titleID, titleW, titleH, err := fm.CreateStringTexture(applicationFont, "Application Logs", textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create title texture: %w", err)
|
|
}
|
|
p.titleTextureID = titleID
|
|
p.titleTextureWidth = titleW
|
|
p.titleTextureHeight = titleH
|
|
|
|
// Line textures
|
|
p.lineTextureIDs = make([]uint32, len(p.lines))
|
|
p.lineTextureWidths = make([]int32, len(p.lines))
|
|
p.lineTextureHeights = make([]int32, len(p.lines))
|
|
|
|
for i, line := range p.lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
texID, w, h, err := fm.CreateStringTexture(applicationFont, line, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create line texture: %w", err)
|
|
}
|
|
p.lineTextureIDs[i] = texID
|
|
p.lineTextureWidths[i] = w
|
|
p.lineTextureHeights[i] = h
|
|
}
|
|
|
|
// Hint texture
|
|
hintText := "Arrow Keys / PgUp/PgDn to scroll | F1|2|3 to Exit"
|
|
hintID, hintW, hintH, err := fm.CreateStringTexture(applicationFont, hintText, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create hint texture: %w", err)
|
|
}
|
|
p.hintTextureID = hintID
|
|
p.hintTextureWidth = hintW
|
|
p.hintTextureHeight = hintH
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *LogViewerPage) HandleEvent(event *sdl.Event) error {
|
|
switch e := (*event).(type) {
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type == sdl.KEYDOWN {
|
|
switch e.Keysym.Sym {
|
|
case sdl.K_BACKSPACE:
|
|
p.Base.Context.NavigateTo(SETTINGS)
|
|
case sdl.K_UP:
|
|
p.scrollOffset -= p.lineHeight
|
|
if p.scrollOffset < 0 {
|
|
p.scrollOffset = 0
|
|
}
|
|
case sdl.K_DOWN:
|
|
p.scrollOffset += p.lineHeight
|
|
if p.scrollOffset > p.maxScrollOffset {
|
|
p.scrollOffset = p.maxScrollOffset
|
|
}
|
|
case sdl.K_PAGEUP:
|
|
p.scrollOffset -= p.visibleHeight
|
|
if p.scrollOffset < 0 {
|
|
p.scrollOffset = 0
|
|
}
|
|
case sdl.K_PAGEDOWN:
|
|
p.scrollOffset += p.visibleHeight
|
|
if p.scrollOffset > p.maxScrollOffset {
|
|
p.scrollOffset = p.maxScrollOffset
|
|
}
|
|
case sdl.K_HOME:
|
|
p.scrollOffset = 0
|
|
case sdl.K_END:
|
|
p.scrollOffset = p.maxScrollOffset
|
|
}
|
|
}
|
|
case *sdl.MouseWheelEvent:
|
|
p.scrollOffset -= e.Y * p.lineHeight * 3
|
|
if p.scrollOffset < 0 {
|
|
p.scrollOffset = 0
|
|
}
|
|
if p.scrollOffset > p.maxScrollOffset {
|
|
p.scrollOffset = p.maxScrollOffset
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *LogViewerPage) Update(dt float32) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *LogViewerPage) Render() error {
|
|
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
|
|
|
|
sm := shaderManager.Get()
|
|
textShader, _ := sm.Get("text")
|
|
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
grey := sdl.Color{R: 180, G: 180, B: 180, A: 255}
|
|
|
|
titleFontSize := int32(48)
|
|
titleScale := float32(titleFontSize) / float32(baseFontSize)
|
|
contentFontSize := int32(24)
|
|
contentScale := float32(contentFontSize) / float32(baseFontSize)
|
|
hintFontSize := int32(20)
|
|
hintScale := float32(hintFontSize) / float32(baseFontSize)
|
|
|
|
margin := int32(40)
|
|
lineSpacing := int32(4)
|
|
p.lineHeight = contentFontSize + lineSpacing
|
|
|
|
contentColumnWidth := int32(float32(p.Base.ScreenWidth) * 0.55)
|
|
contentLeftX := p.Base.ScreenCenterX - contentColumnWidth/2
|
|
|
|
// Title
|
|
titleH := float32(p.titleTextureHeight) * titleScale
|
|
titleW := float32(p.titleTextureWidth) * titleScale
|
|
titleX := p.Base.ScreenCenterX - int32(titleW/2)
|
|
titleY := p.Base.ScreenHeight - margin - int32(titleH)
|
|
p.Base.RenderTexture(textShader, p.titleTextureID, mgl32.Vec2{float32(titleX), float32(titleY)}, mgl32.Vec2{titleW, titleH}, white)
|
|
|
|
// Hint at bottom
|
|
hintH := float32(p.hintTextureHeight) * hintScale
|
|
hintW := float32(p.hintTextureWidth) * hintScale
|
|
hintX := p.Base.ScreenCenterX - int32(hintW/2)
|
|
hintY := margin
|
|
p.Base.RenderTexture(textShader, p.hintTextureID, mgl32.Vec2{float32(hintX), float32(hintY)}, mgl32.Vec2{hintW, hintH}, grey)
|
|
|
|
// Content area bounds
|
|
contentTop := titleY - margin
|
|
contentBottom := hintY + int32(hintH) + margin
|
|
p.visibleHeight = contentTop - contentBottom
|
|
|
|
// Total content height
|
|
totalContentHeight := int32(len(p.lines)) * p.lineHeight
|
|
p.maxScrollOffset = totalContentHeight - p.visibleHeight
|
|
if p.maxScrollOffset < 0 {
|
|
p.maxScrollOffset = 0
|
|
}
|
|
|
|
// Render visible lines
|
|
currentY := contentTop - p.lineHeight + p.scrollOffset
|
|
|
|
for i, line := range p.lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
currentY -= p.lineHeight
|
|
continue
|
|
}
|
|
|
|
if p.lineTextureIDs[i] == 0 {
|
|
currentY -= p.lineHeight
|
|
continue
|
|
}
|
|
|
|
if currentY > contentTop || currentY+p.lineHeight < contentBottom {
|
|
currentY -= p.lineHeight
|
|
continue
|
|
}
|
|
|
|
lineW := float32(p.lineTextureWidths[i]) * contentScale
|
|
lineH := float32(p.lineTextureHeights[i]) * contentScale
|
|
|
|
p.Base.RenderTexture(textShader, p.lineTextureIDs[i], mgl32.Vec2{float32(contentLeftX), float32(currentY)}, mgl32.Vec2{lineW, lineH}, white)
|
|
currentY -= p.lineHeight
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *LogViewerPage) Destroy() error {
|
|
gl.DeleteTextures(1, &p.titleTextureID)
|
|
gl.DeleteTextures(1, &p.hintTextureID)
|
|
|
|
for _, texID := range p.lineTextureIDs {
|
|
if texID != 0 {
|
|
gl.DeleteTextures(1, &texID)
|
|
}
|
|
}
|
|
|
|
return p.Base.Destroy()
|
|
}
|