diff --git a/README.md b/README.md index e7a9c53..7c439b7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/balance/shell.go b/balance/shell.go index 2c9ce50..1657836 100644 --- a/balance/shell.go +++ b/balance/shell.go @@ -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, -} diff --git a/balance/theme.go b/balance/theme.go new file mode 100644 index 0000000..a2c4aee --- /dev/null +++ b/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, + } +) diff --git a/doodle.go b/doodle.go index 8c14ff7..e7a3431 100644 --- a/doodle.go +++ b/doodle.go @@ -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 diff --git a/editor_scene.go b/editor_scene.go index ce03c98..3809a09 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -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) diff --git a/editor_ui.go b/editor_ui.go index 0fd8538..a1ed9ea 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -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: ", StatusFilenameText: "Filename: ", } + 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 diff --git a/fps.go b/fps.go index dd8eb82..9898d60 100644 --- a/fps.go +++ b/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 { diff --git a/guitest_scene.go b/guitest_scene.go index da736cc..90fce3e 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -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{ diff --git a/level/json.go b/level/json.go index 53aef71..f5a8385 100644 --- a/level/json.go +++ b/level/json.go @@ -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( diff --git a/level/palette.go b/level/palette.go index a13d0b4..8c63e05 100644 --- a/level/palette.go +++ b/level/palette.go @@ -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() diff --git a/render/color.go b/render/color.go index d660803..7a88637 100644 --- a/render/color.go +++ b/render/color.go @@ -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, ) } diff --git a/render/interface.go b/render/interface.go index 09c5b16..a977570 100644 --- a/render/interface.go +++ b/render/interface.go @@ -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 { diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go index abfa114..89b7252 100644 --- a/render/sdl/sdl.go +++ b/render/sdl/sdl.go @@ -82,6 +82,7 @@ func (r *Renderer) Setup() error { if err != nil { panic(err) } + renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND) r.renderer = renderer return nil diff --git a/render/sdl/text.go b/render/sdl/text.go index 20b72c2..dc00129 100644 --- a/render/sdl/text.go +++ b/render/sdl/text.go @@ -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 } diff --git a/shell.go b/shell.go index 77f2353..c5f44f2 100644 --- a/shell.go +++ b/shell.go @@ -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, diff --git a/ui/button.go b/ui/button.go index 11e07e5..e279705 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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) diff --git a/ui/frame.go b/ui/frame.go index f1e08a3..2c3d218 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -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 diff --git a/ui/frame_pack.go b/ui/frame_pack.go index 4c2b09b..f0061d4 100644 --- a/ui/frame_pack.go +++ b/ui/frame_pack.go @@ -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) diff --git a/ui/label.go b/ui/label.go index f3b2766..ef464b8 100644 --- a/ui/label.go +++ b/ui/label.go @@ -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, }) } diff --git a/ui/window.go b/ui/window.go index 32001ff..5f17803 100644 --- a/ui/window.go +++ b/ui/window.go @@ -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.