Add Palette Window and Palette Support to Edit Mode
* Add ui.Window to easily create reusable windows with titles. * Add a palette window (panel) to the right edge of the Edit Mode. * Has Radio Buttons listing the colors available in the palette. * Add palette support to Edit Mode so when you draw pixels, they take on the color and attributes of the currently selected Swatch in your palette. * Revise the on-disk format to better serialize the Palette object to JSON. * Break Play Mode: collision detection fails because the Grid key elements are now full Pixel objects (which retain their Palette and Swatch properties). * The Grid will need to be re-worked to separate X,Y coordinates from the Pixel metadata to just test "is something there, and what is it?"
This commit is contained in:
parent
8624a28ea9
commit
e1cbff8c3f
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
fonts/
|
||||
maps/
|
||||
bin/
|
||||
screenshot-*.png
|
||||
map-*.json
|
||||
|
|
|
@ -40,6 +40,7 @@ func main() {
|
|||
)
|
||||
|
||||
app := doodle.New(debug, engine)
|
||||
app.SetupEngine()
|
||||
if filename != "" {
|
||||
if edit {
|
||||
app.EditLevel(filename)
|
||||
|
|
|
@ -68,7 +68,7 @@ const (
|
|||
)
|
||||
|
||||
// CollidesWithGrid checks if a Doodad collides with level geometry.
|
||||
func CollidesWithGrid(d Doodad, grid *render.Grid, target render.Point) (*Collide, bool) {
|
||||
func CollidesWithGrid(d Doodad, grid *level.Grid, target render.Point) (*Collide, bool) {
|
||||
var (
|
||||
P = d.Position()
|
||||
S = d.Size()
|
||||
|
@ -280,7 +280,7 @@ func GetCollisionBox(box render.Rect) CollisionBox {
|
|||
|
||||
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and
|
||||
// returns if any of them intersect with level geometry.
|
||||
func (c *Collide) ScanBoundingBox(box render.Rect, grid *render.Grid) bool {
|
||||
func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Grid) bool {
|
||||
col := GetCollisionBox(box)
|
||||
|
||||
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
|
||||
|
@ -293,9 +293,9 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *render.Grid) bool {
|
|||
// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests
|
||||
// for any pixels to be set, implying a collision between level geometry and the
|
||||
// bounding boxes of the doodad.
|
||||
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *render.Grid, side Side) {
|
||||
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Grid, side Side) {
|
||||
for point := range render.IterLine2(p1, p2) {
|
||||
if grid.Exists(level.Pixel{
|
||||
if grid.Exists(&level.Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
}) {
|
||||
|
|
|
@ -72,7 +72,7 @@ func (p *Player) SetGrounded(v bool) {
|
|||
|
||||
// Draw the player sprite.
|
||||
func (p *Player) Draw(e render.Engine) {
|
||||
e.DrawBox(render.Color{255, 255, 153, 255}, render.Rect{
|
||||
e.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{
|
||||
X: p.point.X,
|
||||
Y: p.point.Y,
|
||||
W: p.size.W,
|
||||
|
|
28
doodle.go
28
doodle.go
|
@ -20,8 +20,9 @@ const (
|
|||
|
||||
// Doodle is the game object.
|
||||
type Doodle struct {
|
||||
Debug bool
|
||||
Engine render.Engine
|
||||
Debug bool
|
||||
Engine render.Engine
|
||||
engineReady bool
|
||||
|
||||
startTime time.Time
|
||||
running bool
|
||||
|
@ -54,12 +55,22 @@ func New(debug bool, engine render.Engine) *Doodle {
|
|||
return d
|
||||
}
|
||||
|
||||
// Run initializes SDL and starts the main loop.
|
||||
func (d *Doodle) Run() error {
|
||||
// Set up the render engine.
|
||||
// SetupEngine sets up the rendering engine.
|
||||
func (d *Doodle) SetupEngine() error {
|
||||
if err := d.Engine.Setup(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.engineReady = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run initializes SDL and starts the main loop.
|
||||
func (d *Doodle) Run() error {
|
||||
if !d.engineReady {
|
||||
if err := d.SetupEngine(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the default scene.
|
||||
if d.Scene == nil {
|
||||
|
@ -156,10 +167,9 @@ func (d *Doodle) NewMap() {
|
|||
// EditLevel loads a map from JSON into the EditorScene.
|
||||
func (d *Doodle) EditLevel(filename string) error {
|
||||
log.Info("Loading level from file: %s", filename)
|
||||
scene := &EditorScene{}
|
||||
err := scene.LoadLevel(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
scene := &EditorScene{
|
||||
Filename: filename,
|
||||
OpenFile: true,
|
||||
}
|
||||
d.Goto(scene)
|
||||
return nil
|
||||
|
|
|
@ -18,14 +18,17 @@ type EditorScene struct {
|
|||
// Configuration for the scene initializer.
|
||||
OpenFile bool
|
||||
Filename string
|
||||
Canvas render.Grid
|
||||
Canvas level.Grid
|
||||
|
||||
UI *EditorUI
|
||||
|
||||
Palette *level.Palette // Full palette of swatches for this level
|
||||
Swatch *level.Swatch // actively selected painting swatch
|
||||
|
||||
// History of all the pixels placed by the user.
|
||||
pixelHistory []level.Pixel
|
||||
pixelHistory []*level.Pixel
|
||||
lastPixel *level.Pixel // last pixel placed while mouse down and dragging
|
||||
canvas render.Grid
|
||||
canvas level.Grid
|
||||
filename string // Last saved filename.
|
||||
|
||||
// Canvas size
|
||||
|
@ -48,7 +51,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
if s.OpenFile {
|
||||
log.Debug("EditorScene: Loading map from filename at %s", s.filename)
|
||||
if err := s.LoadLevel(s.filename); err != nil {
|
||||
return err
|
||||
d.Flash("LoadLevel error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,16 +61,23 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
s.Canvas = nil
|
||||
}
|
||||
|
||||
s.UI = NewEditorUI(d)
|
||||
s.Palette = level.DefaultPalette()
|
||||
if len(s.Palette.Swatches) > 0 {
|
||||
s.Swatch = s.Palette.Swatches[0]
|
||||
s.Palette.ActiveSwatch = s.Swatch.Name
|
||||
}
|
||||
|
||||
// Initialize the user interface. It references the palette and such so it
|
||||
// must be initialized after those things.
|
||||
s.UI = NewEditorUI(d, s)
|
||||
d.Flash("Editor Mode. Press 'P' to play this map.")
|
||||
|
||||
if s.pixelHistory == nil {
|
||||
s.pixelHistory = []level.Pixel{}
|
||||
s.pixelHistory = []*level.Pixel{}
|
||||
}
|
||||
if s.canvas == nil {
|
||||
log.Debug("EditorScene: Setting default canvas to an empty grid")
|
||||
s.canvas = render.Grid{}
|
||||
s.canvas = level.Grid{}
|
||||
}
|
||||
s.width = d.width // TODO: canvas width = copy the window size
|
||||
s.height = d.height
|
||||
|
@ -100,28 +110,31 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
|||
if ev.Button1.Now {
|
||||
// log.Warn("Button1: %+v", ev.Button1)
|
||||
lastPixel := s.lastPixel
|
||||
pixel := level.Pixel{
|
||||
X: ev.CursorX.Now,
|
||||
Y: ev.CursorY.Now,
|
||||
pixel := &level.Pixel{
|
||||
X: ev.CursorX.Now,
|
||||
Y: ev.CursorY.Now,
|
||||
Palette: s.Palette,
|
||||
Swatch: s.Swatch,
|
||||
}
|
||||
|
||||
// Append unique new pixels.
|
||||
if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel {
|
||||
if lastPixel != nil {
|
||||
// Draw the pixels in between.
|
||||
if *lastPixel != pixel {
|
||||
if lastPixel != pixel {
|
||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||
dot := level.Pixel{
|
||||
dot := &level.Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
Palette: lastPixel.Palette,
|
||||
Swatch: lastPixel.Swatch,
|
||||
}
|
||||
s.canvas[dot] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.lastPixel = &pixel
|
||||
s.lastPixel = pixel
|
||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||
|
||||
// Save in the pixel canvas map.
|
||||
|
@ -145,19 +158,20 @@ func (s *EditorScene) Draw(d *Doodle) error {
|
|||
// LoadLevel loads a level from disk.
|
||||
func (s *EditorScene) LoadLevel(filename string) error {
|
||||
s.filename = filename
|
||||
s.pixelHistory = []level.Pixel{}
|
||||
s.canvas = render.Grid{}
|
||||
s.pixelHistory = []*level.Pixel{}
|
||||
s.canvas = level.Grid{}
|
||||
|
||||
m, err := level.LoadJSON(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, point := range m.Pixels {
|
||||
pixel := level.Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
}
|
||||
s.Palette = m.Palette
|
||||
if len(s.Palette.Swatches) > 0 {
|
||||
s.Swatch = m.Palette.Swatches[0]
|
||||
}
|
||||
|
||||
for _, pixel := range m.Pixels {
|
||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||
s.canvas[pixel] = nil
|
||||
}
|
||||
|
@ -168,26 +182,19 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
|||
// SaveLevel saves the level to disk.
|
||||
func (s *EditorScene) SaveLevel(filename string) {
|
||||
s.filename = filename
|
||||
m := level.Level{
|
||||
Version: 1,
|
||||
Title: "Alpha",
|
||||
Author: os.Getenv("USER"),
|
||||
Width: s.width,
|
||||
Height: s.height,
|
||||
Palette: []level.Palette{
|
||||
level.Palette{
|
||||
Color: "#000000",
|
||||
Solid: true,
|
||||
},
|
||||
},
|
||||
Pixels: []level.Pixel{},
|
||||
}
|
||||
|
||||
m := level.New()
|
||||
m.Title = "Alpha"
|
||||
m.Author = os.Getenv("USER")
|
||||
m.Width = s.width
|
||||
m.Height = s.height
|
||||
m.Palette = s.Palette
|
||||
|
||||
for pixel := range s.canvas {
|
||||
m.Pixels = append(m.Pixels, level.Pixel{
|
||||
X: pixel.X,
|
||||
Y: pixel.Y,
|
||||
Palette: 0,
|
||||
m.Pixels = append(m.Pixels, &level.Pixel{
|
||||
X: pixel.X,
|
||||
Y: pixel.Y,
|
||||
PaletteIndex: int32(pixel.Swatch.Index()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
144
editor_ui.go
144
editor_ui.go
|
@ -11,42 +11,110 @@ import (
|
|||
|
||||
// EditorUI manages the user interface for the Editor Scene.
|
||||
type EditorUI struct {
|
||||
d *Doodle
|
||||
d *Doodle
|
||||
Scene *EditorScene
|
||||
|
||||
// Variables
|
||||
StatusMouseText string
|
||||
StatusMouseText string
|
||||
StatusPaletteText string
|
||||
StatusFilenameText string
|
||||
|
||||
// Widgets
|
||||
Supervisor *ui.Supervisor
|
||||
Palette *ui.Window
|
||||
StatusBar *ui.Frame
|
||||
}
|
||||
|
||||
// NewEditorUI initializes the Editor UI.
|
||||
func NewEditorUI(d *Doodle) *EditorUI {
|
||||
func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
||||
u := &EditorUI{
|
||||
d: d,
|
||||
Supervisor: ui.NewSupervisor(),
|
||||
StatusMouseText: ".",
|
||||
d: d,
|
||||
Scene: s,
|
||||
Supervisor: ui.NewSupervisor(),
|
||||
StatusMouseText: "Cursor: (waiting)",
|
||||
StatusPaletteText: "Swatch: <none>",
|
||||
StatusFilenameText: "Filename: <none>",
|
||||
}
|
||||
u.StatusBar = u.SetupStatusBar(d)
|
||||
u.Palette = u.SetupPalette(d)
|
||||
return u
|
||||
}
|
||||
|
||||
// Loop to process events and update the UI.
|
||||
func (u *EditorUI) Loop(ev *events.State) {
|
||||
u.Supervisor.Loop(ev)
|
||||
|
||||
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)",
|
||||
ev.CursorX.Now,
|
||||
ev.CursorY.Now,
|
||||
)
|
||||
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
||||
u.Scene.Swatch,
|
||||
)
|
||||
|
||||
// Statusbar filename label.
|
||||
filename := "untitled.map"
|
||||
if u.Scene.filename != "" {
|
||||
filename = u.Scene.filename
|
||||
}
|
||||
u.StatusFilenameText = fmt.Sprintf("Filename: %s",
|
||||
filename,
|
||||
)
|
||||
|
||||
u.StatusBar.Compute(u.d.Engine)
|
||||
u.Supervisor.Loop(ev)
|
||||
u.Palette.Compute(u.d.Engine)
|
||||
}
|
||||
|
||||
// Present the UI to the screen.
|
||||
func (u *EditorUI) Present(e render.Engine) {
|
||||
u.Palette.Present(e, u.Palette.Point())
|
||||
u.StatusBar.Present(e, u.StatusBar.Point())
|
||||
}
|
||||
|
||||
// SetupPalette sets up the palette panel.
|
||||
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||
window := ui.NewWindow("Palette")
|
||||
window.Configure(ui.Config{
|
||||
Width: 150,
|
||||
Height: u.d.height - u.StatusBar.Size().H,
|
||||
})
|
||||
window.MoveTo(render.NewPoint(
|
||||
u.d.width-window.BoxSize().W,
|
||||
0,
|
||||
))
|
||||
|
||||
// Handler function for the radio buttons being clicked.
|
||||
onClick := func(p render.Point) {
|
||||
name := u.Scene.Palette.ActiveSwatch
|
||||
swatch, ok := u.Scene.Palette.Get(name)
|
||||
if !ok {
|
||||
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
|
||||
return
|
||||
}
|
||||
u.Scene.Swatch = swatch
|
||||
}
|
||||
|
||||
// Draw the radio buttons for the palette.
|
||||
for _, swatch := range u.Scene.Palette.Swatches {
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: swatch.Name,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
label.Font.Color = swatch.Color.Darken(40)
|
||||
|
||||
btn := ui.NewRadioButton("palette", &u.Scene.Palette.ActiveSwatch, swatch.Name, label)
|
||||
btn.Handle("MouseUp", onClick)
|
||||
u.Supervisor.Add(btn)
|
||||
|
||||
window.Pack(btn, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
Fill: true,
|
||||
})
|
||||
}
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
// SetupStatusBar sets up the status bar widget along the bottom of the window.
|
||||
func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
||||
frame := ui.NewFrame("Status Bar")
|
||||
|
@ -57,50 +125,58 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
|||
Width: d.width,
|
||||
})
|
||||
|
||||
cursorLabel := ui.NewLabel(ui.Label{
|
||||
TextVariable: &u.StatusMouseText,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
cursorLabel.Configure(ui.Config{
|
||||
style := ui.Config{
|
||||
Background: render.Grey,
|
||||
BorderStyle: ui.BorderSunken,
|
||||
BorderColor: render.Grey,
|
||||
BorderSize: 1,
|
||||
}
|
||||
|
||||
cursorLabel := ui.NewLabel(ui.Label{
|
||||
TextVariable: &u.StatusMouseText,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
cursorLabel.Configure(style)
|
||||
cursorLabel.Compute(d.Engine)
|
||||
frame.Pack(cursorLabel, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
})
|
||||
|
||||
paletteLabel := ui.NewLabel(ui.Label{
|
||||
TextVariable: &u.StatusPaletteText,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
paletteLabel.Configure(style)
|
||||
paletteLabel.Compute(d.Engine)
|
||||
frame.Pack(paletteLabel, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
})
|
||||
|
||||
filenameLabel := ui.NewLabel(ui.Label{
|
||||
Text: "Filename: untitled.map",
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
filenameLabel.Configure(ui.Config{
|
||||
Background: render.Grey,
|
||||
BorderStyle: ui.BorderSunken,
|
||||
BorderColor: render.Grey,
|
||||
BorderSize: 1,
|
||||
TextVariable: &u.StatusFilenameText,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
filenameLabel.Configure(style)
|
||||
filenameLabel.Compute(d.Engine)
|
||||
frame.Pack(filenameLabel, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
})
|
||||
|
||||
extraLabel := ui.NewLabel(ui.Label{
|
||||
Text: "blah",
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
extraLabel.Configure(ui.Config{
|
||||
Background: render.Grey,
|
||||
BorderStyle: ui.BorderSunken,
|
||||
BorderColor: render.Grey,
|
||||
BorderSize: 1,
|
||||
})
|
||||
extraLabel.Compute(d.Engine)
|
||||
frame.Pack(extraLabel, ui.Pack{
|
||||
Anchor: ui.E,
|
||||
})
|
||||
// TODO: right-aligned labels clip out of bounds
|
||||
// extraLabel := ui.NewLabel(ui.Label{
|
||||
// Text: "blah",
|
||||
// Font: balance.StatusFont,
|
||||
// })
|
||||
// extraLabel.Configure(ui.Config{
|
||||
// Background: render.Grey,
|
||||
// BorderStyle: ui.BorderSunken,
|
||||
// BorderColor: render.Grey,
|
||||
// BorderSize: 1,
|
||||
// })
|
||||
// extraLabel.Compute(d.Engine)
|
||||
// frame.Pack(extraLabel, ui.Pack{
|
||||
// Anchor: ui.E,
|
||||
// })
|
||||
|
||||
frame.Resize(render.Rect{
|
||||
W: d.width,
|
||||
|
|
2
fps.go
2
fps.go
|
@ -13,7 +13,7 @@ const maxSamples = 100
|
|||
// Debug mode options, these can be enabled in the dev console
|
||||
// like: boolProp DebugOverlay true
|
||||
var (
|
||||
DebugOverlay = true
|
||||
DebugOverlay = false
|
||||
DebugCollision = true
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package render
|
||||
package level
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/level"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
// Grid is a 2D grid of pixels in X,Y notation.
|
||||
type Grid map[level.Pixel]interface{}
|
||||
type Grid map[*Pixel]interface{}
|
||||
|
||||
// Exists returns true if the point exists on the grid.
|
||||
func (g *Grid) Exists(p level.Pixel) bool {
|
||||
func (g *Grid) Exists(p *Pixel) bool {
|
||||
if _, ok := (*g)[p]; ok {
|
||||
return true
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ func (g *Grid) Exists(p level.Pixel) bool {
|
|||
}
|
||||
|
||||
// Draw the grid efficiently.
|
||||
func (g *Grid) Draw(e Engine) {
|
||||
func (g *Grid) Draw(e render.Engine) {
|
||||
for pixel := range *g {
|
||||
e.DrawPoint(Black, Point{
|
||||
color := pixel.Swatch.Color
|
||||
e.DrawPoint(color, render.Point{
|
||||
X: pixel.X,
|
||||
Y: pixel.Y,
|
||||
})
|
|
@ -3,6 +3,7 @@ package level
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
@ -16,15 +17,30 @@ func (m *Level) ToJSON() ([]byte, error) {
|
|||
}
|
||||
|
||||
// LoadJSON loads a map from JSON file.
|
||||
func LoadJSON(filename string) (Level, error) {
|
||||
func LoadJSON(filename string) (*Level, error) {
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return Level{}, err
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
m := Level{}
|
||||
m := New()
|
||||
decoder := json.NewDecoder(fh)
|
||||
err = decoder.Decode(&m)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
// Inflate the private instance values.
|
||||
for _, px := range m.Pixels {
|
||||
if int(px.PaletteIndex) > len(m.Palette.Swatches) {
|
||||
return nil, fmt.Errorf(
|
||||
"pixel %s references palette index %d but there are only %d swatches in the palette",
|
||||
px, px.PaletteIndex, len(m.Palette.Swatches),
|
||||
)
|
||||
}
|
||||
px.Palette = m.Palette
|
||||
px.Swatch = m.Palette.Swatches[px.PaletteIndex]
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
|
90
level/palette.go
Normal file
90
level/palette.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package level
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
// DefaultPalette returns a sensible default palette.
|
||||
func DefaultPalette() *Palette {
|
||||
return &Palette{
|
||||
Swatches: []*Swatch{
|
||||
&Swatch{
|
||||
Name: "solid",
|
||||
Color: render.Black,
|
||||
Solid: true,
|
||||
},
|
||||
&Swatch{
|
||||
Name: "decoration",
|
||||
Color: render.Grey,
|
||||
},
|
||||
&Swatch{
|
||||
Name: "fire",
|
||||
Color: render.Red,
|
||||
Fire: true,
|
||||
},
|
||||
&Swatch{
|
||||
Name: "water",
|
||||
Color: render.Blue,
|
||||
Water: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Palette holds an index of colors used in a drawing.
|
||||
type Palette struct {
|
||||
Swatches []*Swatch `json:"swatches"`
|
||||
|
||||
// Private runtime values
|
||||
ActiveSwatch string `json:"-"` // name of the actively selected color
|
||||
byName map[string]int // Cache map of swatches by name
|
||||
}
|
||||
|
||||
// Swatch holds details about a single value in the palette.
|
||||
type Swatch struct {
|
||||
Name string `json:"name"`
|
||||
Color render.Color `json:"color"`
|
||||
|
||||
// Optional attributes.
|
||||
Solid bool `json:"solid,omitempty"`
|
||||
Fire bool `json:"fire,omitempty"`
|
||||
Water bool `json:"water,omitempty"`
|
||||
|
||||
// Private runtime attributes.
|
||||
index int // position in the Palette, for reverse of `Palette.byName`
|
||||
}
|
||||
|
||||
func (s Swatch) String() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
// Index returns the Swatch's position in the palette.
|
||||
func (s Swatch) Index() int {
|
||||
return s.index
|
||||
}
|
||||
|
||||
// Get a swatch by name.
|
||||
func (p *Palette) Get(name string) (result *Swatch, exists bool) {
|
||||
p.update()
|
||||
|
||||
if index, ok := p.byName[name]; ok && index < len(p.Swatches) {
|
||||
result = p.Swatches[index]
|
||||
exists = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update the internal caches and such.
|
||||
func (p *Palette) update() {
|
||||
// Initialize the name cache if nil or if the size disagrees with the
|
||||
// length of the swatches available.
|
||||
if p.byName == nil || len(p.byName) != len(p.Swatches) {
|
||||
// Initialize the name cache.
|
||||
p.byName = map[string]int{}
|
||||
for i, swatch := range p.Swatches {
|
||||
swatch.index = i
|
||||
p.byName[swatch.Name] = i
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,25 +20,42 @@ type Level struct {
|
|||
|
||||
// The Palette holds the unique "colors" used in this map file, and their
|
||||
// properties (solid, fire, slippery, etc.)
|
||||
Palette []Palette `json:"palette"`
|
||||
Palette *Palette `json:"palette"`
|
||||
|
||||
// Pixels is a 2D array indexed by [X][Y]. The cell values are indexes into
|
||||
// the Palette.
|
||||
Pixels []Pixel `json:"pixels"`
|
||||
Pixels []*Pixel `json:"pixels"`
|
||||
}
|
||||
|
||||
// New creates a blank level object with all its members initialized.
|
||||
func New() *Level {
|
||||
return &Level{
|
||||
Version: 1,
|
||||
Pixels: []*Pixel{},
|
||||
Palette: &Palette{},
|
||||
}
|
||||
}
|
||||
|
||||
// Pixel associates a coordinate with a palette index.
|
||||
type Pixel struct {
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
Palette int32 `json:"p"`
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
PaletteIndex int32 `json:"p"`
|
||||
|
||||
// Private runtime values.
|
||||
Palette *Palette `json:"-"` // pointer to its palette, TODO: needed?
|
||||
Swatch *Swatch `json:"-"` // pointer to its swatch, for when rendered.
|
||||
}
|
||||
|
||||
func (p Pixel) String() string {
|
||||
return fmt.Sprintf("Pixel<%s '%s' (%d,%d)>", p.Swatch.Color, p.Swatch.Name, p.X, p.Y)
|
||||
}
|
||||
|
||||
// MarshalJSON serializes a Pixel compactly as a simple list.
|
||||
func (p Pixel) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(
|
||||
`[%d, %d, %d]`,
|
||||
p.X, p.Y, p.Palette,
|
||||
p.X, p.Y, p.PaletteIndex,
|
||||
)), nil
|
||||
}
|
||||
|
||||
|
@ -52,17 +69,8 @@ func (p *Pixel) UnmarshalJSON(text []byte) error {
|
|||
|
||||
p.X = triplet[0]
|
||||
p.Y = triplet[1]
|
||||
p.Palette = triplet[2]
|
||||
if len(triplet) > 2 {
|
||||
p.PaletteIndex = triplet[2]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Palette are the unique pixel attributes that this map uses, and serves
|
||||
// as a lookup table for the Pixels.
|
||||
type Palette struct {
|
||||
// Required attributes.
|
||||
Color string `json:"color"`
|
||||
|
||||
// Optional attributes.
|
||||
Solid bool `json:"solid,omitempty"`
|
||||
Fire bool `json:"fire,omitempty"`
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ import (
|
|||
type PlayScene struct {
|
||||
// Configuration attributes.
|
||||
Filename string
|
||||
Canvas render.Grid
|
||||
Canvas level.Grid
|
||||
|
||||
// Private variables.
|
||||
canvas render.Grid
|
||||
canvas level.Grid
|
||||
|
||||
// Canvas size
|
||||
width int32
|
||||
|
@ -46,7 +46,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
|
||||
if s.canvas == nil {
|
||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||
s.canvas = render.Grid{}
|
||||
s.canvas = level.Grid{}
|
||||
}
|
||||
|
||||
s.width = d.width // TODO: canvas width = copy the window size
|
||||
|
@ -128,18 +128,14 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
|||
|
||||
// LoadLevel loads a level from disk.
|
||||
func (s *PlayScene) LoadLevel(filename string) error {
|
||||
s.canvas = render.Grid{}
|
||||
s.canvas = level.Grid{}
|
||||
|
||||
m, err := level.LoadJSON(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, point := range m.Pixels {
|
||||
pixel := level.Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
}
|
||||
for _, pixel := range m.Pixels {
|
||||
s.canvas[pixel] = nil
|
||||
}
|
||||
|
||||
|
|
150
render/color.go
Normal file
150
render/color.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
// Regexps to parse hex color codes. Three formats are supported:
|
||||
// * reHexColor3 uses only 3 hex characters, like #F90
|
||||
// * reHexColor6 uses standard 6 characters, like #FF9900
|
||||
// * reHexColor8 is the standard 6 plus alpha channel, like #FF9900FF
|
||||
reHexColor3 = regexp.MustCompile(`^([A-Fa-f0-9])([A-Fa-f0-9])([A-Fa-f0-9])$`)
|
||||
reHexColor6 = regexp.MustCompile(`^([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})$`)
|
||||
reHexColor8 = regexp.MustCompile(`^([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})$`)
|
||||
)
|
||||
|
||||
// Color holds an RGBA color value.
|
||||
type Color struct {
|
||||
Red uint8
|
||||
Green uint8
|
||||
Blue uint8
|
||||
Alpha uint8
|
||||
}
|
||||
|
||||
// RGBA creates a new Color.
|
||||
func RGBA(r, g, b, a uint8) Color {
|
||||
return Color{
|
||||
Red: r,
|
||||
Green: g,
|
||||
Blue: b,
|
||||
Alpha: a,
|
||||
}
|
||||
}
|
||||
|
||||
// HexColor parses a color from hexadecimal code.
|
||||
func HexColor(hex string) (Color, error) {
|
||||
c := Black // default color
|
||||
|
||||
if len(hex) > 0 && hex[0] == '#' {
|
||||
hex = hex[1:]
|
||||
}
|
||||
|
||||
var m []string
|
||||
if len(hex) == 3 {
|
||||
m = reHexColor3.FindStringSubmatch(hex)
|
||||
} else if len(hex) == 6 {
|
||||
m = reHexColor6.FindStringSubmatch(hex)
|
||||
} else if len(hex) == 8 {
|
||||
m = reHexColor8.FindStringSubmatch(hex)
|
||||
} else {
|
||||
return c, errors.New("not a valid length for color code; only 3, 6 and 8 supported")
|
||||
}
|
||||
|
||||
// Any luck?
|
||||
if m == nil {
|
||||
return c, errors.New("not a valid hex color code")
|
||||
}
|
||||
|
||||
// Parse the color values. 16=base, 8=bit size
|
||||
red, _ := strconv.ParseUint(m[1], 16, 8)
|
||||
green, _ := strconv.ParseUint(m[2], 16, 8)
|
||||
blue, _ := strconv.ParseUint(m[3], 16, 8)
|
||||
|
||||
// Alpha channel available?
|
||||
var alpha uint64 = 255
|
||||
if len(m) == 5 {
|
||||
alpha, _ = strconv.ParseUint(m[4], 16, 8)
|
||||
}
|
||||
|
||||
c.Red = uint8(red)
|
||||
c.Green = uint8(green)
|
||||
c.Blue = uint8(blue)
|
||||
c.Alpha = uint8(alpha)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c Color) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Color<#%02x%02x%02x>",
|
||||
c.Red, c.Green, c.Blue,
|
||||
)
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the Color for JSON.
|
||||
func (c Color) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(
|
||||
`"#%02x%02x%02x"`,
|
||||
c.Red, c.Green, c.Blue,
|
||||
)), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON reloads the Color from JSON.
|
||||
func (c *Color) UnmarshalJSON(b []byte) error {
|
||||
var hex string
|
||||
err := json.Unmarshal(b, &hex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := HexColor(hex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Red = parsed.Red
|
||||
c.Blue = parsed.Blue
|
||||
c.Green = parsed.Green
|
||||
c.Alpha = parsed.Alpha
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a relative color value to the color.
|
||||
func (c Color) Add(r, g, b, a int32) Color {
|
||||
var (
|
||||
R = int32(c.Red) + r
|
||||
G = int32(c.Green) + g
|
||||
B = int32(c.Blue) + b
|
||||
A = int32(c.Alpha) + a
|
||||
)
|
||||
|
||||
cap8 := func(v int32) uint8 {
|
||||
if v > 255 {
|
||||
v = 255
|
||||
} else if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return uint8(v)
|
||||
}
|
||||
|
||||
return Color{
|
||||
Red: cap8(R),
|
||||
Green: cap8(G),
|
||||
Blue: cap8(B),
|
||||
Alpha: cap8(A),
|
||||
}
|
||||
}
|
||||
|
||||
// Lighten a color value.
|
||||
func (c Color) Lighten(v int32) Color {
|
||||
return c.Add(v, v, v, 0)
|
||||
}
|
||||
|
||||
// Darken a color value.
|
||||
func (c Color) Darken(v int32) Color {
|
||||
return c.Add(-v, -v, -v, 0)
|
||||
}
|
|
@ -38,67 +38,6 @@ type Engine interface {
|
|||
Loop() error // maybe?
|
||||
}
|
||||
|
||||
// Color holds an RGBA color value.
|
||||
type Color struct {
|
||||
Red uint8
|
||||
Green uint8
|
||||
Blue uint8
|
||||
Alpha uint8
|
||||
}
|
||||
|
||||
// RGBA creates a new Color.
|
||||
func RGBA(r, g, b, a uint8) Color {
|
||||
return Color{
|
||||
Red: r,
|
||||
Green: g,
|
||||
Blue: b,
|
||||
Alpha: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Color<#%02x%02x%02x>",
|
||||
c.Red, c.Green, c.Blue,
|
||||
)
|
||||
}
|
||||
|
||||
// Add a relative color value to the color.
|
||||
func (c Color) Add(r, g, b, a int32) Color {
|
||||
var (
|
||||
R = int32(c.Red) + r
|
||||
G = int32(c.Green) + g
|
||||
B = int32(c.Blue) + b
|
||||
A = int32(c.Alpha) + a
|
||||
)
|
||||
|
||||
cap8 := func(v int32) uint8 {
|
||||
if v > 255 {
|
||||
v = 255
|
||||
} else if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return uint8(v)
|
||||
}
|
||||
|
||||
return Color{
|
||||
Red: cap8(R),
|
||||
Green: cap8(G),
|
||||
Blue: cap8(B),
|
||||
Alpha: cap8(A),
|
||||
}
|
||||
}
|
||||
|
||||
// Lighten a color value.
|
||||
func (c Color) Lighten(v int32) Color {
|
||||
return c.Add(v, v, v, 0)
|
||||
}
|
||||
|
||||
// Darken a color value.
|
||||
func (c Color) Darken(v int32) Color {
|
||||
return c.Add(-v, -v, -v, 0)
|
||||
}
|
||||
|
||||
// Point holds an X,Y coordinate value.
|
||||
type Point struct {
|
||||
X int32
|
||||
|
@ -165,7 +104,12 @@ type Text struct {
|
|||
}
|
||||
|
||||
func (t Text) String() string {
|
||||
return fmt.Sprintf("Text<%s>", t.Text)
|
||||
return fmt.Sprintf(`Text<"%s" %dpx %s>`, t.Text, t.Size, t.Color)
|
||||
}
|
||||
|
||||
// IsZero returns if the Text is the zero value.
|
||||
func (t Text) IsZero() bool {
|
||||
return t.Text == "" && t.Size == 0 && t.Color == Invisible && t.Padding == 0 && t.Stroke == Invisible && t.Shadow == Invisible
|
||||
}
|
||||
|
||||
// Common color names.
|
||||
|
|
|
@ -17,7 +17,7 @@ func (r *Renderer) Clear(color render.Color) {
|
|||
// DrawPoint puts a color at a pixel.
|
||||
func (r *Renderer) DrawPoint(color render.Color, point render.Point) {
|
||||
if color != r.lastColor {
|
||||
r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha)
|
||||
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||
}
|
||||
r.renderer.DrawPoint(point.X, point.Y)
|
||||
}
|
||||
|
|
|
@ -2,16 +2,20 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"git.kirsle.net/apps/doodle/ui/theme"
|
||||
)
|
||||
|
||||
// CheckButton is a button that is bound to a boolean variable and stays clicked
|
||||
// once pressed, until clicked again to release.
|
||||
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
||||
// Button and holds a boolean or string pointer (boolean for checkbox,
|
||||
// string for radio).
|
||||
type CheckButton struct {
|
||||
Button
|
||||
BoolVar *bool
|
||||
BoolVar *bool
|
||||
StringVar *string
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewCheckButton creates a new CheckButton.
|
||||
|
@ -24,6 +28,41 @@ func NewCheckButton(name string, boolVar *bool, child Widget) *CheckButton {
|
|||
return fmt.Sprintf("CheckButton<%s %+v>", name, w.BoolVar)
|
||||
})
|
||||
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// NewRadioButton creates a CheckButton bound to a string variable.
|
||||
func NewRadioButton(name string, stringVar *string, value string, child Widget) *CheckButton {
|
||||
w := &CheckButton{
|
||||
StringVar: stringVar,
|
||||
Value: value,
|
||||
}
|
||||
w.Button.child = child
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf(`RadioButton<%s "%s" %s>`, name, w.Value, strconv.FormatBool(*w.StringVar == w.Value))
|
||||
})
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// Compute to re-evaluate the button state (in the case of radio buttons where
|
||||
// a different button will affect the state of this one when clicked).
|
||||
func (w *CheckButton) Compute(e render.Engine) {
|
||||
if w.StringVar != nil {
|
||||
// Radio button, always re-assign the border style in case a sister
|
||||
// radio button has changed the value.
|
||||
if *w.StringVar == w.Value {
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
} else {
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
}
|
||||
}
|
||||
w.Button.Compute(e)
|
||||
}
|
||||
|
||||
// setup the common things between checkboxes and radioboxes.
|
||||
func (w *CheckButton) setup() {
|
||||
var borderStyle BorderStyle = BorderRaised
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar == true {
|
||||
|
@ -57,16 +96,23 @@ func NewCheckButton(name string, boolVar *bool, child Widget) *CheckButton {
|
|||
})
|
||||
|
||||
w.Handle("MouseDown", func(p render.Point) {
|
||||
var sunken bool
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar {
|
||||
*w.BoolVar = false
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
} else {
|
||||
*w.BoolVar = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
sunken = true
|
||||
}
|
||||
} else if w.StringVar != nil {
|
||||
*w.StringVar = w.Value
|
||||
sunken = true
|
||||
}
|
||||
|
||||
if sunken {
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
} else {
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
}
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
|
|
@ -11,12 +11,26 @@ type Checkbox struct {
|
|||
|
||||
// NewCheckbox creates a new Checkbox.
|
||||
func NewCheckbox(name string, boolVar *bool, child Widget) *Checkbox {
|
||||
return makeCheckbox(name, boolVar, nil, "", child)
|
||||
}
|
||||
|
||||
// NewRadiobox creates a new Checkbox in radio mode.
|
||||
func NewRadiobox(name string, stringVar *string, value string, child Widget) *Checkbox {
|
||||
return makeCheckbox(name, nil, stringVar, value, child)
|
||||
}
|
||||
|
||||
// makeCheckbox constructs an appropriate type of checkbox.
|
||||
func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, child Widget) *Checkbox {
|
||||
// Our custom checkbutton widget.
|
||||
mark := NewFrame(name + "_mark")
|
||||
|
||||
w := &Checkbox{
|
||||
button: NewCheckButton(name+"_button", boolVar, mark),
|
||||
child: child,
|
||||
child: child,
|
||||
}
|
||||
if boolVar != nil {
|
||||
w.button = NewCheckButton(name+"_button", boolVar, mark)
|
||||
} else if stringVar != nil {
|
||||
w.button = NewRadioButton(name+"_button", stringVar, value, mark)
|
||||
}
|
||||
w.Frame.Setup()
|
||||
|
||||
|
@ -39,6 +53,11 @@ func NewCheckbox(name string, boolVar *bool, child Widget) *Checkbox {
|
|||
return w
|
||||
}
|
||||
|
||||
// Child returns the child widget.
|
||||
func (w *Checkbox) Child() Widget {
|
||||
return w.child
|
||||
}
|
||||
|
||||
// Supervise the checkbutton inside the widget.
|
||||
func (w *Checkbox) Supervise(s *Supervisor) {
|
||||
s.Add(w.button)
|
||||
|
|
25
ui/label.go
25
ui/label.go
|
@ -6,6 +6,12 @@ import (
|
|||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
// DefaultFont is the default font settings used for a Label.
|
||||
var DefaultFont = render.Text{
|
||||
Size: 12,
|
||||
Color: render.Black,
|
||||
}
|
||||
|
||||
// Label is a simple text label widget.
|
||||
type Label struct {
|
||||
BaseWidget
|
||||
|
@ -24,10 +30,13 @@ func NewLabel(c Label) *Label {
|
|||
w := &Label{
|
||||
Text: c.Text,
|
||||
TextVariable: c.TextVariable,
|
||||
Font: c.Font,
|
||||
Font: DefaultFont,
|
||||
}
|
||||
if !c.Font.IsZero() {
|
||||
w.Font = c.Font
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Label<%s>", w.text().Text)
|
||||
return fmt.Sprintf(`Label<"%s">`, w.text().Text)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
@ -43,9 +52,19 @@ func (w *Label) text() render.Text {
|
|||
return w.Font
|
||||
}
|
||||
|
||||
// Value returns the current text value displayed in the widget, whether it was
|
||||
// the hardcoded value or a TextVariable.
|
||||
func (w *Label) Value() string {
|
||||
return w.text().Text
|
||||
}
|
||||
|
||||
// Compute the size of the label widget.
|
||||
func (w *Label) Compute(e render.Engine) {
|
||||
rect, _ := e.ComputeTextRect(w.text())
|
||||
rect, err := e.ComputeTextRect(w.text())
|
||||
if err != nil {
|
||||
log.Error("%s: failed to compute text rect: %s", w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !w.FixedSize() {
|
||||
w.resizeAuto(render.Rect{
|
||||
|
|
102
ui/window.go
Normal file
102
ui/window.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
// Window is a frame with a title bar.
|
||||
type Window struct {
|
||||
BaseWidget
|
||||
Title string
|
||||
Active bool
|
||||
|
||||
// Private widgets.
|
||||
body *Frame
|
||||
titleBar *Label
|
||||
content *Frame
|
||||
}
|
||||
|
||||
// NewWindow creates a new window.
|
||||
func NewWindow(title string) *Window {
|
||||
w := &Window{
|
||||
Title: title,
|
||||
body: NewFrame("body:" + title),
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Window<%s>",
|
||||
w.Title,
|
||||
)
|
||||
})
|
||||
|
||||
w.body.Configure(Config{
|
||||
Background: render.Grey,
|
||||
BorderSize: 2,
|
||||
BorderStyle: BorderRaised,
|
||||
})
|
||||
|
||||
// Title bar widget.
|
||||
titleBar := NewLabel(Label{
|
||||
TextVariable: &w.Title,
|
||||
Font: render.Text{
|
||||
Color: render.White,
|
||||
Size: 10,
|
||||
Stroke: render.DarkBlue,
|
||||
Padding: 2,
|
||||
},
|
||||
})
|
||||
titleBar.Configure(Config{
|
||||
Background: render.Blue,
|
||||
})
|
||||
w.body.Pack(titleBar, Pack{
|
||||
Anchor: N,
|
||||
Fill: true,
|
||||
})
|
||||
w.titleBar = titleBar
|
||||
|
||||
// Window content frame.
|
||||
content := NewFrame("content:" + title)
|
||||
content.Configure(Config{
|
||||
Background: render.Grey,
|
||||
})
|
||||
w.body.Pack(content, Pack{
|
||||
Anchor: N,
|
||||
Fill: true,
|
||||
})
|
||||
w.content = content
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// TitleBar returns the title bar widget.
|
||||
func (w *Window) TitleBar() *Label {
|
||||
return w.titleBar
|
||||
}
|
||||
|
||||
// Configure the widget. Color and style changes are passed down to the inner
|
||||
// content frame of the window.
|
||||
func (w *Window) Configure(C Config) {
|
||||
w.BaseWidget.Configure(C)
|
||||
w.body.Configure(C)
|
||||
}
|
||||
|
||||
// ConfigureTitle configures the title bar widget.
|
||||
func (w *Window) ConfigureTitle(C Config) {
|
||||
w.titleBar.Configure(C)
|
||||
}
|
||||
|
||||
// Compute the window.
|
||||
func (w *Window) Compute(e render.Engine) {
|
||||
w.body.Compute(e)
|
||||
}
|
||||
|
||||
// Present the window.
|
||||
func (w *Window) Present(e render.Engine, P render.Point) {
|
||||
w.body.Present(e, P)
|
||||
}
|
||||
|
||||
// Pack a widget into the window's frame.
|
||||
func (w *Window) Pack(child Widget, config ...Pack) {
|
||||
w.content.Pack(child, config...)
|
||||
}
|
Loading…
Reference in New Issue
Block a user