Menu Toolbar for Editor + Shell Prompts + Theme

* 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.
chunks
Noah 2018-08-11 17:30:00 -07:00
parent 42caa20f6e
commit 5956863996
20 changed files with 283 additions and 61 deletions

View File

@ -201,3 +201,15 @@ Fedora dependencies:
```bash
$ sudo dnf install SDL2-devel SDL2_ttf-devel
```
## Fonts
The `fonts/` folder is git-ignored. The app currently uses font files here
named:
* `DejaVuSans.ttf` for sans-serif font.
* `DejaVuSans-Bold.ttf` for bold sans-serif font.
* `DejaVuSansMono.ttf` for monospace font.
These are the open source **DejaVu Sans [Mono]** fonts, so copy them in from
your `/usr/share/fonts/dejavu` folder or provide alternative fonts.

View File

@ -7,8 +7,10 @@ import (
// Shell related variables.
var (
// TODO: why not renders transparent
ShellBackgroundColor = render.RGBA(0, 10, 20, 128)
ShellForegroundColor = render.White
ShellFontFilename = "./fonts/DejaVuSansMono.ttf"
ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
ShellPromptColor = render.White
ShellPadding int32 = 8
ShellFontSize = 16
ShellCursorBlinkRate uint64 = 20
@ -17,10 +19,3 @@ var (
// Ticks that a flashed message persists for.
FlashTTL uint64 = 400
)
// StatusFont is the font for the status bar.
var StatusFont = render.Text{
Size: 12,
Padding: 4,
Color: render.Black,
}

39
balance/theme.go Normal file
View File

@ -0,0 +1,39 @@
package balance
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// Theme and appearance variables.
var (
// Window and panel styles.
TitleConfig = ui.Config{
Background: render.MustHexColor("#FF9900"),
OutlineSize: 1,
OutlineColor: render.Black,
}
TitleFont = render.Text{
FontFilename: "./fonts/DejaVuSans-Bold.ttf",
Size: 12,
Padding: 4,
Color: render.White,
Stroke: render.Red,
}
WindowBackground = render.MustHexColor("#cdb689")
WindowBorder = render.Grey
// Menu bar styles.
MenuBackground = render.Black
MenuFont = render.Text{
Size: 12,
PadX: 4,
}
// StatusFont is the font for the status bar.
StatusFont = render.Text{
Size: 12,
Padding: 4,
Color: render.Black,
}
)

View File

@ -96,7 +96,6 @@ func (d *Doodle) Run() error {
// Command line shell.
if d.shell.Open {
} else if ev.EnterKey.Read() {
log.Debug("Shell: opening shell")
d.shell.Open = true

View File

@ -43,6 +43,8 @@ func (s *EditorScene) Name() string {
// Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error {
s.Palette = level.DefaultPalette()
// Were we given configuration data?
if s.Filename != "" {
log.Debug("EditorScene: Set filename to %s", s.Filename)
@ -61,7 +63,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.Canvas = nil
}
s.Palette = level.DefaultPalette()
// Select the first swatch in the palette.
if len(s.Palette.Swatches) > 0 {
s.Swatch = s.Palette.Swatches[0]
s.Palette.ActiveSwatch = s.Swatch.Name
@ -103,9 +105,6 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
return nil
}
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
// Clicking? Log all the pixels while doing so.
if ev.Button1.Now {
// log.Warn("Button1: %+v", ev.Button1)
@ -149,6 +148,9 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
// Draw the current frame.
func (s *EditorScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
s.canvas.Draw(d.Engine)
s.UI.Present(d.Engine)

View File

@ -21,6 +21,7 @@ type EditorUI struct {
// Widgets
Supervisor *ui.Supervisor
MenuBar *ui.Frame
Palette *ui.Window
StatusBar *ui.Frame
}
@ -35,6 +36,7 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusPaletteText: "Swatch: <none>",
StatusFilenameText: "Filename: <none>",
}
u.MenuBar = u.SetupMenuBar(d)
u.StatusBar = u.SetupStatusBar(d)
u.Palette = u.SetupPalette(d)
return u
@ -61,26 +63,124 @@ func (u *EditorUI) Loop(ev *events.State) {
filename,
)
u.MenuBar.Compute(u.d.Engine)
u.StatusBar.Compute(u.d.Engine)
u.Palette.Compute(u.d.Engine)
}
// Present the UI to the screen.
func (u *EditorUI) Present(e render.Engine) {
// TODO: if I don't Compute() the palette window, then, whenever the dev console
// is open the window will blank out its contents leaving only the outermost Frame.
// The title bar and borders are gone. But other UI widgets don't do this.
// FIXME: Scene interface should have a separate ComputeUI() from Loop()?
u.Palette.Compute(u.d.Engine)
u.Palette.Present(e, u.Palette.Point())
u.MenuBar.Present(e, u.MenuBar.Point())
u.StatusBar.Present(e, u.StatusBar.Point())
}
// SetupMenuBar sets up the menu bar.
func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
frame := ui.NewFrame("MenuBar")
frame.Configure(ui.Config{
Width: d.width,
Background: render.Black,
})
type menuButton struct {
Text string
Click func(render.Point)
}
buttons := []menuButton{
menuButton{
Text: "New Level",
Click: func(render.Point) {
d.NewMap()
},
},
menuButton{
Text: "New Doodad",
Click: func(render.Point) {
d.NewMap()
},
},
menuButton{
Text: "Save",
Click: func(render.Point) {
if u.Scene.filename != "" {
u.Scene.SaveLevel(u.Scene.filename)
d.Flash("Saved: %s", u.Scene.filename)
} else {
d.Prompt("Save filename>", func(answer string) {
if answer != "" {
u.Scene.SaveLevel("./maps/" + answer) // TODO: maps path
d.Flash("Saved: %s", answer)
}
})
}
},
},
menuButton{
Text: "Save as...",
Click: func(render.Point) {
d.Prompt("Save as filename>", func(answer string) {
if answer != "" {
u.Scene.SaveLevel("./maps/" + answer) // TODO: maps path
d.Flash("Saved: %s", answer)
}
})
},
},
menuButton{
Text: "Load",
Click: func(render.Point) {
d.Prompt("Open filename>", func(answer string) {
if answer != "" {
u.d.EditLevel("./maps/" + answer) // TODO: maps path
}
})
},
},
}
for _, btn := range buttons {
w := ui.NewButton(btn.Text, ui.NewLabel(ui.Label{
Text: btn.Text,
Font: balance.MenuFont,
}))
w.Configure(ui.Config{
BorderSize: 1,
OutlineSize: 0,
})
w.Handle("MouseUp", btn.Click)
u.Supervisor.Add(w)
frame.Pack(w, ui.Pack{
Anchor: ui.W,
PadX: 1,
})
}
frame.Compute(d.Engine)
return frame
}
// SetupPalette sets up the palette panel.
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
log.Error("SetupPalette Window")
window := ui.NewWindow("Palette")
window.ConfigureTitle(balance.TitleConfig)
window.TitleBar().Font = balance.TitleFont
window.Configure(ui.Config{
Width: 150,
Height: u.d.height - u.StatusBar.Size().H,
Width: 150,
Height: u.d.height - u.StatusBar.Size().H,
Background: balance.WindowBackground,
BorderColor: balance.WindowBorder,
})
window.MoveTo(render.NewPoint(
u.d.width-window.BoxSize().W,
0,
u.MenuBar.BoxSize().H,
))
// Handler function for the radio buttons being clicked.
@ -109,6 +209,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
window.Pack(btn, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
}
@ -140,6 +241,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
cursorLabel.Compute(d.Engine)
frame.Pack(cursorLabel, ui.Pack{
Anchor: ui.W,
PadX: 1,
})
paletteLabel := ui.NewLabel(ui.Label{
@ -150,6 +252,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
paletteLabel.Compute(d.Engine)
frame.Pack(paletteLabel, ui.Pack{
Anchor: ui.W,
PadX: 1,
})
filenameLabel := ui.NewLabel(ui.Label{
@ -160,6 +263,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
filenameLabel.Compute(d.Engine)
frame.Pack(filenameLabel, ui.Pack{
Anchor: ui.W,
PadX: 1,
})
// TODO: right-aligned labels clip out of bounds

2
fps.go
View File

@ -49,7 +49,7 @@ func (d *Doodle) DrawDebugOverlay() {
},
render.Point{
X: DebugTextPadding,
Y: DebugTextPadding,
Y: DebugTextPadding + 32, // extra padding to not overlay menu bars
},
)
if err != nil {

View File

@ -216,13 +216,14 @@ func (s *GUITestScene) Setup(d *Doodle) error {
log.Info("Button1 bg: %s", button1.Background())
button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{
Text: "New Map",
Text: "Load Map",
Font: balance.StatusFont,
}))
button2.Handle("Click", func(p render.Point) {
d.Flash("Button2 clicked")
d.Prompt("Map name>", func(name string) {
d.EditLevel(name)
})
})
button2.SetText("Load Map")
var align = ui.W
btnFrame.Pack(button1, ui.Pack{

View File

@ -32,6 +32,7 @@ func LoadJSON(filename string) (*Level, error) {
}
// Inflate the private instance values.
m.Palette.Inflate()
for _, px := range m.Pixels {
if int(px.PaletteIndex) > len(m.Palette.Swatches) {
return nil, fmt.Errorf(

View File

@ -1,6 +1,8 @@
package level
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
)
@ -60,9 +62,17 @@ func (s Swatch) String() string {
// Index returns the Swatch's position in the palette.
func (s Swatch) Index() int {
fmt.Printf("%+v index: %d", s, s.index)
return s.index
}
// Inflate the palette swatch caches. Always call this method after you have
// initialized the palette (i.e. loaded it from JSON); this will update the
// "color by name" cache and assign the index numbers to each swatch.
func (p *Palette) Inflate() {
p.update()
}
// Get a swatch by name.
func (p *Palette) Get(name string) (result *Swatch, exists bool) {
p.update()

View File

@ -36,6 +36,15 @@ func RGBA(r, g, b, a uint8) Color {
}
}
// MustHexColor parses a color from hex code or panics.
func MustHexColor(hex string) Color {
color, err := HexColor(hex)
if err != nil {
panic(err)
}
return color
}
// HexColor parses a color from hexadecimal code.
func HexColor(hex string) (Color, error) {
c := Black // default color
@ -80,8 +89,8 @@ func HexColor(hex string) (Color, error) {
func (c Color) String() string {
return fmt.Sprintf(
"Color<#%02x%02x%02x>",
c.Red, c.Green, c.Blue,
"Color<#%02x%02x%02x+%02x>",
c.Red, c.Green, c.Blue, c.Alpha,
)
}

View File

@ -95,12 +95,15 @@ func (r Rect) IsZero() bool {
// Text holds information for drawing text.
type Text struct {
Text string
Size int
Color Color
Padding int32
Stroke Color // Stroke color (if not zero)
Shadow Color // Drop shadow color (if not zero)
Text string
Size int
Color Color
Padding int32
PadX int32
PadY int32
Stroke Color // Stroke color (if not zero)
Shadow Color // Drop shadow color (if not zero)
FontFilename string // Path to *.ttf file on disk
}
func (t Text) String() string {

View File

@ -82,6 +82,7 @@ func (r *Renderer) Setup() error {
if err != nil {
panic(err)
}
renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
r.renderer = renderer
return nil

View File

@ -1,6 +1,7 @@
package sdl
import (
"fmt"
"strings"
"git.kirsle.net/apps/doodle/events"
@ -9,19 +10,28 @@ import (
"github.com/veandco/go-sdl2/ttf"
)
var fonts map[int]*ttf.Font = map[int]*ttf.Font{}
// TODO: font filenames
var defaultFontFilename = "./fonts/DejaVuSans.ttf"
var fonts = map[string]*ttf.Font{}
// LoadFont loads and caches the font at a given size.
func LoadFont(size int) (*ttf.Font, error) {
if font, ok := fonts[size]; ok {
func LoadFont(filename string, size int) (*ttf.Font, error) {
if filename == "" {
filename = defaultFontFilename
}
// Cached font available?
keyName := fmt.Sprintf("%s@%d", filename, size)
if font, ok := fonts[keyName]; ok {
return font, nil
}
font, err := ttf.OpenFont("./fonts/DejaVuSansMono.ttf", size)
font, err := ttf.OpenFont(filename, size)
if err != nil {
return nil, err
}
fonts[size] = font
fonts[keyName] = font
return font, nil
}
@ -51,7 +61,7 @@ func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) {
err error
)
if font, err = LoadFont(text.Size); err != nil {
if font, err = LoadFont(text.FontFilename, text.Size); err != nil {
return rect, err
}
@ -74,7 +84,7 @@ func (r *Renderer) DrawText(text render.Text, point render.Point) error {
err error
)
if font, err = LoadFont(text.Size); err != nil {
if font, err = LoadFont(text.FontFilename, text.Size); err != nil {
return err
}

View File

@ -16,16 +16,24 @@ 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
Text string
History []string
Output []string
Flashes []Flash
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
@ -82,6 +90,7 @@ 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
@ -95,6 +104,14 @@ func (s *Shell) Execute(input string) {
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 {
@ -257,9 +274,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
line := s.Output[len(s.Output)-1-i]
d.Engine.DrawText(
render.Text{
Text: line,
Size: balance.ShellFontSize,
Color: render.Grey,
FontFilename: balance.ShellFontFilename,
Text: line,
Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor,
},
render.Point{
X: balance.ShellPadding,
@ -273,9 +291,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Draw the command prompt.
d.Engine.DrawText(
render.Text{
Text: s.Prompt + s.Text + string(s.cursor),
Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor,
FontFilename: balance.ShellFontFilename,
Text: s.Prompt + s.Text + string(s.cursor),
Size: balance.ShellFontSize,
Color: balance.ShellPromptColor,
},
render.Point{
X: balance.ShellPadding,

View File

@ -106,8 +106,6 @@ func (w *Button) Present(e render.Engine, P render.Point) {
if S.Bigger(ChildSize) {
moveTo.X = P.X + (S.W / 2) - (ChildSize.W / 2)
}
_ = S
_ = ChildSize
// Draw the text label inside.
w.child.Present(e, moveTo)

View File

@ -22,9 +22,8 @@ func NewFrame(name string) *Frame {
widgets: []Widget{},
}
w.IDFunc(func() string {
return fmt.Sprintf("Frame<%s; %d widgets>",
return fmt.Sprintf("Frame<%s>",
name,
len(w.widgets),
)
})
return w

View File

@ -41,12 +41,17 @@ func (w *Frame) computePacked(e render.Engine) {
}
for _, packedWidget := range w.packs[anchor] {
child := packedWidget.widget
pack := packedWidget.pack
child.Compute(e)
x += pack.PadX
y += pack.PadY
var (
// point = child.Point()
size = child.BoxSize()
size = child.Size()
yStep = y * yDirection
xStep = x * xDirection
)
@ -59,19 +64,19 @@ func (w *Frame) computePacked(e render.Engine) {
}
if anchor.IsSouth() {
y -= size.H + (pack.PadY * 2)
y -= size.H + pack.PadY
}
if anchor.IsEast() {
x -= size.W + (pack.PadX * 2)
x -= size.W + pack.PadX
}
child.MoveTo(render.NewPoint(x, y))
if anchor.IsNorth() {
y += size.H + (pack.PadY * 2)
y += size.H + pack.PadY
}
if anchor == W {
x += size.W + (pack.PadX * 2)
x += size.W + pack.PadX
}
visited = append(visited, packedWidget)

View File

@ -66,10 +66,15 @@ func (w *Label) Compute(e render.Engine) {
return
}
var (
padX = w.Font.Padding + w.Font.PadX
padY = w.Font.Padding + w.Font.PadY
)
if !w.FixedSize() {
w.resizeAuto(render.Rect{
W: rect.W + (w.Font.Padding * 2),
H: rect.H + (w.Font.Padding * 2),
W: rect.W + (padX * 2),
H: rect.H + (padY * 2),
})
}
@ -83,9 +88,14 @@ func (w *Label) Compute(e render.Engine) {
func (w *Label) Present(e render.Engine, P render.Point) {
border := w.BoxThickness(1)
var (
padX = w.Font.Padding + w.Font.PadX
padY = w.Font.Padding + w.Font.PadY
)
w.DrawBox(e, P)
e.DrawText(w.text(), render.Point{
X: P.X + border + w.Font.Padding,
Y: P.Y + border + w.Font.Padding,
X: P.X + border + padX,
Y: P.Y + border + padY,
})
}

View File

@ -79,6 +79,11 @@ func (w *Window) TitleBar() *Label {
func (w *Window) Configure(C Config) {
w.BaseWidget.Configure(C)
w.body.Configure(C)
// Don't pass dimensions down any further than the body.
C.Width = 0
C.Height = 0
w.content.Configure(C)
}
// ConfigureTitle configures the title bar widget.