From ad826b3b4385688bd46b8b56e6c928583acd766e Mon Sep 17 00:00:00 2001 From: partisan Date: Wed, 25 Dec 2024 19:06:39 +0100 Subject: [PATCH] added installation progress tab --- circle_animator.go | 73 ++++++++++++++++ installer.go | 49 ++++++++--- main.go | 208 +++++++++++++++++++++++++++++++-------------- 3 files changed, 254 insertions(+), 76 deletions(-) create mode 100644 circle_animator.go diff --git a/circle_animator.go b/circle_animator.go new file mode 100644 index 0000000..3ba9802 --- /dev/null +++ b/circle_animator.go @@ -0,0 +1,73 @@ +package main + +import ( + rl "github.com/gen2brain/raylib-go/raylib" +) + +// CircleAnimator manages both the position/radius alpha (0..1) +// and the displayed progress which eases toward the actual SPM progress. +type CircleAnimator struct { + PosAlpha float32 // 0 => top-right, 1 => center + DisplayProgress float32 // The "smooth" progress shown on screen + targetStep int // The step controlling circle's final position (0..2 => top-right, 3 => center) + + // For smoothing the circle position alpha with "easeInOutCubic" + accumSec float32 + duration float32 + startAlpha float32 + endAlpha float32 + + // For smoothing the displayed progress + // We'll do a simple exponential approach each frame. + // Alternatively, you could do a time-based tween. + smoothingFactor float32 // e.g. 0.1 => about 10% approach per frame +} + +// NewCircleAnimator sets initial defaults +func NewCircleAnimator() *CircleAnimator { + return &CircleAnimator{ + PosAlpha: 0.0, // start at top-right + DisplayProgress: 0.0, + duration: 1.0, // 1 second total for position tween + smoothingFactor: 0.07, // how fast progress approaches real + } +} + +// SetStep sets whether we want circle in top-right (step<3 => alpha=0) or center (step=3 => alpha=1). +// This starts a new positional tween from current alpha to the new alpha. +func (c *CircleAnimator) SetStep(step int) { + c.targetStep = step + var newEnd float32 = 0.0 + if step == 3 { + newEnd = 1.0 + } + // Start new tween from current alpha to newEnd + c.startAlpha = c.PosAlpha + c.endAlpha = newEnd + c.accumSec = 0 +} + +// Update updates both the position alpha (with easeInOutCubic) and the displayed progress. +func (c *CircleAnimator) Update(realProgress int) { + dt := rl.GetFrameTime() // seconds + + // 1) Update position alpha + if c.accumSec < c.duration { + c.accumSec += dt + if c.accumSec > c.duration { + c.accumSec = c.duration + } + p := c.accumSec / c.duration + eased := easeInOutCubic(p) + // Lerp from startAlpha to endAlpha with eased fraction + c.PosAlpha = c.startAlpha + (c.endAlpha-c.startAlpha)*eased + } else { + // no active tween, just keep the final alpha + c.PosAlpha = c.endAlpha + } + + // 2) Smoothly approach the real progress + // (exponential approach: new = old + factor*(target - old)) + target := float32(realProgress) + c.DisplayProgress += c.smoothingFactor * (target - c.DisplayProgress) +} diff --git a/installer.go b/installer.go index 6f58364..00cb482 100644 --- a/installer.go +++ b/installer.go @@ -13,11 +13,12 @@ type Installer struct { Task string // Internal states - IsDownloading bool - IsInstalling bool - DoneDownload bool - DoneInstall bool - LastError error + IsDownloading bool + IsInstalling bool + DoneDownload bool + DoneInstall bool + LastError error + PendingInstall bool // Paths DownloadDir string @@ -36,13 +37,18 @@ func (inst *Installer) StartDownloadDecompress() { defer func() { inst.IsDownloading = false inst.DoneDownload = (inst.LastError == nil) + + // If user requested install while we were downloading (PendingInstall), + // automatically do the install now that we're done decompressing. + if inst.PendingInstall && inst.DoneDownload && !inst.IsInstalling && !inst.DoneInstall { + inst.doFinalInstall() + } }() - // Prepare download directory spm.UpdateProgress(0, "Preparing to download...") inst.DownloadDir = spm.GetTempDownloadDir() - // Download APPINDEX + // 1) Download APPINDEX appIndexPath := filepath.Join(inst.DownloadDir, "APPINDEX") spm.UpdateProgress(0, "Downloading APPINDEX") if err := spm.DownloadAppIndex(appIndexPath); err != nil { @@ -50,7 +56,7 @@ func (inst *Installer) StartDownloadDecompress() { return } - // Download package + // 2) Download package packageName := "spitfire-browser" release := "nightly" spm.UpdateProgress(0, "Downloading package...") @@ -59,7 +65,7 @@ func (inst *Installer) StartDownloadDecompress() { return } - // Decompress + // 3) Decompress spm.UpdateProgress(0, "Decompressing...") packagePath := filepath.Join(inst.DownloadDir, "browser-amd64-nightly-linux.tar.gz") tempDir, err := spm.DecompressToTemp(packagePath) @@ -67,19 +73,34 @@ func (inst *Installer) StartDownloadDecompress() { inst.LastError = err return } - inst.TempDir = tempDir }() } -// FinalInstall moves files to the final install directory in a background goroutine. +// FinalInstall is called by the UI to request installation. +// If download is done, it runs immediately, otherwise sets PendingInstall=true. func (inst *Installer) FinalInstall() { - if !inst.DoneDownload { - inst.LastError = fmt.Errorf("Cannot install: download and decompression are not complete") + // Already installed or installing => ignore repeated calls + if inst.IsInstalling || inst.DoneInstall { return } + // If not done downloading, just mark that we want to install once finished + if !inst.DoneDownload { + fmt.Println("Cannot install now: download and decompression not complete -> pending install.") + inst.PendingInstall = true + return + } + + // Otherwise, go ahead and install now + inst.doFinalInstall() +} + +// doFinalInstall does the actual file move and sets states +func (inst *Installer) doFinalInstall() { inst.IsInstalling = true + inst.PendingInstall = false // we are fulfilling the install now + go func() { defer func() { inst.IsInstalling = false @@ -93,6 +114,8 @@ func (inst *Installer) FinalInstall() { return } + // Move files + spm.UpdateProgress(0, "Installing...") if err := spm.MoveFilesToInstallDir(inst.TempDir, installDir); err != nil { inst.LastError = err return diff --git a/main.go b/main.go index e9fe30a..04a1698 100644 --- a/main.go +++ b/main.go @@ -2,24 +2,32 @@ package main import ( "fmt" + "math" + "os" rl "github.com/gen2brain/raylib-go/raylib" ) -// Keep your global variables for steps and transitions var ( - transition = NewTransitionManager() - currentStep = 0 - targetStep = 0 - useDefault = false - + transition = NewTransitionManager() + currentStep = 0 + targetStep = 0 + useDefault = false step1DefaultRect rl.Rectangle step1CustomRect rl.Rectangle - // Our global Installer from installer.go 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) @@ -31,16 +39,17 @@ func main() { refreshRate := rl.GetMonitorRefreshRate(monitor) if refreshRate <= 0 { - refreshRate = 60 // Fallback to 60 if detection fails + refreshRate = 60 } rl.SetTargetFPS(int32(refreshRate)) InitBackground(rl.GetScreenWidth(), rl.GetScreenHeight()) - // Start the download+decompress in background immediately: + // Start the download+decompress in background immediately installer.StartDownloadDecompress() - targetStep = 0 + // Initially step 0 => circle is top-right => circleAnimator.PosAlpha=0 + circleAnimator.SetStep(0) for !rl.WindowShouldClose() { screenW = rl.GetScreenWidth() @@ -54,24 +63,50 @@ func main() { nextX := int32(screenW - 150) nextY := prevY - // Update transition + // 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 for the GUI + // Poll SPM progress installer.PollProgress() - // If an unrecoverable error occurred, you could handle it here: - // (For now, we just print it in the console.) + // If error if installer.LastError != nil { fmt.Println("SPM Error:", installer.LastError) - // You might choose to show a popup or do something else } - // GUI input + // 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) } @@ -89,7 +124,6 @@ func main() { oldStep = transition.oldStep newStep = transition.newStep } - phase := transition.GetPhase() if transition.IsActive() { if phase == TransitionOutFadeScale { @@ -99,10 +133,11 @@ func main() { drawStep(newStep, screenW, screenH, mousePos, newAlpha, newScale, newOffsetX) } } else { - drawStep(currentStep, screenW, screenH, mousePos, 1.0, 1.0, 0.0) + drawStep(currentStep, screenW, screenH, mousePos, 1, 1, 0) } - if !transition.IsActive() { + // 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) } @@ -113,33 +148,29 @@ func main() { } } - // Draw the semi-transparent loading circle if user is downloading or installing - if installer.IsDownloading || installer.IsInstalling { - drawInstallProgress(screenW, installer.Progress, installer.Task) - } + // Draw the circle + drawInstallCircle(screenW, screenH, circleAnimator.PosAlpha, circleAnimator.DisplayProgress, installer.DoneInstall, textHoverFade) rl.EndDrawing() } } -// handleInput acts on mouse clicks in each step -func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, - prevX, prevY, nextX, nextY int32) { - - if currentStep == 0 { +func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, prevX, prevY, nextX, nextY int32) { + switch currentStep { + case 0: if overRect(mousePos, step1DefaultRect) { - // user clicked "Default" => do final install if not already installing useDefault = true if !installer.IsInstalling && !installer.DoneInstall { installer.FinalInstall() } - fmt.Println("Installation started with default settings.") + startTransition(currentStep, finalStep) } if overRect(mousePos, step1CustomRect) { useDefault = false startTransition(currentStep, 1) } - } else if currentStep == 1 { + + case 1: if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { startTransition(currentStep, 2) } @@ -149,14 +180,14 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, selectColor(mousePos) selectContrastIcon(mousePos) selectTheme(mousePos) - } else if currentStep == 2 { + + case 2: if overButton(mousePos, nextX, nextY, int32(buttonW), int32(buttonH)) { - // user clicked "Finish" => final install if not already installing if !installer.IsInstalling && !installer.DoneInstall { installer.FinalInstall() } - fmt.Printf("Installation started:\nDefault: %v\nColor: %s\nTheme: %s\nLayout: %s\n", - useDefault, selectedColor, selectedTheme, selectedLayout) + 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) @@ -165,7 +196,6 @@ func handleInput(mousePos rl.Vector2, screenW, screenH, buttonW, buttonH int, } } -// drawHeader is your original function func drawHeader(screenW int) { title := "Spitfire Browser Installer" titleFontSize := int32(30) @@ -174,37 +204,89 @@ func drawHeader(screenW int) { rl.DrawLine(50, 60, int32(screenW)-50, 60, rl.Fade(rl.White, 0.5)) } -// drawInstallProgress displays a simple white circle with partial alpha, -// plus the current task text below it, in the top-right area. -func drawInstallProgress(screenW int, progress int, task string) { - circleX := float32(screenW - 80) - circleY := float32(100) - radius := float32(30) - - // Colors for the circle - bgColor := rl.Color{R: 255, G: 255, B: 255, A: 80} - fillColor := rl.Color{R: 255, G: 255, B: 255, A: 200} +// 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 - rl.DrawCircle(int32(circleX), int32(circleY), radius, bgColor) + bgColor := rl.Color{255, 255, 255, 80} + fillColor := rl.Color{255, 255, 255, 200} - // Draw progress arc - angle := float32(progress) / 100.0 * 360.0 - rl.DrawCircleSector( - rl.Vector2{X: circleX, Y: circleY}, - radius, - 0, - angle, - 40, - fillColor, - ) + 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) - // Print numeric progress - txt := fmt.Sprintf("%3d%%", progress) - rl.DrawText(txt, int32(circleX)-rl.MeasureText(txt, 18)/2, int32(circleY)-10, 18, rl.White) + // 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) - // Draw wrapped task text below the circle - drawTextWrapped(task, int32(circleX)-100, int32(circleY)+40, 200, 18, 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