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/
|
fonts/
|
||||||
|
maps/
|
||||||
|
bin/
|
||||||
screenshot-*.png
|
screenshot-*.png
|
||||||
map-*.json
|
map-*.json
|
||||||
|
|
|
@ -40,6 +40,7 @@ func main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
app := doodle.New(debug, engine)
|
app := doodle.New(debug, engine)
|
||||||
|
app.SetupEngine()
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
if edit {
|
if edit {
|
||||||
app.EditLevel(filename)
|
app.EditLevel(filename)
|
||||||
|
|
|
@ -68,7 +68,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// CollidesWithGrid checks if a Doodad collides with level geometry.
|
// 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 (
|
var (
|
||||||
P = d.Position()
|
P = d.Position()
|
||||||
S = d.Size()
|
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
|
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and
|
||||||
// returns if any of them intersect with level geometry.
|
// 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)
|
col := GetCollisionBox(box)
|
||||||
|
|
||||||
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
|
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
|
// 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
|
// for any pixels to be set, implying a collision between level geometry and the
|
||||||
// bounding boxes of the doodad.
|
// 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) {
|
for point := range render.IterLine2(p1, p2) {
|
||||||
if grid.Exists(level.Pixel{
|
if grid.Exists(&level.Pixel{
|
||||||
X: point.X,
|
X: point.X,
|
||||||
Y: point.Y,
|
Y: point.Y,
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (p *Player) SetGrounded(v bool) {
|
||||||
|
|
||||||
// Draw the player sprite.
|
// Draw the player sprite.
|
||||||
func (p *Player) Draw(e render.Engine) {
|
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,
|
X: p.point.X,
|
||||||
Y: p.point.Y,
|
Y: p.point.Y,
|
||||||
W: p.size.W,
|
W: p.size.W,
|
||||||
|
|
28
doodle.go
28
doodle.go
|
@ -20,8 +20,9 @@ const (
|
||||||
|
|
||||||
// Doodle is the game object.
|
// Doodle is the game object.
|
||||||
type Doodle struct {
|
type Doodle struct {
|
||||||
Debug bool
|
Debug bool
|
||||||
Engine render.Engine
|
Engine render.Engine
|
||||||
|
engineReady bool
|
||||||
|
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
running bool
|
running bool
|
||||||
|
@ -54,12 +55,22 @@ func New(debug bool, engine render.Engine) *Doodle {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run initializes SDL and starts the main loop.
|
// SetupEngine sets up the rendering engine.
|
||||||
func (d *Doodle) Run() error {
|
func (d *Doodle) SetupEngine() error {
|
||||||
// Set up the render engine.
|
|
||||||
if err := d.Engine.Setup(); err != nil {
|
if err := d.Engine.Setup(); err != nil {
|
||||||
return err
|
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.
|
// Set up the default scene.
|
||||||
if d.Scene == nil {
|
if d.Scene == nil {
|
||||||
|
@ -156,10 +167,9 @@ func (d *Doodle) NewMap() {
|
||||||
// EditLevel loads a map from JSON into the EditorScene.
|
// EditLevel loads a map from JSON into the EditorScene.
|
||||||
func (d *Doodle) EditLevel(filename string) error {
|
func (d *Doodle) EditLevel(filename string) error {
|
||||||
log.Info("Loading level from file: %s", filename)
|
log.Info("Loading level from file: %s", filename)
|
||||||
scene := &EditorScene{}
|
scene := &EditorScene{
|
||||||
err := scene.LoadLevel(filename)
|
Filename: filename,
|
||||||
if err != nil {
|
OpenFile: true,
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
d.Goto(scene)
|
d.Goto(scene)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -18,14 +18,17 @@ type EditorScene struct {
|
||||||
// Configuration for the scene initializer.
|
// Configuration for the scene initializer.
|
||||||
OpenFile bool
|
OpenFile bool
|
||||||
Filename string
|
Filename string
|
||||||
Canvas render.Grid
|
Canvas level.Grid
|
||||||
|
|
||||||
UI *EditorUI
|
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.
|
// 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
|
lastPixel *level.Pixel // last pixel placed while mouse down and dragging
|
||||||
canvas render.Grid
|
canvas level.Grid
|
||||||
filename string // Last saved filename.
|
filename string // Last saved filename.
|
||||||
|
|
||||||
// Canvas size
|
// Canvas size
|
||||||
|
@ -48,7 +51,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
if s.OpenFile {
|
if s.OpenFile {
|
||||||
log.Debug("EditorScene: Loading map from filename at %s", s.filename)
|
log.Debug("EditorScene: Loading map from filename at %s", s.filename)
|
||||||
if err := s.LoadLevel(s.filename); err != nil {
|
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.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.")
|
d.Flash("Editor Mode. Press 'P' to play this map.")
|
||||||
|
|
||||||
if s.pixelHistory == nil {
|
if s.pixelHistory == nil {
|
||||||
s.pixelHistory = []level.Pixel{}
|
s.pixelHistory = []*level.Pixel{}
|
||||||
}
|
}
|
||||||
if s.canvas == nil {
|
if s.canvas == nil {
|
||||||
log.Debug("EditorScene: Setting default canvas to an empty grid")
|
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.width = d.width // TODO: canvas width = copy the window size
|
||||||
s.height = d.height
|
s.height = d.height
|
||||||
|
@ -100,28 +110,31 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
if ev.Button1.Now {
|
if ev.Button1.Now {
|
||||||
// log.Warn("Button1: %+v", ev.Button1)
|
// log.Warn("Button1: %+v", ev.Button1)
|
||||||
lastPixel := s.lastPixel
|
lastPixel := s.lastPixel
|
||||||
pixel := level.Pixel{
|
pixel := &level.Pixel{
|
||||||
X: ev.CursorX.Now,
|
X: ev.CursorX.Now,
|
||||||
Y: ev.CursorY.Now,
|
Y: ev.CursorY.Now,
|
||||||
|
Palette: s.Palette,
|
||||||
|
Swatch: s.Swatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append unique new pixels.
|
// Append unique new pixels.
|
||||||
if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel {
|
if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel {
|
||||||
if lastPixel != nil {
|
if lastPixel != nil {
|
||||||
// Draw the pixels in between.
|
// Draw the pixels in between.
|
||||||
if *lastPixel != pixel {
|
if lastPixel != pixel {
|
||||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||||
dot := level.Pixel{
|
dot := &level.Pixel{
|
||||||
X: point.X,
|
X: point.X,
|
||||||
Y: point.Y,
|
Y: point.Y,
|
||||||
Palette: lastPixel.Palette,
|
Palette: lastPixel.Palette,
|
||||||
|
Swatch: lastPixel.Swatch,
|
||||||
}
|
}
|
||||||
s.canvas[dot] = nil
|
s.canvas[dot] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.lastPixel = &pixel
|
s.lastPixel = pixel
|
||||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||||
|
|
||||||
// Save in the pixel canvas map.
|
// Save in the pixel canvas map.
|
||||||
|
@ -145,19 +158,20 @@ func (s *EditorScene) Draw(d *Doodle) error {
|
||||||
// LoadLevel loads a level from disk.
|
// LoadLevel loads a level from disk.
|
||||||
func (s *EditorScene) LoadLevel(filename string) error {
|
func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
s.pixelHistory = []level.Pixel{}
|
s.pixelHistory = []*level.Pixel{}
|
||||||
s.canvas = render.Grid{}
|
s.canvas = level.Grid{}
|
||||||
|
|
||||||
m, err := level.LoadJSON(filename)
|
m, err := level.LoadJSON(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, point := range m.Pixels {
|
s.Palette = m.Palette
|
||||||
pixel := level.Pixel{
|
if len(s.Palette.Swatches) > 0 {
|
||||||
X: point.X,
|
s.Swatch = m.Palette.Swatches[0]
|
||||||
Y: point.Y,
|
}
|
||||||
}
|
|
||||||
|
for _, pixel := range m.Pixels {
|
||||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||||
s.canvas[pixel] = nil
|
s.canvas[pixel] = nil
|
||||||
}
|
}
|
||||||
|
@ -168,26 +182,19 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
// SaveLevel saves the level to disk.
|
// SaveLevel saves the level to disk.
|
||||||
func (s *EditorScene) SaveLevel(filename string) {
|
func (s *EditorScene) SaveLevel(filename string) {
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
m := level.Level{
|
|
||||||
Version: 1,
|
m := level.New()
|
||||||
Title: "Alpha",
|
m.Title = "Alpha"
|
||||||
Author: os.Getenv("USER"),
|
m.Author = os.Getenv("USER")
|
||||||
Width: s.width,
|
m.Width = s.width
|
||||||
Height: s.height,
|
m.Height = s.height
|
||||||
Palette: []level.Palette{
|
m.Palette = s.Palette
|
||||||
level.Palette{
|
|
||||||
Color: "#000000",
|
|
||||||
Solid: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Pixels: []level.Pixel{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for pixel := range s.canvas {
|
for pixel := range s.canvas {
|
||||||
m.Pixels = append(m.Pixels, level.Pixel{
|
m.Pixels = append(m.Pixels, &level.Pixel{
|
||||||
X: pixel.X,
|
X: pixel.X,
|
||||||
Y: pixel.Y,
|
Y: pixel.Y,
|
||||||
Palette: 0,
|
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.
|
// EditorUI manages the user interface for the Editor Scene.
|
||||||
type EditorUI struct {
|
type EditorUI struct {
|
||||||
d *Doodle
|
d *Doodle
|
||||||
|
Scene *EditorScene
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
StatusMouseText string
|
StatusMouseText string
|
||||||
|
StatusPaletteText string
|
||||||
|
StatusFilenameText string
|
||||||
|
|
||||||
// Widgets
|
// Widgets
|
||||||
Supervisor *ui.Supervisor
|
Supervisor *ui.Supervisor
|
||||||
|
Palette *ui.Window
|
||||||
StatusBar *ui.Frame
|
StatusBar *ui.Frame
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEditorUI initializes the Editor UI.
|
// NewEditorUI initializes the Editor UI.
|
||||||
func NewEditorUI(d *Doodle) *EditorUI {
|
func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
||||||
u := &EditorUI{
|
u := &EditorUI{
|
||||||
d: d,
|
d: d,
|
||||||
Supervisor: ui.NewSupervisor(),
|
Scene: s,
|
||||||
StatusMouseText: ".",
|
Supervisor: ui.NewSupervisor(),
|
||||||
|
StatusMouseText: "Cursor: (waiting)",
|
||||||
|
StatusPaletteText: "Swatch: <none>",
|
||||||
|
StatusFilenameText: "Filename: <none>",
|
||||||
}
|
}
|
||||||
u.StatusBar = u.SetupStatusBar(d)
|
u.StatusBar = u.SetupStatusBar(d)
|
||||||
|
u.Palette = u.SetupPalette(d)
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop to process events and update the UI.
|
// Loop to process events and update the UI.
|
||||||
func (u *EditorUI) Loop(ev *events.State) {
|
func (u *EditorUI) Loop(ev *events.State) {
|
||||||
|
u.Supervisor.Loop(ev)
|
||||||
|
|
||||||
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)",
|
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)",
|
||||||
ev.CursorX.Now,
|
ev.CursorX.Now,
|
||||||
ev.CursorY.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.StatusBar.Compute(u.d.Engine)
|
||||||
u.Supervisor.Loop(ev)
|
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) {
|
||||||
|
u.Palette.Present(e, u.Palette.Point())
|
||||||
u.StatusBar.Present(e, u.StatusBar.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.
|
// SetupStatusBar sets up the status bar widget along the bottom of the window.
|
||||||
func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
||||||
frame := ui.NewFrame("Status Bar")
|
frame := ui.NewFrame("Status Bar")
|
||||||
|
@ -57,50 +125,58 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
||||||
Width: d.width,
|
Width: d.width,
|
||||||
})
|
})
|
||||||
|
|
||||||
cursorLabel := ui.NewLabel(ui.Label{
|
style := ui.Config{
|
||||||
TextVariable: &u.StatusMouseText,
|
|
||||||
Font: balance.StatusFont,
|
|
||||||
})
|
|
||||||
cursorLabel.Configure(ui.Config{
|
|
||||||
Background: render.Grey,
|
Background: render.Grey,
|
||||||
BorderStyle: ui.BorderSunken,
|
BorderStyle: ui.BorderSunken,
|
||||||
BorderColor: render.Grey,
|
BorderColor: render.Grey,
|
||||||
BorderSize: 1,
|
BorderSize: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorLabel := ui.NewLabel(ui.Label{
|
||||||
|
TextVariable: &u.StatusMouseText,
|
||||||
|
Font: balance.StatusFont,
|
||||||
})
|
})
|
||||||
|
cursorLabel.Configure(style)
|
||||||
cursorLabel.Compute(d.Engine)
|
cursorLabel.Compute(d.Engine)
|
||||||
frame.Pack(cursorLabel, ui.Pack{
|
frame.Pack(cursorLabel, ui.Pack{
|
||||||
Anchor: ui.W,
|
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{
|
filenameLabel := ui.NewLabel(ui.Label{
|
||||||
Text: "Filename: untitled.map",
|
TextVariable: &u.StatusFilenameText,
|
||||||
Font: balance.StatusFont,
|
Font: balance.StatusFont,
|
||||||
})
|
|
||||||
filenameLabel.Configure(ui.Config{
|
|
||||||
Background: render.Grey,
|
|
||||||
BorderStyle: ui.BorderSunken,
|
|
||||||
BorderColor: render.Grey,
|
|
||||||
BorderSize: 1,
|
|
||||||
})
|
})
|
||||||
|
filenameLabel.Configure(style)
|
||||||
filenameLabel.Compute(d.Engine)
|
filenameLabel.Compute(d.Engine)
|
||||||
frame.Pack(filenameLabel, ui.Pack{
|
frame.Pack(filenameLabel, ui.Pack{
|
||||||
Anchor: ui.W,
|
Anchor: ui.W,
|
||||||
})
|
})
|
||||||
|
|
||||||
extraLabel := ui.NewLabel(ui.Label{
|
// TODO: right-aligned labels clip out of bounds
|
||||||
Text: "blah",
|
// extraLabel := ui.NewLabel(ui.Label{
|
||||||
Font: balance.StatusFont,
|
// Text: "blah",
|
||||||
})
|
// Font: balance.StatusFont,
|
||||||
extraLabel.Configure(ui.Config{
|
// })
|
||||||
Background: render.Grey,
|
// extraLabel.Configure(ui.Config{
|
||||||
BorderStyle: ui.BorderSunken,
|
// Background: render.Grey,
|
||||||
BorderColor: render.Grey,
|
// BorderStyle: ui.BorderSunken,
|
||||||
BorderSize: 1,
|
// BorderColor: render.Grey,
|
||||||
})
|
// BorderSize: 1,
|
||||||
extraLabel.Compute(d.Engine)
|
// })
|
||||||
frame.Pack(extraLabel, ui.Pack{
|
// extraLabel.Compute(d.Engine)
|
||||||
Anchor: ui.E,
|
// frame.Pack(extraLabel, ui.Pack{
|
||||||
})
|
// Anchor: ui.E,
|
||||||
|
// })
|
||||||
|
|
||||||
frame.Resize(render.Rect{
|
frame.Resize(render.Rect{
|
||||||
W: d.width,
|
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
|
// Debug mode options, these can be enabled in the dev console
|
||||||
// like: boolProp DebugOverlay true
|
// like: boolProp DebugOverlay true
|
||||||
var (
|
var (
|
||||||
DebugOverlay = true
|
DebugOverlay = false
|
||||||
DebugCollision = true
|
DebugCollision = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package render
|
package level
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Grid is a 2D grid of pixels in X,Y notation.
|
// 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.
|
// 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 {
|
if _, ok := (*g)[p]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,10 @@ func (g *Grid) Exists(p level.Pixel) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the grid efficiently.
|
// Draw the grid efficiently.
|
||||||
func (g *Grid) Draw(e Engine) {
|
func (g *Grid) Draw(e render.Engine) {
|
||||||
for pixel := range *g {
|
for pixel := range *g {
|
||||||
e.DrawPoint(Black, Point{
|
color := pixel.Swatch.Color
|
||||||
|
e.DrawPoint(color, render.Point{
|
||||||
X: pixel.X,
|
X: pixel.X,
|
||||||
Y: pixel.Y,
|
Y: pixel.Y,
|
||||||
})
|
})
|
|
@ -3,6 +3,7 @@ package level
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,15 +17,30 @@ func (m *Level) ToJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadJSON loads a map from JSON file.
|
// LoadJSON loads a map from JSON file.
|
||||||
func LoadJSON(filename string) (Level, error) {
|
func LoadJSON(filename string) (*Level, error) {
|
||||||
fh, err := os.Open(filename)
|
fh, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Level{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
defer fh.Close()
|
||||||
|
|
||||||
m := Level{}
|
m := New()
|
||||||
decoder := json.NewDecoder(fh)
|
decoder := json.NewDecoder(fh)
|
||||||
err = decoder.Decode(&m)
|
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
|
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
|
// The Palette holds the unique "colors" used in this map file, and their
|
||||||
// properties (solid, fire, slippery, etc.)
|
// 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
|
// Pixels is a 2D array indexed by [X][Y]. The cell values are indexes into
|
||||||
// the Palette.
|
// 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.
|
// Pixel associates a coordinate with a palette index.
|
||||||
type Pixel struct {
|
type Pixel struct {
|
||||||
X int32 `json:"x"`
|
X int32 `json:"x"`
|
||||||
Y int32 `json:"y"`
|
Y int32 `json:"y"`
|
||||||
Palette int32 `json:"p"`
|
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.
|
// MarshalJSON serializes a Pixel compactly as a simple list.
|
||||||
func (p Pixel) MarshalJSON() ([]byte, error) {
|
func (p Pixel) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(fmt.Sprintf(
|
return []byte(fmt.Sprintf(
|
||||||
`[%d, %d, %d]`,
|
`[%d, %d, %d]`,
|
||||||
p.X, p.Y, p.Palette,
|
p.X, p.Y, p.PaletteIndex,
|
||||||
)), nil
|
)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,17 +69,8 @@ func (p *Pixel) UnmarshalJSON(text []byte) error {
|
||||||
|
|
||||||
p.X = triplet[0]
|
p.X = triplet[0]
|
||||||
p.Y = triplet[1]
|
p.Y = triplet[1]
|
||||||
p.Palette = triplet[2]
|
if len(triplet) > 2 {
|
||||||
|
p.PaletteIndex = triplet[2]
|
||||||
|
}
|
||||||
return nil
|
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 {
|
type PlayScene struct {
|
||||||
// Configuration attributes.
|
// Configuration attributes.
|
||||||
Filename string
|
Filename string
|
||||||
Canvas render.Grid
|
Canvas level.Grid
|
||||||
|
|
||||||
// Private variables.
|
// Private variables.
|
||||||
canvas render.Grid
|
canvas level.Grid
|
||||||
|
|
||||||
// Canvas size
|
// Canvas size
|
||||||
width int32
|
width int32
|
||||||
|
@ -46,7 +46,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
|
|
||||||
if s.canvas == nil {
|
if s.canvas == nil {
|
||||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
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
|
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.
|
// LoadLevel loads a level from disk.
|
||||||
func (s *PlayScene) LoadLevel(filename string) error {
|
func (s *PlayScene) LoadLevel(filename string) error {
|
||||||
s.canvas = render.Grid{}
|
s.canvas = level.Grid{}
|
||||||
|
|
||||||
m, err := level.LoadJSON(filename)
|
m, err := level.LoadJSON(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, point := range m.Pixels {
|
for _, pixel := range m.Pixels {
|
||||||
pixel := level.Pixel{
|
|
||||||
X: point.X,
|
|
||||||
Y: point.Y,
|
|
||||||
}
|
|
||||||
s.canvas[pixel] = nil
|
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?
|
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.
|
// Point holds an X,Y coordinate value.
|
||||||
type Point struct {
|
type Point struct {
|
||||||
X int32
|
X int32
|
||||||
|
@ -165,7 +104,12 @@ type Text struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Text) String() string {
|
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.
|
// Common color names.
|
||||||
|
|
|
@ -17,7 +17,7 @@ func (r *Renderer) Clear(color render.Color) {
|
||||||
// DrawPoint puts a color at a pixel.
|
// DrawPoint puts a color at a pixel.
|
||||||
func (r *Renderer) DrawPoint(color render.Color, point render.Point) {
|
func (r *Renderer) DrawPoint(color render.Color, point render.Point) {
|
||||||
if color != r.lastColor {
|
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)
|
r.renderer.DrawPoint(point.X, point.Y)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,20 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"git.kirsle.net/apps/doodle/ui/theme"
|
"git.kirsle.net/apps/doodle/ui/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckButton is a button that is bound to a boolean variable and stays clicked
|
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
||||||
// once pressed, until clicked again to release.
|
// Button and holds a boolean or string pointer (boolean for checkbox,
|
||||||
|
// string for radio).
|
||||||
type CheckButton struct {
|
type CheckButton struct {
|
||||||
Button
|
Button
|
||||||
BoolVar *bool
|
BoolVar *bool
|
||||||
|
StringVar *string
|
||||||
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCheckButton creates a new CheckButton.
|
// 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)
|
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
|
var borderStyle BorderStyle = BorderRaised
|
||||||
if w.BoolVar != nil {
|
if w.BoolVar != nil {
|
||||||
if *w.BoolVar == true {
|
if *w.BoolVar == true {
|
||||||
|
@ -57,16 +96,23 @@ func NewCheckButton(name string, boolVar *bool, child Widget) *CheckButton {
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle("MouseDown", func(p render.Point) {
|
w.Handle("MouseDown", func(p render.Point) {
|
||||||
|
var sunken bool
|
||||||
if w.BoolVar != nil {
|
if w.BoolVar != nil {
|
||||||
if *w.BoolVar {
|
if *w.BoolVar {
|
||||||
*w.BoolVar = false
|
*w.BoolVar = false
|
||||||
w.SetBorderStyle(BorderRaised)
|
|
||||||
} else {
|
} else {
|
||||||
*w.BoolVar = true
|
*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.
|
// NewCheckbox creates a new Checkbox.
|
||||||
func NewCheckbox(name string, boolVar *bool, child Widget) *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.
|
// Our custom checkbutton widget.
|
||||||
mark := NewFrame(name + "_mark")
|
mark := NewFrame(name + "_mark")
|
||||||
|
|
||||||
w := &Checkbox{
|
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()
|
w.Frame.Setup()
|
||||||
|
|
||||||
|
@ -39,6 +53,11 @@ func NewCheckbox(name string, boolVar *bool, child Widget) *Checkbox {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Child returns the child widget.
|
||||||
|
func (w *Checkbox) Child() Widget {
|
||||||
|
return w.child
|
||||||
|
}
|
||||||
|
|
||||||
// Supervise the checkbutton inside the widget.
|
// Supervise the checkbutton inside the widget.
|
||||||
func (w *Checkbox) Supervise(s *Supervisor) {
|
func (w *Checkbox) Supervise(s *Supervisor) {
|
||||||
s.Add(w.button)
|
s.Add(w.button)
|
||||||
|
|
25
ui/label.go
25
ui/label.go
|
@ -6,6 +6,12 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"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.
|
// Label is a simple text label widget.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
BaseWidget
|
BaseWidget
|
||||||
|
@ -24,10 +30,13 @@ func NewLabel(c Label) *Label {
|
||||||
w := &Label{
|
w := &Label{
|
||||||
Text: c.Text,
|
Text: c.Text,
|
||||||
TextVariable: c.TextVariable,
|
TextVariable: c.TextVariable,
|
||||||
Font: c.Font,
|
Font: DefaultFont,
|
||||||
|
}
|
||||||
|
if !c.Font.IsZero() {
|
||||||
|
w.Font = c.Font
|
||||||
}
|
}
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
return fmt.Sprintf("Label<%s>", w.text().Text)
|
return fmt.Sprintf(`Label<"%s">`, w.text().Text)
|
||||||
})
|
})
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
@ -43,9 +52,19 @@ func (w *Label) text() render.Text {
|
||||||
return w.Font
|
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.
|
// Compute the size of the label widget.
|
||||||
func (w *Label) Compute(e render.Engine) {
|
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() {
|
if !w.FixedSize() {
|
||||||
w.resizeAuto(render.Rect{
|
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