From 73421d27f2b1dbf4867020c1d668e9d4247a32d6 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 24 Sep 2022 18:39:02 -0700 Subject: [PATCH] Wait Modal * Add modal.Wait() that creates a global progress bar modal which is not dismissable by the user; the caller must Dismiss() the modal themselves when ready. * It will be useful in the future in case e.g. saving a Level needs to take a while to rebalance chunks and the modal prevents ALL interaction with the game so the user can't further modify the level while it's busy refactoring itself. * Cheat code: "test wait screen" to show the Wait modal for 10 seconds. --- pkg/balance/cheats.go | 1 + pkg/cheats.go | 10 +++ pkg/modal/modal.go | 12 +++- pkg/modal/wait.go | 140 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 pkg/modal/wait.go diff --git a/pkg/balance/cheats.go b/pkg/balance/cheats.go index c87ee81..38827f4 100644 --- a/pkg/balance/cheats.go +++ b/pkg/balance/cheats.go @@ -29,6 +29,7 @@ var ( CheatPlayAsBird = "fly like a bird" CheatGodMode = "god mode" CheatDebugLoadScreen = "test load screen" + CheatDebugWaitScreen = "test wait screen" CheatUnlockLevels = "master key" CheatSkipLevel = "warp whistle" ) diff --git a/pkg/cheats.go b/pkg/cheats.go index cf9c452..6397495 100644 --- a/pkg/cheats.go +++ b/pkg/cheats.go @@ -5,6 +5,7 @@ import ( "time" "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen" ) @@ -168,6 +169,15 @@ func (c Command) cheatCommand(d *Doodle) bool { loadscreen.Hide() }() + case balance.CheatDebugWaitScreen: + m := modal.Wait("Crunching some numbers...").WithTitle("Please hold").Then(func() { + d.Flash("Wait modal dismissed.") + }) + go func() { + time.Sleep(10 * time.Second) + m.Dismiss(true) + }() + case balance.CheatUnlockLevels: balance.CheatEnabledUnlockLevels = !balance.CheatEnabledUnlockLevels if balance.CheatEnabledUnlockLevels { diff --git a/pkg/modal/modal.go b/pkg/modal/modal.go index e58cb90..534a29c 100644 --- a/pkg/modal/modal.go +++ b/pkg/modal/modal.go @@ -66,13 +66,13 @@ func Handled(ev *event.State) bool { } // Enter key submits the default button. - if keybind.Enter(ev) { + if keybind.Enter(ev) && !current.force { current.Dismiss(true) return true } // Escape key cancels the modal. - if keybind.Shutdown(ev) && current.cancelable { + if keybind.Shutdown(ev) && current.cancelable && !current.force { current.Dismiss(false) return true } @@ -123,7 +123,9 @@ type Modal struct { message string window *ui.Window callback func() - cancelable bool // Escape key can cancel the modal + cancelable bool // Escape key can cancel the modal + force bool // Enter key can not close the modal (e.g. Wait) + teardown func() // Optional teardown logic a modal can attach. } // WithTitle sets the title of the modal. @@ -144,4 +146,8 @@ func (m *Modal) Dismiss(call bool) { if call && m.callback != nil { m.callback() } + + if m.teardown != nil { + m.teardown() + } } diff --git a/pkg/modal/wait.go b/pkg/modal/wait.go new file mode 100644 index 0000000..79a59cc --- /dev/null +++ b/pkg/modal/wait.go @@ -0,0 +1,140 @@ +package modal + +import ( + "fmt" + "time" + + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// Wait pops up a non-dismissable modal that the caller can close when they're ready. +func Wait(message string, args ...interface{}) *Modal { + if !ready { + panic("modal.Wait(): not ready") + } else if current != nil { + current.Dismiss(false) + } + + // Reset the supervisor. + supervisor = ui.NewSupervisor() + + m := &Modal{ + title: "Wait", + message: fmt.Sprintf(message, args...), + force: true, + } + m.window = makeWaitModal(m) + + center(m.window) + current = m + + return m +} + +// creates the ui.Window for the Wait modal. +func makeWaitModal(m *Modal) *ui.Window { + win := ui.NewWindow("Wait") + _, title := win.TitleBar() + title.TextVariable = &m.title + + msgFrame := ui.NewFrame("Wait Message") + win.Pack(msgFrame, ui.Pack{ + Side: ui.N, + }) + + msg := ui.NewLabel(ui.Label{ + TextVariable: &m.message, + Font: balance.UIFont, + }) + msgFrame.Pack(msg, ui.Pack{ + Side: ui.N, + }) + + // Create a bouncing progress bar. + var ( + trough *ui.Frame + troughW = 250 + progressBar *ui.Frame + progressX int + progressW = 64 + progressH = 30 + progressSpeed = 8 + progressFreq = 16 * time.Millisecond + ) + + trough = ui.NewFrame("Progress Trough") + trough.Configure(ui.Config{ + Width: troughW, + Height: progressH, + BorderSize: 1, + BorderStyle: ui.BorderSunken, + Background: render.Grey, + }) + win.Pack(trough, ui.Pack{ + Side: ui.N, + Padding: 4, + }) + + progressBar = ui.NewFrame("Progress Bar") + progressBar.Configure(ui.Config{ + Width: progressW, + Height: 30, + Background: render.Green, + }) + trough.Place(progressBar, ui.Place{ + Left: progressX, + Top: 0, + }) + + trough.Compute(engine) + + win.Compute(engine) + win.Supervise(supervisor) + + // Animate the bouncing of the progress bar in a background goroutine, + // and allow canceling it when the modal is dismissed. + var ( + cancel = make(chan interface{}) + ping = time.NewTicker(progressFreq) + ) + go func() { + for { + select { + case <-cancel: + ping.Stop() + return + case <-ping.C: + // Have room to move the progress bar? + progressX += progressSpeed + + // Cap it to within bounds. + if progressX+progressW >= troughW { + progressX = troughW - progressW + if progressSpeed > 0 { + progressSpeed *= -1 + } + } else if progressX < 0 { + progressX = 0 + if progressSpeed < 0 { + progressSpeed *= -1 + } + } + + trough.Place(progressBar, ui.Place{ + Left: progressX, + Top: 0, + }) + trough.Compute(engine) + } + } + }() + + // Cancel the goroutine on modal teardown. + m.teardown = func() { + cancel <- nil + } + + return win +}