package page import ( "fmt" "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/download" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/shaderManager" ) var GitVersion = "Audio-Bug-Fix 0.1" type Home struct { Base fm *fontManager.FontManager font *fontManager.FontEntry title StringItem version StringItem prompt StringItem note StringItem message StringItem downloadProgressChan chan download.DownloadStatus licenseInfoHeading StringItem emailDisplay StringItem licenseKeyDisplay StringItem hasLicenseInfo bool baseFontSize int isDownloadComplete bool } // Init performs basic page initialization and creation of static assets. func (p *Home) Init(ctx *PageCtx) error { title := "Radiant Wave" version := GitVersion prompt := "Press F2 to Start, or F3 for Settings" note := "You can always press F1 to return here" if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } // Get font p.fm = fontManager.Get() font, ok := p.fm.GetFont("SpaceMonoNerdFont-Regular") if !ok { return fmt.Errorf("unable to fetch font") } p.font = font // Create title texture var err error p.title, err = NewStringItem(title, font, colors.White) if err != nil { return fmt.Errorf("unable to create title string texture: %w", err) } // Create version texture versionString := "Version: " + version p.version, err = NewStringItem(versionString, font, colors.White) if err != nil { return fmt.Errorf("unable to create version string texture: %w", err) } // Create prompt texture p.prompt, err = NewStringItem(prompt, font, colors.White) if err != nil { return fmt.Errorf("unable to create prompt string texture: %w", err) } // Create note texture p.note, err = NewStringItem(note, font, colors.White) if err != nil { return fmt.Errorf("unable to create note string texture: %w", err) } // Create download channel and message p.downloadProgressChan = make(chan download.DownloadStatus) // Initialize message p.message, err = NewStringItem("Status: Initializing...", font, colors.White) if err != nil { return fmt.Errorf("unable to create initial message texture: %w", err) } config, err := db.GetConfig(p.Context.DbPtr) if err != nil { panic(err) } // Create license info textures if data exists if config.EmailAddress != "" || config.LicenseKey != "" { p.hasLicenseInfo = true // Create heading p.licenseInfoHeading, err = NewStringItem("License Info", font, colors.White) if err != nil { return fmt.Errorf("unable to create license info heading texture: %w", err) } // Create email display if exists if config.EmailAddress != "" { emailText := fmt.Sprintf("Email: %s", config.EmailAddress) p.emailDisplay, err = NewStringItem(emailText, font, colors.White) if err != nil { return fmt.Errorf("unable to create email display texture: %w", err) } } // Create license key display if exists if config.LicenseKey != "" { licenseKeyText := fmt.Sprintf("License Key: %s", config.LicenseKey) p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White) if err != nil { return fmt.Errorf("unable to create license key display texture: %w", err) } } } // Get base font size from database baseFontSize := config.StandardFontSize p.baseFontSize = baseFontSize // Scale the note text fontSize := int32(32) scale := float32(fontSize) / float32(baseFontSize) p.note.W = int32(float32(p.note.W) * scale * 0.75) p.note.H = int32(float32(p.note.H) * scale * 0.75) // Scale license info texts if p.hasLicenseInfo { licenseInfoScale := float32(28) / float32(baseFontSize) p.licenseInfoHeading.W = int32(float32(p.licenseInfoHeading.W) * licenseInfoScale) p.licenseInfoHeading.H = int32(float32(p.licenseInfoHeading.H) * licenseInfoScale) if p.emailDisplay.ID != 0 { p.emailDisplay.W = int32(float32(p.emailDisplay.W) * licenseInfoScale * 0.85) p.emailDisplay.H = int32(float32(p.emailDisplay.H) * licenseInfoScale * 0.85) } if p.licenseKeyDisplay.ID != 0 { p.licenseKeyDisplay.W = int32(float32(p.licenseKeyDisplay.W) * licenseInfoScale * 0.85) p.licenseKeyDisplay.H = int32(float32(p.licenseKeyDisplay.H) * licenseInfoScale * 0.85) } } // Scale message text messageScale := float32(28) / float32(baseFontSize) p.message.W = int32(float32(p.message.W) * messageScale) p.message.H = int32(float32(p.message.H) * messageScale) return nil } // HandleEvent processes SDL events and handles them appropriately for the Home page. func (p *Home) HandleEvent(event *sdl.Event) error { return nil } func (p *Home) Update(dt float32) error { var ( progressUpdate download.DownloadStatus ok bool gotUpdate bool ) // Drain the channel, keeping only the newest update. for { select { case progressUpdate, ok = <-p.Context.MessageChannel: gotUpdate = true if !ok { p.setMessage("Status: READY", colors.Green) p.isDownloadComplete = true return nil } default: goto drained } } drained: if !gotUpdate { return nil } if progressUpdate.Err != nil { p.setMessage(fmt.Sprintf("Error: %s", progressUpdate.Err.Error()), colors.Red) return nil } // Build the display string based on what the download package tells us. var msg string if progressUpdate.Max <= 0 { // No total known yet — show the descriptive message from the package. // This covers: "Fetching file manifest", "Checking local files", // "Comparing checksums", "Building download list", etc. msg = fmt.Sprintf("Status: %s", progressUpdate.Msg) } else { // We have a known total — show percentage. pct := float64(progressUpdate.Cur) / float64(progressUpdate.Max) * 100 msg = fmt.Sprintf("Status: Downloading Assets — %.1f%%", pct) } p.setMessage(msg, colors.White) return nil } // setMessage deletes the old message texture and builds a new one. func (p *Home) setMessage(text string, color sdl.Color) { if p.message.ID != 0 { gl.DeleteTextures(1, &p.message.ID) p.message.ID = 0 } newMsg, err := NewStringItem(text, p.font, color) if err != nil { return } messageScale := float32(28) / float32(p.baseFontSize) newMsg.W = int32(float32(newMsg.W) * messageScale) newMsg.H = int32(float32(newMsg.H) * messageScale) p.message = newMsg } func (p *Home) Render() error { baseFontSize := p.baseFontSize sm := shaderManager.Get() verticalMargin := int32(20) licenseInfoMargin := int32(10) // 1) Calculate scales titleFontSize := int32(64) bodyFontSize := int32(32) titleScale := float32(titleFontSize) / float32(baseFontSize) bodyScale := float32(bodyFontSize) / float32(baseFontSize) // 2) Calculate dimensions (scaled) titleW := float32(p.title.W) * titleScale titleH := float32(p.title.H) * titleScale titleDim := mgl32.Vec2{titleW, titleH} versionW := float32(p.version.W) * bodyScale versionH := float32(p.version.H) * bodyScale versionDim := mgl32.Vec2{versionW, versionH} promptW := float32(p.prompt.W) * bodyScale promptH := float32(p.prompt.H) * bodyScale promptDim := mgl32.Vec2{promptW, promptH} // Note dimensions (already scaled in Init) noteW := float32(p.note.W) noteH := float32(p.note.H) // Message dimensions (already scaled in Init/Update) messageW := float32(p.message.W) messageH := float32(p.message.H) // License info dimensions (already scaled in Init) licenseHeadingW := float32(p.licenseInfoHeading.W) licenseHeadingH := float32(p.licenseInfoHeading.H) var emailDisplayW, emailDisplayH, licenseKeyDisplayW, licenseKeyDisplayH float32 if p.emailDisplay.ID != 0 { emailDisplayW = float32(p.emailDisplay.W) emailDisplayH = float32(p.emailDisplay.H) } if p.licenseKeyDisplay.ID != 0 { licenseKeyDisplayW = float32(p.licenseKeyDisplay.W) licenseKeyDisplayH = float32(p.licenseKeyDisplay.H) } // 3) Calculate total height of the vertical stack totalH := int32(titleH) + verticalMargin + int32(versionH) + verticalMargin + int32(promptH) + verticalMargin + int32(noteH) // 4) Y-up layout: start from the TOP of the centered stack and step DOWN top := p.Base.ScreenCenterY + totalH/2 titleY := top - int32(titleH) versionY := titleY - verticalMargin - int32(versionH) p.prompt.Y = versionY - verticalMargin - int32(promptH) p.note.Y = p.prompt.Y - verticalMargin - int32(noteH) // 5) Center X for each item titleX := p.Base.ScreenCenterX - int32(titleW/2) versionX := p.Base.ScreenCenterX - int32(versionW/2) p.prompt.X = p.Base.ScreenCenterX - int32(promptW/2) p.note.X = p.Base.ScreenCenterX - int32(noteW/2) titlePos := mgl32.Vec2{float32(titleX), float32(titleY)} versionPos := mgl32.Vec2{float32(versionX), float32(versionY)} // 6) Position license info at bottom center if p.hasLicenseInfo { bottomMargin := int32(20) // Calculate total height of license info block licenseInfoTotalH := int32(licenseHeadingH) if p.emailDisplay.ID != 0 { licenseInfoTotalH += int32(emailDisplayH) + licenseInfoMargin } if p.licenseKeyDisplay.ID != 0 { licenseInfoTotalH += int32(licenseKeyDisplayH) + licenseInfoMargin } // Start from bottom and work up currentY := bottomMargin // License key (bottom-most if it exists) if p.licenseKeyDisplay.ID != 0 { p.licenseKeyDisplay.X = p.Base.ScreenCenterX - int32(licenseKeyDisplayW/2) p.licenseKeyDisplay.Y = currentY currentY += int32(licenseKeyDisplayH) + licenseInfoMargin } // Email if p.emailDisplay.ID != 0 { p.emailDisplay.X = p.Base.ScreenCenterX - int32(emailDisplayW/2) p.emailDisplay.Y = currentY currentY += int32(emailDisplayH) + licenseInfoMargin } // Heading (top-most) p.licenseInfoHeading.X = p.Base.ScreenCenterX - int32(licenseHeadingW/2) p.licenseInfoHeading.Y = currentY } // 7) Position message between license info and note/prompt if p.hasLicenseInfo { // Calculate the top of the license info block licenseInfoTopY := p.licenseInfoHeading.Y + int32(licenseHeadingH) if p.emailDisplay.ID != 0 { licenseInfoTopY += int32(emailDisplayH) + licenseInfoMargin } if p.licenseKeyDisplay.ID != 0 { licenseInfoTopY += int32(licenseKeyDisplayH) + licenseInfoMargin } // Center the message vertically between license info and note availableSpace := p.note.Y - licenseInfoTopY - verticalMargin p.message.Y = licenseInfoTopY + availableSpace/2 - int32(messageH/2) } else { // If no license info, place message below note p.message.Y = p.note.Y - verticalMargin - int32(messageH) } p.message.X = p.Base.ScreenCenterX - int32(messageW/2) // 8) Get shader and render textShader, ok := sm.Get("text") if !ok { return fmt.Errorf("failed to fetch text shader") } color := colors.White p.Base.RenderTexture(textShader, p.title.ID, titlePos, titleDim, color) p.Base.RenderTexture(textShader, p.version.ID, versionPos, versionDim, color) p.Base.RenderTexture(textShader, p.prompt.ID, mgl32.Vec2{float32(p.prompt.X), float32(p.prompt.Y)}, promptDim, color) p.Base.RenderTexture(textShader, p.note.ID, mgl32.Vec2{float32(p.note.X), float32(p.note.Y)}, mgl32.Vec2{noteW, noteH}, color) // Render message if p.message.ID != 0 { msgColor := colors.White if p.isDownloadComplete { msgColor = colors.Green } p.Base.RenderTexture(textShader, p.message.ID, mgl32.Vec2{float32(p.message.X), float32(p.message.Y)}, mgl32.Vec2{messageW, messageH}, msgColor) } // Render license info if it exists if p.hasLicenseInfo { p.Base.RenderTexture(textShader, p.licenseInfoHeading.ID, mgl32.Vec2{float32(p.licenseInfoHeading.X), float32(p.licenseInfoHeading.Y)}, mgl32.Vec2{licenseHeadingW, licenseHeadingH}, color) if p.emailDisplay.ID != 0 { p.Base.RenderTexture(textShader, p.emailDisplay.ID, mgl32.Vec2{float32(p.emailDisplay.X), float32(p.emailDisplay.Y)}, mgl32.Vec2{emailDisplayW, emailDisplayH}, color) } if p.licenseKeyDisplay.ID != 0 { p.Base.RenderTexture(textShader, p.licenseKeyDisplay.ID, mgl32.Vec2{float32(p.licenseKeyDisplay.X), float32(p.licenseKeyDisplay.Y)}, mgl32.Vec2{licenseKeyDisplayW, licenseKeyDisplayH}, color) } } return nil } // Destroy cleans up page-specific resources. func (p *Home) Destroy() error { gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.version.ID) gl.DeleteTextures(1, &p.prompt.ID) gl.DeleteTextures(1, &p.note.ID) gl.DeleteTextures(1, &p.message.ID) if p.hasLicenseInfo { gl.DeleteTextures(1, &p.licenseInfoHeading.ID) if p.emailDisplay.ID != 0 { gl.DeleteTextures(1, &p.emailDisplay.ID) } if p.licenseKeyDisplay.ID != 0 { gl.DeleteTextures(1, &p.licenseKeyDisplay.ID) } } return p.Base.Destroy() }