263 lines
5.8 KiB
Go
263 lines
5.8 KiB
Go
|
// Package exceptions handles JavaScript errors nicely for the game.
|
||
|
package exceptions
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
|
||
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
||
|
"git.kirsle.net/go/render"
|
||
|
"git.kirsle.net/go/render/event"
|
||
|
"git.kirsle.net/go/ui"
|
||
|
)
|
||
|
|
||
|
// The exception catching window is a singleton and appears on top with
|
||
|
// its own supervisor apart from whatever the game is currently doing.
|
||
|
var (
|
||
|
Supervisor *ui.Supervisor
|
||
|
Window *ui.Window
|
||
|
Disabled bool // don't reopen the window again
|
||
|
lastException string // text of last exception
|
||
|
excLabel *string // trimmed exception label text
|
||
|
mu sync.RWMutex // thread safety
|
||
|
|
||
|
// Configurables.
|
||
|
winSize = render.NewRect(380, 260)
|
||
|
excSize = render.NewRect(360, 140)
|
||
|
charsWide = 50 // to trim the exception text onto screen
|
||
|
charsTall = 9
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
l := "No Exception Traceback"
|
||
|
excLabel = &l
|
||
|
}
|
||
|
|
||
|
// Catch a JavaScript exception and maybe show it to the user.
|
||
|
func Catch(exc string, args ...interface{}) {
|
||
|
if len(args) > 0 {
|
||
|
exc = fmt.Sprintf(exc, args...)
|
||
|
}
|
||
|
|
||
|
log.Error("[JS] Exception: %s", exc)
|
||
|
if Disabled {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
Setup()
|
||
|
|
||
|
width, _ := shmem.CurrentRenderEngine.WindowSize()
|
||
|
Window.MoveTo(render.Point{
|
||
|
X: (width / 2) - (Window.Size().W / 2),
|
||
|
Y: 60,
|
||
|
})
|
||
|
|
||
|
Window.Show()
|
||
|
lastException = exc
|
||
|
*excLabel = trim(exc)
|
||
|
}
|
||
|
|
||
|
// Setup the global supervisor and window the first time - after the render engine has initialized,
|
||
|
// e.g., when you want the window to show up the first time.
|
||
|
func Setup() {
|
||
|
mu.Lock()
|
||
|
defer mu.Unlock()
|
||
|
|
||
|
log.Info("Setup Exceptions")
|
||
|
|
||
|
if Supervisor == nil {
|
||
|
Supervisor = ui.NewSupervisor()
|
||
|
}
|
||
|
|
||
|
if Window == nil {
|
||
|
Window = MakeWindow(Exception{
|
||
|
Supervisor: Supervisor,
|
||
|
Engine: shmem.CurrentRenderEngine,
|
||
|
})
|
||
|
Window.Compute(shmem.CurrentRenderEngine)
|
||
|
Window.Supervise(Supervisor)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Teardown the exception UI, if it was loaded. This is done between Scene transitions in the game
|
||
|
// to reduce memory leaks in case the scenes will flush SDL2 caches or w/e.
|
||
|
func Teardown() {
|
||
|
log.Error("Teardown Exceptions")
|
||
|
mu.Lock()
|
||
|
defer mu.Unlock()
|
||
|
|
||
|
if Window != nil {
|
||
|
Window = nil
|
||
|
}
|
||
|
|
||
|
if Supervisor != nil {
|
||
|
Supervisor = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Handled returns true if the exception window handles the events this tick, e.g.
|
||
|
// so clicking on its window won't let your mouse click also hit things behind it.
|
||
|
func Handled(ev *event.State) bool {
|
||
|
if Window != nil && !Window.Hidden() {
|
||
|
// If they hit the Return/Escape key, dismiss the exception.
|
||
|
if ev.Enter || ev.Escape {
|
||
|
ev.Escape = false
|
||
|
Window.Close()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
Supervisor.Loop(ev)
|
||
|
|
||
|
// NOTE: if the window Close handler tears down the Supervisor, the
|
||
|
// previous call to Supervisor.Loop() may run it and then Supervisor
|
||
|
// no longer exists.
|
||
|
if Supervisor != nil && Supervisor.IsPointInWindow(shmem.Cursor) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Loop allows the exception window to appear on game tick.
|
||
|
func Draw(e render.Engine) {
|
||
|
if Supervisor != nil {
|
||
|
Supervisor.Present(e)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Exception window to show scripting errors in doodads.
|
||
|
type Exception struct {
|
||
|
// Settings passed in by doodle
|
||
|
Supervisor *ui.Supervisor
|
||
|
Engine render.Engine
|
||
|
}
|
||
|
|
||
|
// Function to trim the raw exception text so it fits neatly within the label.
|
||
|
// In case it's long, use the Copy button to copy to your clipboard.
|
||
|
func trim(input string) string {
|
||
|
var lines = []string{}
|
||
|
for _, line := range strings.Split(input, "\n") {
|
||
|
if len(lines) >= charsTall {
|
||
|
lines = lines[:charsTall]
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if len(line) > charsWide {
|
||
|
// Word wrap it.
|
||
|
for len(line) > charsWide {
|
||
|
lines = append(lines, line[:charsWide])
|
||
|
line = line[charsWide:]
|
||
|
}
|
||
|
if len(line) > 0 {
|
||
|
lines = append(lines, line)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
lines = append(lines, line)
|
||
|
}
|
||
|
return strings.Join(lines, "\n")
|
||
|
}
|
||
|
|
||
|
// MakeWindow initializes the window.
|
||
|
func MakeWindow(cfg Exception) *ui.Window {
|
||
|
window := ui.NewWindow("Exception")
|
||
|
window.Configure(ui.Config{
|
||
|
Width: winSize.W,
|
||
|
Height: winSize.H,
|
||
|
Background: render.Red.Lighten(128),
|
||
|
})
|
||
|
|
||
|
window.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
||
|
Teardown()
|
||
|
return ui.ErrStopPropagation
|
||
|
})
|
||
|
|
||
|
header := ui.NewLabel(ui.Label{
|
||
|
Text: "A JavaScript error has occurred in a doodad:",
|
||
|
Font: balance.UIFont,
|
||
|
})
|
||
|
window.Pack(header, ui.Pack{
|
||
|
Side: ui.N,
|
||
|
Padding: 8,
|
||
|
})
|
||
|
|
||
|
text := ui.NewLabel(ui.Label{
|
||
|
TextVariable: excLabel,
|
||
|
Font: balance.ExceptionFont,
|
||
|
})
|
||
|
text.Configure(ui.Config{
|
||
|
BorderSize: 1,
|
||
|
BorderStyle: ui.BorderSunken,
|
||
|
Background: render.White,
|
||
|
Width: excSize.W,
|
||
|
Height: excSize.H,
|
||
|
})
|
||
|
window.Pack(text, ui.Pack{
|
||
|
Side: ui.N,
|
||
|
})
|
||
|
|
||
|
frame := ui.NewFrame("Button frame")
|
||
|
buttons := []struct {
|
||
|
label string
|
||
|
tooltip string
|
||
|
f func()
|
||
|
}{
|
||
|
{"Dismiss", "", func() {
|
||
|
Window.Close()
|
||
|
}},
|
||
|
{"Copy", "Copy the full text to clipboard", func() {
|
||
|
native.CopyToClipboard(lastException)
|
||
|
}},
|
||
|
{"Don't show again", "Don't show errors like this again\nuntil your next play session", func() {
|
||
|
Disabled = true
|
||
|
Window.Close()
|
||
|
}},
|
||
|
}
|
||
|
for i, button := range buttons {
|
||
|
button := button
|
||
|
|
||
|
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
|
||
|
Text: button.label,
|
||
|
Font: balance.MenuFont,
|
||
|
}))
|
||
|
if i == 0 {
|
||
|
btn.SetStyle(&balance.ButtonPrimary)
|
||
|
}
|
||
|
|
||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||
|
button.f()
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
btn.Compute(cfg.Engine)
|
||
|
|
||
|
// Tooltips?
|
||
|
if len(button.tooltip) > 0 {
|
||
|
ui.NewTooltip(btn, ui.Tooltip{
|
||
|
Text: button.tooltip,
|
||
|
Edge: ui.Bottom,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
cfg.Supervisor.Add(btn)
|
||
|
|
||
|
frame.Pack(btn, ui.Pack{
|
||
|
Side: ui.W,
|
||
|
PadX: 4,
|
||
|
Expand: true,
|
||
|
Fill: true,
|
||
|
})
|
||
|
}
|
||
|
window.Pack(frame, ui.Pack{
|
||
|
Side: ui.N,
|
||
|
PadY: 12,
|
||
|
})
|
||
|
|
||
|
return window
|
||
|
}
|