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.
This commit is contained in:
Noah 2022-09-24 18:39:02 -07:00
parent 546b5705db
commit 73421d27f2
4 changed files with 160 additions and 3 deletions

View File

@ -29,6 +29,7 @@ var (
CheatPlayAsBird = "fly like a bird" CheatPlayAsBird = "fly like a bird"
CheatGodMode = "god mode" CheatGodMode = "god mode"
CheatDebugLoadScreen = "test load screen" CheatDebugLoadScreen = "test load screen"
CheatDebugWaitScreen = "test wait screen"
CheatUnlockLevels = "master key" CheatUnlockLevels = "master key"
CheatSkipLevel = "warp whistle" CheatSkipLevel = "warp whistle"
) )

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen" "git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
) )
@ -168,6 +169,15 @@ func (c Command) cheatCommand(d *Doodle) bool {
loadscreen.Hide() 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: case balance.CheatUnlockLevels:
balance.CheatEnabledUnlockLevels = !balance.CheatEnabledUnlockLevels balance.CheatEnabledUnlockLevels = !balance.CheatEnabledUnlockLevels
if balance.CheatEnabledUnlockLevels { if balance.CheatEnabledUnlockLevels {

View File

@ -66,13 +66,13 @@ func Handled(ev *event.State) bool {
} }
// Enter key submits the default button. // Enter key submits the default button.
if keybind.Enter(ev) { if keybind.Enter(ev) && !current.force {
current.Dismiss(true) current.Dismiss(true)
return true return true
} }
// Escape key cancels the modal. // Escape key cancels the modal.
if keybind.Shutdown(ev) && current.cancelable { if keybind.Shutdown(ev) && current.cancelable && !current.force {
current.Dismiss(false) current.Dismiss(false)
return true return true
} }
@ -124,6 +124,8 @@ type Modal struct {
window *ui.Window window *ui.Window
callback func() 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. // WithTitle sets the title of the modal.
@ -144,4 +146,8 @@ func (m *Modal) Dismiss(call bool) {
if call && m.callback != nil { if call && m.callback != nil {
m.callback() m.callback()
} }
if m.teardown != nil {
m.teardown()
}
} }

140
pkg/modal/wait.go Normal file
View File

@ -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
}