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."
396 lines
12 KiB
Go
396 lines
12 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"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/colors"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
// emailRegex validates email format
|
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
type EmailAddressPage struct {
|
|
Base
|
|
|
|
title string
|
|
titleTextureID uint32
|
|
titleTextureWidth int32
|
|
titleTextureHeight int32
|
|
|
|
currentEmailDisplayTextureID uint32
|
|
currentEmailDisplayTextureWidth int32
|
|
currentEmailDisplayTextureHeight int32
|
|
|
|
inputText string
|
|
inputTextTextureID uint32
|
|
inputTextTextureWidth int32
|
|
inputTextTextureHeight int32
|
|
|
|
// Error message handling
|
|
errorMessage string
|
|
errorTextureID uint32
|
|
errorTextureWidth int32
|
|
errorTextureHeight int32
|
|
|
|
// isFirstTimeSetup tracks whether page was loaded empty (first-time setup)
|
|
isFirstTimeSetup bool
|
|
|
|
// Text input deduplication
|
|
textInputSupported bool
|
|
}
|
|
|
|
// isValidEmail checks if the given string is a valid email format
|
|
func isValidEmail(email string) bool {
|
|
return emailRegex.MatchString(email)
|
|
}
|
|
|
|
// Init performs basic page initialization and creation of static assets.
|
|
func (p *EmailAddressPage) Init(ctx *PageCtx) error {
|
|
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
p.title = "Your Email Address"
|
|
|
|
applicationFont := "Roboto-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
textShader, ok := shaderManager.Get().Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch text shader")
|
|
}
|
|
|
|
// Create title texture
|
|
titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.title, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create title texture: %w", err)
|
|
}
|
|
if titleTextureID == 0 {
|
|
return fmt.Errorf("CreateStringTexture returned a nil texture ID")
|
|
}
|
|
|
|
p.titleTextureID = titleTextureID
|
|
p.titleTextureWidth = titleWidth
|
|
p.titleTextureHeight = titleHeight
|
|
|
|
// Load existing email address from database
|
|
emailAddress := ctx.CfgPtr.EmailAddress
|
|
|
|
if emailAddress != "" {
|
|
p.inputText = emailAddress
|
|
p.isFirstTimeSetup = false
|
|
|
|
// Create current email display texture
|
|
currentEmailText := fmt.Sprintf("Current Email: %s", emailAddress)
|
|
currentEmailTextureID, currentEmailWidth, currentEmailHeight, err := fm.CreateStringTexture(applicationFont, currentEmailText, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create current email display texture: %w", err)
|
|
}
|
|
if currentEmailTextureID == 0 {
|
|
return fmt.Errorf("CreateStringTexture returned a nil texture ID for current email display")
|
|
}
|
|
|
|
p.currentEmailDisplayTextureID = currentEmailTextureID
|
|
p.currentEmailDisplayTextureWidth = currentEmailWidth
|
|
p.currentEmailDisplayTextureHeight = currentEmailHeight
|
|
} else {
|
|
p.inputText = ""
|
|
p.isFirstTimeSetup = true
|
|
}
|
|
|
|
sdl.StartTextInput()
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleEvent processes SDL events and handles them appropriately for the EmailAddressPage.
|
|
func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error {
|
|
actualEvent := *event
|
|
if !sdl.IsTextInputActive() {
|
|
sdl.StartTextInput()
|
|
}
|
|
|
|
switch e := actualEvent.(type) {
|
|
case *sdl.TextInputEvent:
|
|
if !p.textInputSupported {
|
|
// First TextInputEvent: the keyboard fallback already
|
|
// handled this character, so just record that text input
|
|
// is supported and skip the duplicate append.
|
|
p.textInputSupported = true
|
|
} else {
|
|
p.inputText += e.GetText()
|
|
// Clear error when user starts typing
|
|
p.errorMessage = ""
|
|
}
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type == sdl.KEYDOWN {
|
|
switch e.Keysym.Sym {
|
|
case sdl.K_ESCAPE:
|
|
// Block escape during first-time setup
|
|
if p.isFirstTimeSetup {
|
|
return nil
|
|
}
|
|
// Allow escape back to settings if coming from settings
|
|
sdl.StopTextInput()
|
|
p.Context.NavigateTo(SETTINGS)
|
|
case sdl.K_RETURN:
|
|
// Validate email - check empty first, then format
|
|
if p.inputText == "" {
|
|
p.errorMessage = "Email address cannot be empty"
|
|
return nil
|
|
}
|
|
if !isValidEmail(p.inputText) {
|
|
p.errorMessage = "Please enter a valid email address"
|
|
return nil
|
|
}
|
|
|
|
sdl.StopTextInput()
|
|
|
|
// Save email address to database
|
|
p.Base.Context.CfgPtr.EmailAddress = p.inputText
|
|
p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr)
|
|
p.Base.Context.RefreshConfig()
|
|
|
|
if p.isFirstTimeSetup {
|
|
// First-time setup: navigate to License Key page
|
|
p.Context.NavigateTo(LICENSE_KEY)
|
|
} else {
|
|
p.Context.NavigateTo(SETTINGS)
|
|
}
|
|
case sdl.K_BACKSPACE:
|
|
if len(p.inputText) > 0 {
|
|
p.inputText = p.inputText[:len(p.inputText)-1]
|
|
// Clear error when user edits input
|
|
p.errorMessage = ""
|
|
}
|
|
default:
|
|
// Keyboard fallback: if TextInputEvent is not supported,
|
|
// convert printable keycodes to characters directly.
|
|
// This handles kiosk systems without an input method framework.
|
|
if !p.textInputSupported {
|
|
if ch := keycodeToChar(e.Keysym.Sym, e.Keysym.Mod); ch != 0 {
|
|
p.inputText += string(ch)
|
|
p.errorMessage = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
|
|
func (p *EmailAddressPage) Update(dt float32) error {
|
|
// Get application font from database
|
|
applicationFont := "Roboto-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
sm := shaderManager.Get()
|
|
shader, ok := sm.Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch text shader")
|
|
}
|
|
|
|
// Update input text texture
|
|
if len(p.inputText) > 0 {
|
|
// Create the new inputText texture
|
|
inputTextTextureID, inputTextTextureWidth, inputTextTextureHeight, err := fm.CreateStringTexture(
|
|
applicationFont,
|
|
p.inputText,
|
|
shader,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create new input text texture: %w", err)
|
|
}
|
|
|
|
// Delete old texture if it exists
|
|
if p.inputTextTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.inputTextTextureID)
|
|
}
|
|
|
|
p.inputTextTextureID = inputTextTextureID
|
|
p.inputTextTextureWidth = inputTextTextureWidth
|
|
p.inputTextTextureHeight = inputTextTextureHeight
|
|
}
|
|
|
|
// Update error message texture
|
|
if p.errorMessage != "" {
|
|
errorTextureID, errorWidth, errorHeight, err := fm.CreateStringTexture(
|
|
applicationFont,
|
|
p.errorMessage,
|
|
shader,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create error text texture: %w", err)
|
|
}
|
|
|
|
// Delete old error texture if it exists
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
}
|
|
|
|
p.errorTextureID = errorTextureID
|
|
p.errorTextureWidth = errorWidth
|
|
p.errorTextureHeight = errorHeight
|
|
} else {
|
|
// Clear error texture if no error message
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
p.errorTextureID = 0
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Render performs page drawing logic.
|
|
func (p *EmailAddressPage) Render() error {
|
|
// Get base font size from database
|
|
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
|
|
|
|
sm := shaderManager.Get()
|
|
|
|
// 1. Calculate Scale
|
|
titleFontSize := int32(64)
|
|
titleScale := float32(titleFontSize) / float32(baseFontSize)
|
|
currentEmailFontSize := int32(28)
|
|
currentEmailScale := float32(currentEmailFontSize) / float32(baseFontSize)
|
|
padding := int32(4)
|
|
verticalMargin := int32(20)
|
|
|
|
// 2. Define dimensions
|
|
targetFontSize := int32(36)
|
|
scale := float32(targetFontSize) / float32(baseFontSize)
|
|
border := int32(3)
|
|
boxH := targetFontSize + (padding * 2)
|
|
boxW := int32(600) + (padding * 2)
|
|
boxDimensions := mgl32.Vec2{float32(boxW), float32(boxH)}
|
|
borderBoxH := boxH + (border * 2)
|
|
borderBoxW := boxW + (border * 2)
|
|
borderBoxDimensions := mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}
|
|
titleH := float32(p.titleTextureHeight) * titleScale
|
|
titleW := float32(p.titleTextureWidth) * titleScale
|
|
titleDimensions := mgl32.Vec2{titleW, titleH}
|
|
inputTextH := float32(p.inputTextTextureHeight) * scale
|
|
inputTextW := float32(p.inputTextTextureWidth) * scale
|
|
inputTextDimensions := mgl32.Vec2{inputTextW, inputTextH}
|
|
|
|
var currentEmailDisplayH, currentEmailDisplayW float32
|
|
var currentEmailDisplayDimensions mgl32.Vec2
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
currentEmailDisplayH = float32(p.currentEmailDisplayTextureHeight) * currentEmailScale
|
|
currentEmailDisplayW = float32(p.currentEmailDisplayTextureWidth) * currentEmailScale
|
|
currentEmailDisplayDimensions = mgl32.Vec2{currentEmailDisplayW, currentEmailDisplayH}
|
|
}
|
|
|
|
// Error text dimensions
|
|
errorFontSize := int32(20)
|
|
errorScale := float32(errorFontSize) / float32(baseFontSize)
|
|
var errorTextH, errorTextW float32
|
|
var errorTextDimensions mgl32.Vec2
|
|
if p.errorTextureID != 0 {
|
|
errorTextH = float32(p.errorTextureHeight) * errorScale
|
|
errorTextW = float32(p.errorTextureWidth) * errorScale
|
|
errorTextDimensions = mgl32.Vec2{errorTextW, errorTextH}
|
|
}
|
|
|
|
// 3. Define positions
|
|
borderBoxY := p.Base.ScreenCenterY - (borderBoxH / 2)
|
|
borderBoxX := p.Base.ScreenCenterX - (borderBoxW / 2)
|
|
borderBoxPosition := mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}
|
|
boxY := p.Base.ScreenCenterY - (boxH / 2)
|
|
boxX := p.Base.ScreenCenterX - (boxW / 2)
|
|
boxPosition := mgl32.Vec2{float32(boxX), float32(boxY)}
|
|
titleY := borderBoxY + borderBoxH + verticalMargin
|
|
titleX := p.Base.ScreenCenterX - int32(titleW/2)
|
|
titlePosition := mgl32.Vec2{float32(titleX), float32(titleY)}
|
|
inputTextY := boxY + padding
|
|
inputTextX := boxX + padding
|
|
inputTextPosition := mgl32.Vec2{float32(inputTextX), float32(inputTextY)}
|
|
|
|
var currentEmailDisplayPosition mgl32.Vec2
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
currentEmailDisplayY := borderBoxY - int32(currentEmailDisplayH) - verticalMargin
|
|
currentEmailDisplayX := p.Base.ScreenCenterX - int32(currentEmailDisplayW/2)
|
|
currentEmailDisplayPosition = mgl32.Vec2{float32(currentEmailDisplayX), float32(currentEmailDisplayY)}
|
|
}
|
|
|
|
// Error text position (below current email display to avoid overlap)
|
|
var errorTextPosition mgl32.Vec2
|
|
if p.errorTextureID != 0 {
|
|
var errorTextY int32
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
// Position below the current email display
|
|
errorTextY = borderBoxY - int32(currentEmailDisplayH) - verticalMargin - int32(errorTextH) - (verticalMargin / 2)
|
|
} else {
|
|
// No current email display, position below the input box
|
|
errorTextY = borderBoxY - int32(errorTextH) - (verticalMargin / 2)
|
|
}
|
|
errorTextX := p.Base.ScreenCenterX - int32(errorTextW/2)
|
|
errorTextPosition = mgl32.Vec2{float32(errorTextX), float32(errorTextY)}
|
|
}
|
|
|
|
// 4. Define Colors
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
grey := sdl.Color{R: 120, G: 120, B: 120, A: 255}
|
|
darkGrey := sdl.Color{R: 30, G: 30, B: 30, A: 255}
|
|
|
|
// 5. Get Shaders
|
|
textShader, _ := sm.Get("text")
|
|
solidShader, _ := sm.Get("solid_color")
|
|
|
|
// 6. Draw everything
|
|
p.Base.RenderSolidColorQuad(solidShader, borderBoxPosition, borderBoxDimensions, grey)
|
|
p.Base.RenderSolidColorQuad(solidShader, boxPosition, boxDimensions, darkGrey)
|
|
p.Base.RenderTexture(textShader, p.titleTextureID, titlePosition, titleDimensions, white)
|
|
|
|
// Draw current email display if it exists
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
p.Base.RenderTexture(textShader, p.currentEmailDisplayTextureID, currentEmailDisplayPosition, currentEmailDisplayDimensions, white)
|
|
}
|
|
|
|
if len(p.inputText) > 0 {
|
|
p.Base.RenderTexture(textShader, p.inputTextTextureID, inputTextPosition, inputTextDimensions, white)
|
|
}
|
|
|
|
// Draw error message if it exists
|
|
if p.errorTextureID != 0 {
|
|
p.Base.RenderTexture(textShader, p.errorTextureID, errorTextPosition, errorTextDimensions, colors.ErrorRed)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Destroy cleans up page-specific resources.
|
|
func (p *EmailAddressPage) Destroy() error {
|
|
|
|
gl.DeleteTextures(1, &p.titleTextureID)
|
|
|
|
// Delete current email display texture if it exists
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.currentEmailDisplayTextureID)
|
|
p.currentEmailDisplayTextureID = 0
|
|
}
|
|
|
|
// Delete input text texture if it exists
|
|
if p.inputTextTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.inputTextTextureID)
|
|
p.inputTextTextureID = 0
|
|
}
|
|
|
|
// Delete error texture if it exists
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
p.errorTextureID = 0
|
|
}
|
|
|
|
sdl.StopTextInput()
|
|
return p.Base.Destroy()
|
|
}
|