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.
这个提交包含在:
Noah 2018-08-11 17:30:00 -07:00
父节点 42caa20f6e
当前提交 5956863996
共有 20 个文件被更改,包括 283 次插入61 次删除

查看文件

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

查看文件

@ -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 普通文件
查看文件

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

查看文件

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

查看文件

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

查看文件

@ -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
查看文件

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

查看文件

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

查看文件

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

查看文件

@ -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()

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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

查看文件

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