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>
416 lines
13 KiB
Go
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()
|
|
}
|