doodle/pkg/modal/modal.go
Noah Petherbridge 73421d27f2 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.
2022-09-24 18:39:02 -07:00

154 lines
3.7 KiB
Go

// Package modal provides UI pop-up modals for Doodle.
package modal
import (
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/cursor"
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
)
// Package global variables.
var (
ready bool // Has been initialized with a render.Engine
current *Modal // Current modal object, nil if no modal active.
engine render.Engine
window render.Rect // cached window dimensions
supervisor *ui.Supervisor
screen *ui.Frame
)
// Initialize the modal package.
func Initialize(e render.Engine) {
engine = e
supervisor = ui.NewSupervisor()
width, height := engine.WindowSize()
window = render.NewRect(width, height)
screen = ui.NewFrame("Modal Screen")
screen.SetBackground(balance.ModalBackdrop)
screen.Resize(window)
screen.Compute(e)
ready = true
}
// Reset the modal state (closing all modals).
func Reset() {
supervisor = nil
current = nil
}
// Handled runs the modal manager's logic. Returns true if a modal
// is presently active, to signal to Doodle not to run game logic.
//
// This function also returns true if the pkg/modal/loadscreen is
// currently active.
func Handled(ev *event.State) bool {
// The loadscreen counts as a modal for this purpose.
if loadscreen.IsActive() {
// Pass in window resize events in case the user maximizes the window during loading.
if ev.WindowResized {
loadscreen.Resized()
}
return true
}
// Check if we have a modal currently active.
if !ready || current == nil {
return false
}
// Enter key submits the default button.
if keybind.Enter(ev) && !current.force {
current.Dismiss(true)
return true
}
// Escape key cancels the modal.
if keybind.Shutdown(ev) && current.cancelable && !current.force {
current.Dismiss(false)
return true
}
supervisor.Loop(ev)
// Has the window changed size?
size := render.NewRect(engine.WindowSize())
if size != window {
window = size
screen.Resize(window)
}
return true
}
// Draw the modal UI to the screen.
func Draw() {
if ready && current != nil {
cursor.Current = cursor.NewPointer(engine)
screen.Present(engine, render.Origin)
supervisor.Present(engine)
}
}
// Center the window on screen.
func center(win *ui.Window) {
var (
modSize = win.Size()
width, height = shmem.CurrentRenderEngine.WindowSize()
)
var moveTo = render.Point{
X: (width / 2) - (modSize.W / 2),
Y: (height / 4) - (modSize.H / 2),
}
win.MoveTo(moveTo)
// HACK: ideally the modal should auto-size itself, but currently
// the body of the window juts out the right and bottom side by
// a few pixels. Fix the underlying problem later, for now we
// set the modal size to big enough to hide the problem.
win.Children()[0].Resize(render.NewRect(modSize.W+12, modSize.H+12))
}
// Modal is an instance of a modal, i.e. Alert or Confirm.
type Modal struct {
title string
message string
window *ui.Window
callback func()
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.
func (m *Modal) WithTitle(title string) *Modal {
m.title = title
return m
}
// Then calls a function after the modal is answered.
func (m *Modal) Then(f func()) *Modal {
m.callback = f
return m
}
// Dismiss the modal and optionally call the callback function.
func (m *Modal) Dismiss(call bool) {
Reset()
if call && m.callback != nil {
m.callback()
}
if m.teardown != nil {
m.teardown()
}
}