Initial release: RadiantWave v1.0
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var GitVersion = "dev"
|
||||
|
||||
type Welcome struct {
|
||||
Base
|
||||
title StringItem
|
||||
version StringItem
|
||||
prompt StringItem
|
||||
note StringItem
|
||||
licenseInfoHeading StringItem
|
||||
emailDisplay StringItem
|
||||
licenseKeyDisplay StringItem
|
||||
displayStatus StringItem
|
||||
sdlDisplayStatus StringItem
|
||||
hasLicenseInfo bool
|
||||
lastDisplayUpdate time.Time
|
||||
drmDisplayCount int
|
||||
sdlDisplayCount int
|
||||
baseFontSize int // cached from db at init
|
||||
}
|
||||
|
||||
// countConnectedDisplays returns the number of connected displays via DRM
|
||||
func countConnectedDisplays() int {
|
||||
matches, err := filepath.Glob("/sys/class/drm/card*/status")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
connected := 0
|
||||
for _, path := range matches {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(content))
|
||||
if status == "connected" {
|
||||
connected++
|
||||
}
|
||||
}
|
||||
|
||||
return connected
|
||||
}
|
||||
|
||||
// countSDLDisplays returns the number of displays detected by SDL
|
||||
func countSDLDisplays() int {
|
||||
numDisplays, err := sdl.GetNumVideoDisplays()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return numDisplays
|
||||
}
|
||||
|
||||
// updateDisplayCounts updates the display count textures
|
||||
func (p *Welcome) updateDisplayCounts() error {
|
||||
// Get font
|
||||
fm := fontManager.Get()
|
||||
font, ok := fm.GetFont("Roboto-Regular")
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to fetch font")
|
||||
}
|
||||
|
||||
// Get base font size
|
||||
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)
|
||||
}
|
||||
|
||||
// Update DRM count
|
||||
p.drmDisplayCount = countConnectedDisplays()
|
||||
|
||||
// Delete old texture
|
||||
if p.displayStatus.ID != 0 {
|
||||
gl.DeleteTextures(1, &p.displayStatus.ID)
|
||||
}
|
||||
|
||||
// Create new texture
|
||||
displayStatusText := fmt.Sprintf("Connected Displays: %d", p.drmDisplayCount)
|
||||
p.displayStatus, err = NewStringItem(displayStatusText, font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create display status texture: %w", err)
|
||||
}
|
||||
|
||||
displayStatusScale := float32(24) / float32(baseFontSize)
|
||||
p.displayStatus.Scale(displayStatusScale)
|
||||
|
||||
// Update SDL count
|
||||
p.sdlDisplayCount = countSDLDisplays()
|
||||
|
||||
// Delete old texture
|
||||
if p.sdlDisplayStatus.ID != 0 {
|
||||
gl.DeleteTextures(1, &p.sdlDisplayStatus.ID)
|
||||
}
|
||||
|
||||
// Create new texture
|
||||
sdlDisplayStatusText := fmt.Sprintf("SDL Displays Detected: %d", p.sdlDisplayCount)
|
||||
p.sdlDisplayStatus, err = NewStringItem(sdlDisplayStatusText, font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create SDL display status texture: %w", err)
|
||||
}
|
||||
|
||||
p.sdlDisplayStatus.Scale(displayStatusScale)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init performs basic page initialization and creation of static assets.
|
||||
func (p *Welcome) Init(app ApplicationInterface) error {
|
||||
|
||||
title := "Rhema 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(app); err != nil {
|
||||
return fmt.Errorf("failed base page initialization: %w", err)
|
||||
}
|
||||
|
||||
// Get font
|
||||
fm := fontManager.Get()
|
||||
font, ok := fm.GetFont("Roboto-Regular")
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to fetch 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)
|
||||
}
|
||||
|
||||
// Initialize display counts
|
||||
p.lastDisplayUpdate = time.Now()
|
||||
if err := p.updateDisplayCounts(); err != nil {
|
||||
return fmt.Errorf("unable to update display counts: %w", err)
|
||||
}
|
||||
|
||||
// Load license information from database
|
||||
emailAddress, err := db.GetConfigValue("email_address")
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving email_address from db: %w", err)
|
||||
}
|
||||
|
||||
licenseKey, err := db.GetConfigValue("license_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving license_key from db: %w", err)
|
||||
}
|
||||
|
||||
// Create license info textures if data exists
|
||||
if emailAddress != "" || 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 emailAddress != "" {
|
||||
emailText := fmt.Sprintf("Email: %s", 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 licenseKey != "" {
|
||||
licenseKeyText := fmt.Sprintf("License Key: %s", 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
|
||||
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)
|
||||
}
|
||||
|
||||
p.baseFontSize = baseFontSize
|
||||
|
||||
// Scale the note text
|
||||
fontSize := int32(32)
|
||||
scale := float32(fontSize) / float32(baseFontSize)
|
||||
p.note.Scale(scale * 0.75)
|
||||
|
||||
// Scale license info texts
|
||||
if p.hasLicenseInfo {
|
||||
licenseInfoScale := float32(28) / float32(baseFontSize)
|
||||
p.licenseInfoHeading.Scale(licenseInfoScale)
|
||||
if p.emailDisplay.ID != 0 {
|
||||
p.emailDisplay.Scale(licenseInfoScale * 0.85)
|
||||
}
|
||||
if p.licenseKeyDisplay.ID != 0 {
|
||||
p.licenseKeyDisplay.Scale(licenseInfoScale * 0.85)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEvent processes SDL events and handles them appropriately for the Welcome page.
|
||||
func (p *Welcome) HandleEvent(event *sdl.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32.
|
||||
func (p *Welcome) Update(dt float32) error {
|
||||
// Update display counts every 2 seconds
|
||||
if time.Since(p.lastDisplayUpdate) >= 2*time.Second {
|
||||
if err := p.updateDisplayCounts(); err != nil {
|
||||
}
|
||||
p.lastDisplayUpdate = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Welcome) Render() error {
|
||||
baseFontSize := p.baseFontSize
|
||||
sm := shaderManager.Get()
|
||||
verticalMargin := int32(20)
|
||||
|
||||
// 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}
|
||||
|
||||
// p.note already scaled in Init via p.note.Scale(scale*0.75)
|
||||
noteDim := p.note.RawDimensions()
|
||||
noteW, noteH := noteDim[0], noteDim[1]
|
||||
|
||||
// 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) // Title sits at the top
|
||||
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 display status in bottom left corner
|
||||
displayStatusDim := p.displayStatus.RawDimensions()
|
||||
displayStatusH := displayStatusDim[1]
|
||||
|
||||
// sdlDisplayStatusDim := p.sdlDisplayStatus.RawDimensions()
|
||||
// sdlDisplayStatusH := sdlDisplayStatusDim[1]
|
||||
|
||||
displayStatusMargin := int32(5)
|
||||
|
||||
p.displayStatus.X = 15
|
||||
p.displayStatus.Y = 15
|
||||
|
||||
p.sdlDisplayStatus.X = 15
|
||||
p.sdlDisplayStatus.Y = 15 + int32(displayStatusH) + displayStatusMargin
|
||||
|
||||
// 7) Position license info at bottom center
|
||||
if p.hasLicenseInfo {
|
||||
licenseInfoMargin := int32(10)
|
||||
bottomMargin := int32(20)
|
||||
|
||||
// License info items are already scaled in Init
|
||||
licenseHeadingDim := p.licenseInfoHeading.RawDimensions()
|
||||
licenseHeadingW, licenseHeadingH := licenseHeadingDim[0], licenseHeadingDim[1]
|
||||
|
||||
var emailDisplayH, licenseKeyDisplayH float32
|
||||
if p.emailDisplay.ID != 0 {
|
||||
emailDisplayDim := p.emailDisplay.RawDimensions()
|
||||
emailDisplayH = emailDisplayDim[1]
|
||||
}
|
||||
if p.licenseKeyDisplay.ID != 0 {
|
||||
licenseKeyDisplayDim := p.licenseKeyDisplay.RawDimensions()
|
||||
licenseKeyDisplayH = licenseKeyDisplayDim[1]
|
||||
}
|
||||
|
||||
// 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 {
|
||||
licenseKeyDisplayDim := p.licenseKeyDisplay.RawDimensions()
|
||||
licenseKeyDisplayW := licenseKeyDisplayDim[0]
|
||||
p.licenseKeyDisplay.X = p.Base.ScreenCenterX - int32(licenseKeyDisplayW/2)
|
||||
p.licenseKeyDisplay.Y = currentY
|
||||
currentY += int32(licenseKeyDisplayH) + licenseInfoMargin
|
||||
}
|
||||
|
||||
// Email
|
||||
if p.emailDisplay.ID != 0 {
|
||||
emailDisplayDim := p.emailDisplay.RawDimensions()
|
||||
emailDisplayW := emailDisplayDim[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
|
||||
}
|
||||
|
||||
// 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, p.prompt.Position(), promptDim, color)
|
||||
p.Base.RenderTexture(textShader, p.note.ID, p.note.Position(), p.note.RawDimensions(), color)
|
||||
|
||||
// Render display status
|
||||
p.Base.RenderTexture(textShader, p.displayStatus.ID, p.displayStatus.Position(), p.displayStatus.RawDimensions(), color)
|
||||
p.Base.RenderTexture(textShader, p.sdlDisplayStatus.ID, p.sdlDisplayStatus.Position(), p.sdlDisplayStatus.RawDimensions(), color)
|
||||
|
||||
// Render license info if it exists
|
||||
if p.hasLicenseInfo {
|
||||
p.Base.RenderTexture(textShader, p.licenseInfoHeading.ID, p.licenseInfoHeading.Position(), p.licenseInfoHeading.RawDimensions(), color)
|
||||
if p.emailDisplay.ID != 0 {
|
||||
p.Base.RenderTexture(textShader, p.emailDisplay.ID, p.emailDisplay.Position(), p.emailDisplay.RawDimensions(), color)
|
||||
}
|
||||
if p.licenseKeyDisplay.ID != 0 {
|
||||
p.Base.RenderTexture(textShader, p.licenseKeyDisplay.ID, p.licenseKeyDisplay.Position(), p.licenseKeyDisplay.RawDimensions(), color)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy cleans up page-specific resources.
|
||||
func (p *Welcome) 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.displayStatus.ID)
|
||||
gl.DeleteTextures(1, &p.sdlDisplayStatus.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()
|
||||
}
|
||||
Reference in New Issue
Block a user