package main import ( "fmt" "math" "os" rl "github.com/gen2brain/raylib-go/raylib" ) var ( transition = NewTransitionManager() currentStep = 0 targetStep = 0 useDefault = false step1DefaultRect rl.Rectangle step1CustomRect rl.Rectangle installer = NewInstaller() // We no longer store circleToCenter or "display progress" here. // Instead, we use the new CircleAnimator: circleAnimator = NewCircleAnimator() // textHoverFade => for the small circle’s info text in steps 0..2 textHoverFade float32 ) const finalStep = 3 func main() { monitor := rl.GetCurrentMonitor() screenW := rl.GetMonitorWidth(monitor) screenH := rl.GetMonitorHeight(monitor) rl.InitWindow(int32(screenW), int32(screenH), "Spitfire Browser Installer") rl.SetWindowState(rl.FlagFullscreenMode) defer rl.CloseWindow() refreshRate := rl.GetMonitorRefreshRate(monitor) if refreshRate <= 0 { refreshRate = 60 } rl.SetTargetFPS(int32(refreshRate)) InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight()) // Start the download+decompress in background immediately installer.StartDownloadDecompress() // Initially step 0 => circle is top-right => circleAnimator.PosAlpha=0 circleAnimator.SetStep(0) for !rl.WindowShouldClose() { screenW = rl.GetScreenWidth() screenH = rl.GetScreenHeight() mousePos := rl.GetMousePosition() buttonW := 100 buttonH := 30 prevX := int32(50) prevY := int32(screenH - 50) nextX := int32(screenW - 150) nextY := prevY // Transition update oldAlpha, oldScale, oldOffsetX, newAlpha, newScale, newOffsetX := transition.Update() if !transition.IsActive() && currentStep != targetStep { currentStep = targetStep // Update circleAnimator step so it moves (0 => top-right, 3 => center) circleAnimator.SetStep(currentStep) } // Poll SPM progress installer.PollProgress() // If error if installer.LastError != nil { fmt.Println("SPM Error:", installer.LastError) } // Update circle animator every frame // This will ease the actual SPM progress => "DisplayProgress" circleAnimator.Update(installer.Progress) // textHoverFade for step<3 if currentStep < finalStep { topRightX := float32(screenW - 80) topRightY := float32(100) radius := float32(30) dx := mousePos.X - topRightX dy := mousePos.Y - topRightY dist := float32(math.Sqrt(float64(dx*dx + dy*dy))) if dist < radius+30 { textHoverFade += 0.1 if textHoverFade > 1 { textHoverFade = 1 } } else { textHoverFade -= 0.1 if textHoverFade < 0 { textHoverFade = 0 } } } else { textHoverFade = 0 } // Mouse input if !transition.IsActive() && rl.IsMouseButtonPressed(rl.MouseLeftButton) { handleInput(mousePos, screenW, screenH, buttonW, buttonH, prevX, prevY, nextX, nextY) } UpdateBackground(screenW, screenH) rl.BeginDrawing() DrawBackground(screenW, screenH) drawHeader(screenW) oldStep := currentStep newStep := currentStep if transition.IsActive() { oldStep = transition.oldStep newStep = transition.newStep } phase := transition.GetPhase() if transition.IsActive() { if phase == TransitionOutFadeScale { drawStep(oldStep, screenW, screenH, mousePos, oldAlpha, oldScale, oldOffsetX) } else { drawStep(oldStep, screenW, screenH, mousePos, oldAlpha, oldScale, oldOffsetX) drawStep(newStep, screenW, screenH, mousePos, newAlpha, newScale, newOffsetX) } } else { drawStep(currentStep, screenW, screenH, mousePos, 1, 1, 0) } // Nav buttons if not final step if !transition.IsActive() && currentStep < finalStep { if currentStep > 0 && currentStep < 3 { drawButton("Previous", prevX, prevY, int32(buttonW), int32(buttonH), mousePos) } if currentStep == 1 { drawButton("Next", nextX, nextY, int32(buttonW), int32(buttonH), mousePos) } else if currentStep == 2 { drawButton("Finish", nextX, nextY, int32(buttonW), int32(buttonH), mousePos) } } // Draw the circle drawInstallCircle(screenW, screenH, circleAnimator.PosAlpha, circleAnimator.DisplayProgress, installer.DoneInstall, textHoverFade) rl.EndDrawing() } } func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, prevX, prevY, nextX, nextY int32) { switch currentStep { case 0: if overRect(mousePos, step1DefaultRect) { useDefault = true if !installer.IsInstalling && !installer.DoneInstall { installer.FinalInstall() } startTransition(currentStep, finalStep) } if overRect(mousePos, step1CustomRect) { useDefault = false startTransition(currentStep, 1) } case 1: if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { startTransition(currentStep, 2) } if overButton(mousePos, int32(prevX), int32(prevY), int32(buttonW), int32(buttonH)) { startTransition(currentStep, 0) } selectColor(mousePos) selectContrastIcon(mousePos) selectTheme(mousePos) case 2: if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { if !installer.IsInstalling && !installer.DoneInstall { installer.FinalInstall() } fmt.Printf("Installation started:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n", useDefault, selectedColor, selectedTheme, selectedLayout) startTransition(currentStep, finalStep) } if overButton(mousePos, int32(prevX), int32(prevY), int32(buttonW), int32(buttonH)) { startTransition(currentStep, 1) } selectLayoutOption(mousePos) } } func drawHeader(screenW int) { title := "Spitfire Browser Installer" titleFontSize := int32(30) titleWidth := rl.MeasureText(title, titleFontSize) rl.DrawText(title, (int32(screenW)-titleWidth)/2, 20, titleFontSize, rl.RayWhite) rl.DrawLine(50, 60, int32(screenW)-50, 60, rl.Fade(rl.White, 0.5)) } // Now we pass "displayProgress" (0..100) as a float, "posAlpha" in [0..1], // plus "doneInstall" to show the "Run App" button func drawInstallCircle(screenW, screenH int, posAlpha, displayProgress float32, doneInstall bool, hoverAlpha float32) { // Lerp position/radius topRight := rl.Vector2{X: float32(screenW - 80), Y: 100} center := rl.Vector2{X: float32(screenW) / 2, Y: float32(screenH)/2 - 50} x := lerp(topRight.X, center.X, posAlpha) y := lerp(topRight.Y, center.Y, posAlpha) r := lerp(30, 60, posAlpha) // Draw background circle bgColor := rl.Color{255, 255, 255, 80} fillColor := rl.Color{255, 255, 255, 200} rl.DrawCircle(int32(x), int32(y), r, bgColor) angle := displayProgress / 100.0 * 360.0 rl.DrawCircleSector(rl.Vector2{X: x, Y: y}, r, 0, angle, 60, fillColor) // Draw numeric progress txt := fmt.Sprintf("%3.0f%%", displayProgress) var fontSize int32 = 18 if posAlpha > 0.5 { fontSize = 24 // bigger in final steps } txtW := rl.MeasureText(txt, fontSize) rl.DrawText(txt, int32(x)-txtW/2, int32(y)-(fontSize/2), fontSize, rl.White) if currentStep < finalStep { // show the task text with "hoverAlpha" if hoverAlpha > 0 { drawTextWrappedAlpha(installer.Task, int32(x)-100, int32(y)+35, 200, 18, rl.White, hoverAlpha) } } else { // final step => always show text, lower so bigger circle won't overlap drawTextWrapped(installer.Task, int32(x)-100, int32(y+r+10), 200, 18, rl.White) if doneInstall { drawRunAppButton(x, y+r+80) } } } // "Run App" button logic is the same func drawRunAppButton(cx, cy float32) { w := float32(180) h := float32(50) rect := rl.Rectangle{X: cx - w/2, Y: cy, Width: w, Height: h} hovered := overRect(rl.GetMousePosition(), rect) drawRoundedRectButton(rect, "Start Spitfire", 1.0, hovered) if hovered && rl.IsMouseButtonPressed(rl.MouseLeftButton) { fmt.Println("Launching the app (placeholder)...") os.Exit(0) } } // linear interpolation func lerp(a, b, t float32) float32 { return a + (b-a)*t } // Helper for text wrapping with alpha func drawTextWrappedAlpha(text string, x, y, maxWidth, fontSize int32, color rl.Color, alpha float32) { words := splitIntoWords(text) line := "" offsetY := int32(0) for _, word := range words { testLine := line + word + " " if rl.MeasureText(testLine, fontSize) > int32(maxWidth) { drawColoredText(line, x, y+offsetY, fontSize, color) line = word + " " offsetY += fontSize + 2 } else { line = testLine } } if line != "" { drawColoredText(line, x, y+offsetY, fontSize, color) } } // Same as drawTextWrapped, just using a color param func drawColoredText(text string, x, y, fontSize int32, color rl.Color) { rl.DrawText(text, x, y, fontSize, color) } // Custom function to draw text with wrapping func drawTextWrapped(text string, x, y int32, maxWidth, fontSize int32, color rl.Color) { words := splitIntoWords(text) line := "" offsetY := int32(0) for _, word := range words { testLine := line + word + " " if rl.MeasureText(testLine, fontSize) > int32(maxWidth) { rl.DrawText(line, x, y+offsetY, fontSize, color) line = word + " " offsetY += fontSize + 2 } else { line = testLine } } // Draw the last line if line != "" { rl.DrawText(line, x, y+offsetY, fontSize, color) } } // Helper function to split text into words func splitIntoWords(text string) []string { words := []string{} word := "" for _, char := range text { if char == ' ' || char == '\n' { if word != "" { words = append(words, word) word = "" } if char == '\n' { words = append(words, "\n") } } else { word += string(char) } } if word != "" { words = append(words, word) } return words } // startTransition is your existing function func startTransition(from, to int) { targetStep = to transition.Start(from, to) } // SPM example // package main // import ( // "fmt" // "os" // "path/filepath" // "spitfire-installer/spm" // "time" // ) // func main() { // // Start a goroutine to display progress updates // done := make(chan bool) // go func() { // for { // select { // case <-done: // return // default: // percentage, task := spm.GetProgress() // fmt.Printf("\r[%3d%%] %s", percentage, task) // time.Sleep(500 * time.Millisecond) // } // } // }() // // Set up the download directory // downloadDir := spm.GetTempDownloadDir() // fmt.Println("\nTemporary download directory:", downloadDir) // // Download the APPINDEX // appIndexPath := filepath.Join(downloadDir, "APPINDEX") // spm.UpdateProgress(0, "Starting APPINDEX download") // if err := spm.DownloadAppIndex(appIndexPath); err != nil { // fmt.Println("\nError downloading APPINDEX:", err) // done <- true // os.Exit(1) // } // // Download the desired package version (e.g., nightly) // packageName := "spitfire-browser" // release := "nightly" // spm.UpdateProgress(0, "Starting package download") // if err := spm.DownloadPackageFromAppIndex(appIndexPath, packageName, release, downloadDir); err != nil { // fmt.Println("\nError downloading package:", err) // done <- true // os.Exit(1) // } // // Decompress and install // packagePath := filepath.Join(downloadDir, "browser-amd64-nightly-linux.tar.gz") // spm.UpdateProgress(0, "Starting decompression") // tempDir, err := spm.DecompressToTemp(packagePath) // if err != nil { // fmt.Println("\nError decompressing package:", err) // done <- true // os.Exit(1) // } // fmt.Println("\nDecompressed package to:", tempDir) // // Generate default install directory // installDir, err := spm.GetDefaultInstallDir() // if err != nil { // inst.LastError = fmt.Errorf("failed to determine default install directory: %w", err) // return // } // spm.UpdateProgress(0, "Starting installation") // if err := spm.MoveFilesToInstallDir(tempDir, installDir); err != nil { // fmt.Println("\nError installing package:", err) // done <- true // os.Exit(1) // } // // Notify progress display to stop and finalize // done <- true // fmt.Printf("\nSuccessfully installed %s (%s) to %s\n", packageName, release, installDir) // }