Files
radiantwave-legacy/internal/page/emailAddressPage.go
T
NeilClackandClaude Opus 4.6 df5e64d38a Fix text input on kiosk: add keycodeToChar fallback for email/license pages
The kiosk lacks an input method framework, so SDL2 never generates
TextInputEvent. The WiFi password page already had a keycodeToChar
fallback but email and license key pages did not — characters were
never appended to inputText. Also removes excessive debug logging
from the previous commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:27:28 -05:00

416 lines
13 KiB
Go

package page
import (
"fmt"
"regexp"
"strconv"
"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/db"
"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(app ApplicationInterface) error {
if err := p.Base.Init(app); err != nil {
return fmt.Errorf("failed base page initialization: %w", err)
}
p.title = "Your Email Address"
// Get application font from database
applicationFont, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
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, err := db.GetConfigValue("email_address")
if err != nil {
return fmt.Errorf("retrieving email_address from db: %w", err)
}
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.App.UnwindToPage(&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
if err := db.SetConfigValue("email_address", p.inputText); err != nil {
return fmt.Errorf("failed to save email address: %w", err)
}
if p.isFirstTimeSetup {
// First-time setup: navigate to License Key page
p.App.PushPage(&LicenseKeyPage{})
} else {
// Coming from settings: rewind back to settings
p.App.UnwindToPage(&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, err := db.GetConfigValue("application_font")
if err != nil {
return fmt.Errorf("retrieving application_font from db: %w", err)
}
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
baseFontSizeStr, err := db.GetConfigValue("init_font_size")
if err != nil {
return fmt.Errorf("retrieving init_font_size from db: %w", err)
}
baseFontSize, err := strconv.Atoi(baseFontSizeStr)
if err != nil {
return fmt.Errorf("parsing init_font_size: %w", err)
}
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()
}