From 595686399663fcf5c500ddeb1d41f98a8ddc0443 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 11 Aug 2018 17:30:00 -0700 Subject: [PATCH] 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. --- README.md | 12 +++++ balance/shell.go | 13 ++---- balance/theme.go | 39 ++++++++++++++++ doodle.go | 1 - editor_scene.go | 10 ++-- editor_ui.go | 110 ++++++++++++++++++++++++++++++++++++++++++-- fps.go | 2 +- guitest_scene.go | 7 +-- level/json.go | 1 + level/palette.go | 10 ++++ render/color.go | 13 +++++- render/interface.go | 15 +++--- render/sdl/sdl.go | 1 + render/sdl/text.go | 24 +++++++--- shell.go | 43 ++++++++++++----- ui/button.go | 2 - ui/frame.go | 3 +- ui/frame_pack.go | 15 ++++-- ui/label.go | 18 ++++++-- ui/window.go | 5 ++ 20 files changed, 283 insertions(+), 61 deletions(-) create mode 100644 balance/theme.go 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.