Files
radiantwave-legacy/internal/page/emailAddressPage.go
T
NeilClack 17c088cc2f Update Database
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."
2026-03-23 15:02:07 -05:00

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()
}