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/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(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 := "SpaceMonoNerdFont-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 config, err := db.GetConfig(p.Context.DbPtr) if err != nil { panic(err) } emailAddress := config.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 config, err := db.GetConfig(p.Context.DbPtr) if err != nil { panic(err) } config.EmailAddress = p.inputText p.Context.DbPtr.Save(config) 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 := "SpaceMonoNerdFont-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 { config, err := db.GetConfig(p.Context.DbPtr) if err != nil { panic(err) } // Get base font size from database baseFontSize := config.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() }