Noah Petherbridge
5956863996
* Added a "menu toolbar" to the top of the Edit Mode with useful buttons that work: New Level, New Doodad (same thing), Save, Save as, Open. * Added ability for the dev console to prompt the user for a question, which opens the console automatically. "Save", "Save as" and "Load" ask for their filenames this way. * Started groundwork for theming the app. The palette window is a light brown with an orange title bar, the Menu Toolbar has a black background, etc. * Added support for multiple fonts instead of just monospace. DejaVu Sans (normal and bold) are used now for most labels and window titles, respectively. The dev console uses DejaVu Sans Mono as before. * Update ui.Label to accept PadX and PadY separately instead of only having the Padding option which did both. * Improvements to Frame packing algorithm. * Set the SDL draw mode to BLEND so we can use alpha colors properly, so now the dev console is semi-translucent.
340 lines
7.0 KiB
Go
340 lines
7.0 KiB
Go
package doodle
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.kirsle.net/apps/doodle/balance"
|
|
"git.kirsle.net/apps/doodle/events"
|
|
"git.kirsle.net/apps/doodle/render"
|
|
"github.com/robertkrimen/otto"
|
|
)
|
|
|
|
// Flash a message to the user.
|
|
func (d *Doodle) Flash(template string, v ...interface{}) {
|
|
d.shell.Write(fmt.Sprintf(template, v...))
|
|
}
|
|
|
|
// Prompt the user for a question in the dev console.
|
|
func (d *Doodle) Prompt(question string, callback func(string)) {
|
|
d.shell.Prompt = question
|
|
d.shell.callback = callback
|
|
d.shell.Open = true
|
|
}
|
|
|
|
// Shell implements the developer console in-game.
|
|
type Shell struct {
|
|
parent *Doodle
|
|
|
|
Open bool
|
|
Prompt string
|
|
callback func(string) // for prompt answers only
|
|
Text string
|
|
History []string
|
|
Output []string
|
|
Flashes []Flash
|
|
|
|
// Blinky cursor variables.
|
|
cursor byte // cursor symbol
|
|
cursorFlip uint64 // ticks until cursor flip
|
|
cursorRate uint64
|
|
|
|
// Paging through history variables.
|
|
historyPaging bool
|
|
historyIndex int
|
|
|
|
// JavaScript shell interpreter.
|
|
js *otto.Otto
|
|
}
|
|
|
|
// Flash holds a message to flash on screen.
|
|
type Flash struct {
|
|
Text string
|
|
Expires uint64 // tick that it expires
|
|
}
|
|
|
|
// NewShell initializes the shell helper (the "Shellper").
|
|
func NewShell(d *Doodle) Shell {
|
|
s := Shell{
|
|
parent: d,
|
|
History: []string{},
|
|
Output: []string{},
|
|
Flashes: []Flash{},
|
|
Prompt: ">",
|
|
cursor: '_',
|
|
cursorRate: balance.ShellCursorBlinkRate,
|
|
js: otto.New(),
|
|
}
|
|
|
|
// Make the Doodle instance available to the shell.
|
|
bindings := map[string]interface{}{
|
|
"d": d,
|
|
"log": log,
|
|
"RGBA": render.RGBA,
|
|
"Point": render.NewPoint,
|
|
"Rect": render.NewRect,
|
|
}
|
|
for name, v := range bindings {
|
|
err := s.js.Set(name, v)
|
|
if err != nil {
|
|
log.Error("Failed to make `%s` available to JS shell: %s", name, err)
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Close the shell, resetting its internal state.
|
|
func (s *Shell) Close() {
|
|
log.Debug("Shell: closing shell")
|
|
s.Open = false
|
|
s.Prompt = ">"
|
|
s.callback = nil
|
|
s.Text = ""
|
|
s.historyPaging = false
|
|
s.historyIndex = 0
|
|
}
|
|
|
|
// Execute a command in the shell.
|
|
func (s *Shell) Execute(input string) {
|
|
command := s.Parse(input)
|
|
if command.Raw != "" {
|
|
s.Output = append(s.Output, s.Prompt+command.Raw)
|
|
s.History = append(s.History, command.Raw)
|
|
}
|
|
|
|
// Are we answering a Prompt?
|
|
if s.callback != nil {
|
|
log.Info("Invoking prompt callback:")
|
|
s.callback(command.Raw)
|
|
s.Close()
|
|
return
|
|
}
|
|
|
|
if command.Command == "clear" {
|
|
s.Output = []string{}
|
|
} else {
|
|
err := command.Run(s.parent)
|
|
if err != nil {
|
|
s.Write(err.Error())
|
|
}
|
|
}
|
|
|
|
// Reset the text buffer in the shell.
|
|
s.Text = ""
|
|
}
|
|
|
|
// Write a line of output text to the console.
|
|
func (s *Shell) Write(line string) {
|
|
s.Output = append(s.Output, line)
|
|
s.Flashes = append(s.Flashes, Flash{
|
|
Text: line,
|
|
Expires: s.parent.ticks + balance.FlashTTL,
|
|
})
|
|
}
|
|
|
|
// Parse the command line.
|
|
func (s *Shell) Parse(input string) Command {
|
|
input = strings.TrimSpace(input)
|
|
if len(input) == 0 {
|
|
return Command{}
|
|
}
|
|
|
|
var (
|
|
inQuote bool
|
|
buffer = bytes.NewBuffer([]byte{})
|
|
words = []string{}
|
|
)
|
|
for i := 0; i < len(input); i++ {
|
|
char := input[i]
|
|
switch char {
|
|
case ' ':
|
|
if inQuote {
|
|
buffer.WriteByte(char)
|
|
continue
|
|
}
|
|
|
|
if word := buffer.String(); word != "" {
|
|
words = append(words, word)
|
|
buffer.Reset()
|
|
}
|
|
case '"':
|
|
if !inQuote {
|
|
// An opening quote character.
|
|
inQuote = true
|
|
} else {
|
|
// The closing quote.
|
|
inQuote = false
|
|
|
|
if word := buffer.String(); word != "" {
|
|
words = append(words, word)
|
|
buffer.Reset()
|
|
}
|
|
}
|
|
default:
|
|
buffer.WriteByte(char)
|
|
}
|
|
}
|
|
|
|
if remainder := buffer.String(); remainder != "" {
|
|
words = append(words, remainder)
|
|
}
|
|
|
|
return Command{
|
|
Raw: input,
|
|
Command: words[0],
|
|
Args: words[1:],
|
|
ArgsLiteral: strings.TrimSpace(input[len(words[0]):]),
|
|
}
|
|
}
|
|
|
|
// Draw the shell.
|
|
func (s *Shell) Draw(d *Doodle, ev *events.State) error {
|
|
if ev.EscapeKey.Read() {
|
|
s.Close()
|
|
return nil
|
|
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
|
|
s.Execute(s.Text)
|
|
s.Close()
|
|
return nil
|
|
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
|
|
// Paging through history.
|
|
if !s.historyPaging {
|
|
s.historyPaging = true
|
|
s.historyIndex = len(s.History)
|
|
}
|
|
|
|
// Consume the inputs and make convenient variables.
|
|
ev.Down.Read()
|
|
isUp := ev.Up.Read()
|
|
|
|
// Scroll through the input history.
|
|
if isUp {
|
|
s.historyIndex--
|
|
if s.historyIndex < 0 {
|
|
s.historyIndex = 0
|
|
}
|
|
} else {
|
|
s.historyIndex++
|
|
if s.historyIndex >= len(s.History) {
|
|
s.historyIndex = len(s.History) - 1
|
|
}
|
|
}
|
|
|
|
s.Text = s.History[s.historyIndex]
|
|
|
|
}
|
|
|
|
// Compute the line height we can draw.
|
|
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
|
|
|
|
// If the console is open, draw the console.
|
|
if s.Open {
|
|
// Cursor flip?
|
|
if d.ticks > s.cursorFlip {
|
|
s.cursorFlip = d.ticks + s.cursorRate
|
|
if s.cursor == ' ' {
|
|
s.cursor = '_'
|
|
} else {
|
|
s.cursor = ' '
|
|
}
|
|
}
|
|
|
|
// Read a character from the keyboard.
|
|
if key := ev.ReadKey(); key != "" {
|
|
// Backspace?
|
|
if key == `\b` {
|
|
if len(s.Text) > 0 {
|
|
s.Text = s.Text[:len(s.Text)-1]
|
|
}
|
|
} else {
|
|
s.Text += key
|
|
}
|
|
}
|
|
|
|
// How tall is the box?
|
|
boxHeight := int32(lineHeight*(balance.ShellHistoryLineCount+1)) + balance.ShellPadding
|
|
|
|
// Draw the background color.
|
|
d.Engine.DrawBox(
|
|
balance.ShellBackgroundColor,
|
|
render.Rect{
|
|
X: 0,
|
|
Y: d.height - boxHeight,
|
|
W: d.width,
|
|
H: boxHeight,
|
|
},
|
|
)
|
|
|
|
// Draw the recent commands.
|
|
outputY := d.height - int32(lineHeight*2)
|
|
for i := 0; i < balance.ShellHistoryLineCount; i++ {
|
|
if len(s.Output) > i {
|
|
line := s.Output[len(s.Output)-1-i]
|
|
d.Engine.DrawText(
|
|
render.Text{
|
|
FontFilename: balance.ShellFontFilename,
|
|
Text: line,
|
|
Size: balance.ShellFontSize,
|
|
Color: balance.ShellForegroundColor,
|
|
},
|
|
render.Point{
|
|
X: balance.ShellPadding,
|
|
Y: outputY,
|
|
},
|
|
)
|
|
}
|
|
outputY -= int32(lineHeight)
|
|
}
|
|
|
|
// Draw the command prompt.
|
|
d.Engine.DrawText(
|
|
render.Text{
|
|
FontFilename: balance.ShellFontFilename,
|
|
Text: s.Prompt + s.Text + string(s.cursor),
|
|
Size: balance.ShellFontSize,
|
|
Color: balance.ShellPromptColor,
|
|
},
|
|
render.Point{
|
|
X: balance.ShellPadding,
|
|
Y: d.height - int32(balance.ShellFontSize) - balance.ShellPadding,
|
|
},
|
|
)
|
|
} else if len(s.Flashes) > 0 {
|
|
// Otherwise, just draw flashed messages.
|
|
valid := false // Did we actually draw any?
|
|
|
|
outputY := d.height - int32(lineHeight*2)
|
|
for i := len(s.Flashes); i > 0; i-- {
|
|
flash := s.Flashes[i-1]
|
|
if d.ticks >= flash.Expires {
|
|
continue
|
|
}
|
|
|
|
d.Engine.DrawText(
|
|
render.Text{
|
|
Text: flash.Text,
|
|
Size: balance.ShellFontSize,
|
|
Color: render.SkyBlue,
|
|
Stroke: render.Grey,
|
|
Shadow: render.Black,
|
|
},
|
|
render.Point{
|
|
X: balance.ShellPadding,
|
|
Y: outputY,
|
|
},
|
|
)
|
|
outputY -= int32(lineHeight)
|
|
valid = true
|
|
}
|
|
|
|
// If we've exhausted all flashes, free up the memory.
|
|
if !valid {
|
|
s.Flashes = []Flash{}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|