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.
This commit is contained in:
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 ```bash
$ sudo dnf install SDL2-devel SDL2_ttf-devel $ 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. // Shell related variables.
var ( var (
// TODO: why not renders transparent // TODO: why not renders transparent
ShellBackgroundColor = render.RGBA(0, 10, 20, 128) ShellFontFilename = "./fonts/DejaVuSansMono.ttf"
ShellForegroundColor = render.White ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
ShellPromptColor = render.White
ShellPadding int32 = 8 ShellPadding int32 = 8
ShellFontSize = 16 ShellFontSize = 16
ShellCursorBlinkRate uint64 = 20 ShellCursorBlinkRate uint64 = 20
@ -17,10 +19,3 @@ var (
// Ticks that a flashed message persists for. // Ticks that a flashed message persists for.
FlashTTL uint64 = 400 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. // Command line shell.
if d.shell.Open { if d.shell.Open {
} else if ev.EnterKey.Read() { } else if ev.EnterKey.Read() {
log.Debug("Shell: opening shell") log.Debug("Shell: opening shell")
d.shell.Open = true d.shell.Open = true

View File

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

View File

@ -21,6 +21,7 @@ type EditorUI struct {
// Widgets // Widgets
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
MenuBar *ui.Frame
Palette *ui.Window Palette *ui.Window
StatusBar *ui.Frame StatusBar *ui.Frame
} }
@ -35,6 +36,7 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusPaletteText: "Swatch: <none>", StatusPaletteText: "Swatch: <none>",
StatusFilenameText: "Filename: <none>", StatusFilenameText: "Filename: <none>",
} }
u.MenuBar = u.SetupMenuBar(d)
u.StatusBar = u.SetupStatusBar(d) u.StatusBar = u.SetupStatusBar(d)
u.Palette = u.SetupPalette(d) u.Palette = u.SetupPalette(d)
return u return u
@ -61,26 +63,124 @@ func (u *EditorUI) Loop(ev *events.State) {
filename, filename,
) )
u.MenuBar.Compute(u.d.Engine)
u.StatusBar.Compute(u.d.Engine) u.StatusBar.Compute(u.d.Engine)
u.Palette.Compute(u.d.Engine) u.Palette.Compute(u.d.Engine)
} }
// Present the UI to the screen. // Present the UI to the screen.
func (u *EditorUI) Present(e render.Engine) { 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.Palette.Present(e, u.Palette.Point())
u.MenuBar.Present(e, u.MenuBar.Point())
u.StatusBar.Present(e, u.StatusBar.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. // SetupPalette sets up the palette panel.
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
log.Error("SetupPalette Window")
window := ui.NewWindow("Palette") window := ui.NewWindow("Palette")
window.ConfigureTitle(balance.TitleConfig)
window.TitleBar().Font = balance.TitleFont
window.Configure(ui.Config{ window.Configure(ui.Config{
Width: 150, Width: 150,
Height: u.d.height - u.StatusBar.Size().H, Height: u.d.height - u.StatusBar.Size().H,
Background: balance.WindowBackground,
BorderColor: balance.WindowBorder,
}) })
window.MoveTo(render.NewPoint( window.MoveTo(render.NewPoint(
u.d.width-window.BoxSize().W, u.d.width-window.BoxSize().W,
0, u.MenuBar.BoxSize().H,
)) ))
// Handler function for the radio buttons being clicked. // 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{ window.Pack(btn, ui.Pack{
Anchor: ui.N, Anchor: ui.N,
Fill: true, Fill: true,
PadY: 4,
}) })
} }
@ -140,6 +241,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
cursorLabel.Compute(d.Engine) cursorLabel.Compute(d.Engine)
frame.Pack(cursorLabel, ui.Pack{ frame.Pack(cursorLabel, ui.Pack{
Anchor: ui.W, Anchor: ui.W,
PadX: 1,
}) })
paletteLabel := ui.NewLabel(ui.Label{ paletteLabel := ui.NewLabel(ui.Label{
@ -150,6 +252,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
paletteLabel.Compute(d.Engine) paletteLabel.Compute(d.Engine)
frame.Pack(paletteLabel, ui.Pack{ frame.Pack(paletteLabel, ui.Pack{
Anchor: ui.W, Anchor: ui.W,
PadX: 1,
}) })
filenameLabel := ui.NewLabel(ui.Label{ filenameLabel := ui.NewLabel(ui.Label{
@ -160,6 +263,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
filenameLabel.Compute(d.Engine) filenameLabel.Compute(d.Engine)
frame.Pack(filenameLabel, ui.Pack{ frame.Pack(filenameLabel, ui.Pack{
Anchor: ui.W, Anchor: ui.W,
PadX: 1,
}) })
// TODO: right-aligned labels clip out of bounds // TODO: right-aligned labels clip out of bounds

2
fps.go
View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
package level package level
import ( import (
"fmt"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
) )
@ -60,9 +62,17 @@ func (s Swatch) String() string {
// Index returns the Swatch's position in the palette. // Index returns the Swatch's position in the palette.
func (s Swatch) Index() int { func (s Swatch) Index() int {
fmt.Printf("%+v index: %d", s, s.index)
return 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. // Get a swatch by name.
func (p *Palette) Get(name string) (result *Swatch, exists bool) { func (p *Palette) Get(name string) (result *Swatch, exists bool) {
p.update() 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. // HexColor parses a color from hexadecimal code.
func HexColor(hex string) (Color, error) { func HexColor(hex string) (Color, error) {
c := Black // default color c := Black // default color
@ -80,8 +89,8 @@ func HexColor(hex string) (Color, error) {
func (c Color) String() string { func (c Color) String() string {
return fmt.Sprintf( return fmt.Sprintf(
"Color<#%02x%02x%02x>", "Color<#%02x%02x%02x+%02x>",
c.Red, c.Green, c.Blue, 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. // Text holds information for drawing text.
type Text struct { type Text struct {
Text string Text string
Size int Size int
Color Color Color Color
Padding int32 Padding int32
Stroke Color // Stroke color (if not zero) PadX int32
Shadow Color // Drop shadow color (if not zero) 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 { func (t Text) String() string {

View File

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

View File

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

View File

@ -16,16 +16,24 @@ func (d *Doodle) Flash(template string, v ...interface{}) {
d.shell.Write(fmt.Sprintf(template, v...)) 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. // Shell implements the developer console in-game.
type Shell struct { type Shell struct {
parent *Doodle parent *Doodle
Open bool Open bool
Prompt string Prompt string
Text string callback func(string) // for prompt answers only
History []string Text string
Output []string History []string
Flashes []Flash Output []string
Flashes []Flash
// Blinky cursor variables. // Blinky cursor variables.
cursor byte // cursor symbol cursor byte // cursor symbol
@ -82,6 +90,7 @@ func (s *Shell) Close() {
log.Debug("Shell: closing shell") log.Debug("Shell: closing shell")
s.Open = false s.Open = false
s.Prompt = ">" s.Prompt = ">"
s.callback = nil
s.Text = "" s.Text = ""
s.historyPaging = false s.historyPaging = false
s.historyIndex = 0 s.historyIndex = 0
@ -95,6 +104,14 @@ func (s *Shell) Execute(input string) {
s.History = append(s.History, 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" { if command.Command == "clear" {
s.Output = []string{} s.Output = []string{}
} else { } else {
@ -257,9 +274,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
line := s.Output[len(s.Output)-1-i] line := s.Output[len(s.Output)-1-i]
d.Engine.DrawText( d.Engine.DrawText(
render.Text{ render.Text{
Text: line, FontFilename: balance.ShellFontFilename,
Size: balance.ShellFontSize, Text: line,
Color: render.Grey, Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor,
}, },
render.Point{ render.Point{
X: balance.ShellPadding, X: balance.ShellPadding,
@ -273,9 +291,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Draw the command prompt. // Draw the command prompt.
d.Engine.DrawText( d.Engine.DrawText(
render.Text{ render.Text{
Text: s.Prompt + s.Text + string(s.cursor), FontFilename: balance.ShellFontFilename,
Size: balance.ShellFontSize, Text: s.Prompt + s.Text + string(s.cursor),
Color: balance.ShellForegroundColor, Size: balance.ShellFontSize,
Color: balance.ShellPromptColor,
}, },
render.Point{ render.Point{
X: balance.ShellPadding, X: balance.ShellPadding,

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,11 @@ func (w *Window) TitleBar() *Label {
func (w *Window) Configure(C Config) { func (w *Window) Configure(C Config) {
w.BaseWidget.Configure(C) w.BaseWidget.Configure(C)
w.body.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. // ConfigureTitle configures the title bar widget.