Move Editor Canvas Into UI + UI Improvements

* Increase the default window size from 800x600 to 1024x768.
* Move the drawing canvas in EditorMode to inside the EditorUI where it can
  be better managed with the other widgets it shares the screen with.
* Slightly fix Frame packing bug (with East orientation) that was causing
  right-aligned statusbar items to be partially cropped off-screen. Moved a
  couple statusbar labels in EditorMode to the right.
* Add `Parent()` and `Adopt()` methods to widgets for when they're managed
  by containers like the Frame.
* Add utility functions to UI toolkit for computing a widget's Absolute
  Position and Absolute Rect, by crawling all parent widgets and summing
  them up.
* Add `lib/debugging` package with useful stack tracing utilities.
* Add `make guitest` to launch the program into the GUI Test.
  The command line flag is: `doodle -guitest`
* Console: add a `close` command which returns to the MainScene.
* Initialize the font cache directory (~/.cache/doodle/fonts) but don't
  extract the fonts there yet.
This commit is contained in:
Noah 2018-10-08 10:38:49 -07:00
parent cfe26cb964
commit f18dcf9c2c
18 changed files with 373 additions and 110 deletions

View File

@ -23,6 +23,11 @@ build:
run: run:
go run cmd/doodle/main.go -debug go run cmd/doodle/main.go -debug
# `make guitest` to run it in guitest mode.
.PHONY: guitest
guitest:
go run cmd/doodle/main.go -debug -guitest
# `make test` to run unit tests. # `make test` to run unit tests.
.PHONY: test .PHONY: test
test: test:

View File

@ -2,6 +2,10 @@ package balance
// Numbers. // Numbers.
var ( var (
// Window dimensions.
Width = 1024
Height = 768
// Speed to scroll a canvas with arrow keys in Edit Mode. // Speed to scroll a canvas with arrow keys in Edit Mode.
CanvasScrollSpeed int32 = 8 CanvasScrollSpeed int32 = 8

View File

@ -5,6 +5,7 @@ import (
"runtime" "runtime"
"git.kirsle.net/apps/doodle" "git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render/sdl" "git.kirsle.net/apps/doodle/render/sdl"
) )
@ -15,11 +16,13 @@ var Build string
var ( var (
debug bool debug bool
edit bool edit bool
guitest bool
) )
func init() { func init() {
flag.BoolVar(&debug, "debug", false, "Debug mode") flag.BoolVar(&debug, "debug", false, "Debug mode")
flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.") flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.")
flag.BoolVar(&guitest, "guitest", false, "Enter the GUI Test scene.")
} }
func main() { func main() {
@ -35,13 +38,15 @@ func main() {
// SDL engine. // SDL engine.
engine := sdl.New( engine := sdl.New(
"Doodle v"+doodle.Version, "Doodle v"+doodle.Version,
800, balance.Width,
600, balance.Height,
) )
app := doodle.New(debug, engine) app := doodle.New(debug, engine)
app.SetupEngine() app.SetupEngine()
if filename != "" { if guitest {
app.Goto(&doodle.GUITestScene{})
} else if filename != "" {
if edit { if edit {
app.EditFile(filename) app.EditFile(filename)
} else { } else {

View File

@ -34,6 +34,8 @@ func (c Command) Run(d *Doodle) error {
return c.Edit(d) return c.Edit(d)
case "play": case "play":
return c.Play(d) return c.Play(d)
case "close":
return c.Close(d)
case "exit": case "exit":
case "quit": case "quit":
return c.Quit() return c.Quit()
@ -65,6 +67,13 @@ func (c Command) New(d *Doodle) error {
return nil return nil
} }
// Close returns to the Main Scene.
func (c Command) Close(d *Doodle) error {
main := &MainScene{}
d.Goto(main)
return nil
}
// Help prints the help info. // Help prints the help info.
func (c Command) Help(d *Doodle) error { func (c Command) Help(d *Doodle) error {
if len(c.Args) == 0 { if len(c.Args) == 0 {

View File

@ -23,10 +23,14 @@ var (
// Profile Directory settings. // Profile Directory settings.
var ( var (
ConfigDirectoryName = "doodle" ConfigDirectoryName = "doodle"
ProfileDirectory string ProfileDirectory string
LevelDirectory string LevelDirectory string
DoodadDirectory string DoodadDirectory string
CacheDirectory string
FontDirectory string
// Regexp to match simple filenames for maps and doodads. // Regexp to match simple filenames for maps and doodads.
reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`) reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`)
) )
@ -38,10 +42,19 @@ const (
) )
func init() { func init() {
// Profile directory contains the user's levels and doodads.
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName) ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels") LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads") DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
configdir.MakePath(LevelDirectory, DoodadDirectory)
// Cache directory to extract font files to.
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
FontDirectory = configdir.LocalCache(ConfigDirectoryName, "fonts")
// Ensure all the directories exist.
configdir.MakePath(LevelDirectory)
configdir.MakePath(DoodadDirectory)
configdir.MakePath(FontDirectory)
} }
// LevelPath will turn a "simple" filename into an absolute path in the user's // LevelPath will turn a "simple" filename into an absolute path in the user's

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"time" "time"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"github.com/kirsle/golog" "github.com/kirsle/golog"
@ -46,8 +47,8 @@ func New(debug bool, engine render.Engine) *Doodle {
Engine: engine, Engine: engine,
startTime: time.Now(), startTime: time.Now(),
running: true, running: true,
width: 800, width: int32(balance.Width),
height: 600, height: int32(balance.Height),
} }
d.shell = NewShell(d) d.shell = NewShell(d)

View File

@ -7,13 +7,11 @@ import (
"os" "os"
"strings" "strings"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
) )
// EditorScene manages the "Edit Level" game mode. // EditorScene manages the "Edit Level" game mode.
@ -31,10 +29,6 @@ type EditorScene struct {
Level *level.Level Level *level.Level
Doodad *doodads.Doodad Doodad *doodads.Doodad
// The canvas widget that contains the map we're working on.
// XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *uix.Canvas
// Last saved filename by the user. // Last saved filename by the user.
filename string filename string
} }
@ -46,15 +40,9 @@ 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.drawing = uix.NewCanvas(balance.ChunkSize, true) // Initialize the user interface. It references the palette and such so it
if len(s.drawing.Palette.Swatches) > 0 { // must be initialized after those things.
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0]) s.UI = NewEditorUI(d, s)
}
// TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(0, 19))
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine)
// Were we given configuration data? // Were we given configuration data?
if s.Filename != "" { if s.Filename != "" {
@ -68,7 +56,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
case enum.LevelDrawing: case enum.LevelDrawing:
if s.Level != nil { if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller") log.Debug("EditorScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(s.Level)
} else if s.filename != "" && s.OpenFile { } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
if err := s.LoadLevel(s.filename); err != nil { if err := s.LoadLevel(s.filename); err != nil {
@ -81,9 +69,9 @@ func (s *EditorScene) Setup(d *Doodle) error {
log.Debug("EditorScene.Setup: initializing a new Level") log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New() s.Level = level.New()
s.Level.Palette = level.DefaultPalette() s.Level.Palette = level.DefaultPalette()
s.drawing.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(s.Level)
s.drawing.ScrollTo(render.Origin) s.UI.Canvas.ScrollTo(render.Origin)
s.drawing.Scrollable = true s.UI.Canvas.Scrollable = true
} }
case enum.DoodadDrawing: case enum.DoodadDrawing:
// No Doodad? // No Doodad?
@ -98,20 +86,16 @@ func (s *EditorScene) Setup(d *Doodle) error {
if s.Doodad == nil { if s.Doodad == nil {
log.Debug("EditorScene.Setup: initializing a new Doodad") log.Debug("EditorScene.Setup: initializing a new Doodad")
s.Doodad = doodads.New(s.DoodadSize) s.Doodad = doodads.New(s.DoodadSize)
s.drawing.LoadDoodad(s.Doodad) s.UI.Canvas.LoadDoodad(s.Doodad)
} }
// TODO: move inside the UI. Just an approximate position for now. // TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(200, 200)) s.UI.Canvas.Resize(render.NewRect(int32(s.DoodadSize), int32(s.DoodadSize)))
s.drawing.Resize(render.NewRect(int32(s.DoodadSize), int32(s.DoodadSize))) s.UI.Canvas.ScrollTo(render.Origin)
s.drawing.ScrollTo(render.Origin) s.UI.Canvas.Scrollable = false
s.drawing.Scrollable = false s.UI.Workspace.Compute(d.Engine)
s.drawing.Compute(d.Engine)
} }
// 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.")
return nil return nil
@ -120,7 +104,6 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Loop the editor scene. // Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
s.UI.Loop(ev) s.UI.Loop(ev)
s.drawing.Loop(ev)
// Switching to Play Mode? // Switching to Play Mode?
if ev.KeyName.Read() == "p" { if ev.KeyName.Read() == "p" {
@ -141,7 +124,6 @@ func (s *EditorScene) Draw(d *Doodle) error {
d.Engine.Clear(render.Magenta) d.Engine.Clear(render.Magenta)
s.UI.Present(d.Engine) s.UI.Present(d.Engine)
s.drawing.Present(d.Engine, s.drawing.Point())
return nil return nil
} }
@ -157,7 +139,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
s.DrawingType = enum.LevelDrawing s.DrawingType = enum.LevelDrawing
s.Level = level s.Level = level
s.drawing.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(s.Level)
return nil return nil
} }
@ -182,8 +164,8 @@ func (s *EditorScene) SaveLevel(filename string) error {
m.Author = os.Getenv("USER") m.Author = os.Getenv("USER")
} }
m.Palette = s.drawing.Palette m.Palette = s.UI.Canvas.Palette
m.Chunker = s.drawing.Chunker() m.Chunker = s.UI.Canvas.Chunker()
json, err := m.ToJSON() json, err := m.ToJSON()
if err != nil { if err != nil {
@ -213,7 +195,7 @@ func (s *EditorScene) LoadDoodad(filename string) error {
s.DrawingType = enum.DoodadDrawing s.DrawingType = enum.DoodadDrawing
s.Doodad = doodad s.Doodad = doodad
s.DoodadSize = doodad.Layers[0].Chunker.Size s.DoodadSize = doodad.Layers[0].Chunker.Size
s.drawing.LoadDoodad(s.Doodad) s.UI.Canvas.LoadDoodad(s.Doodad)
return nil return nil
} }
@ -237,8 +219,8 @@ func (s *EditorScene) SaveDoodad(filename string) error {
} }
// TODO: is this copying necessary? // TODO: is this copying necessary?
d.Palette = s.drawing.Palette d.Palette = s.UI.Canvas.Palette
d.Layers[0].Chunker = s.drawing.Chunker() d.Layers[0].Chunker = s.UI.Canvas.Chunker()
// Save it to their profile directory. // Save it to their profile directory.
filename = DoodadPath(filename) filename = DoodadPath(filename)

View File

@ -7,5 +7,5 @@ import "git.kirsle.net/apps/doodle/uix"
// GetDrawing returns the uix.Canvas // GetDrawing returns the uix.Canvas
func (w *EditorScene) GetDrawing() *uix.Canvas { func (w *EditorScene) GetDrawing() *uix.Canvas {
return w.drawing return w.UI.Canvas
} }

View File

@ -7,8 +7,10 @@ import (
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/uix"
) )
// EditorUI manages the user interface for the Editor Scene. // EditorUI manages the user interface for the Editor Scene.
@ -24,6 +26,8 @@ type EditorUI struct {
// Widgets // Widgets
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
Canvas *uix.Canvas
Workspace *ui.Frame
MenuBar *ui.Frame MenuBar *ui.Frame
Palette *ui.Window Palette *ui.Window
StatusBar *ui.Frame StatusBar *ui.Frame
@ -40,14 +44,23 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusFilenameText: "Filename: <none>", StatusFilenameText: "Filename: <none>",
} }
// Select the first swatch of the palette. u.Canvas = u.SetupCanvas(d)
if u.Scene.drawing.Palette.ActiveSwatch != nil {
u.selectedSwatch = u.Scene.drawing.Palette.ActiveSwatch.Name
}
u.MenuBar = u.SetupMenuBar(d) 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)
u.Workspace = u.SetupWorkspace(d) // important that this is last!
// Position the Canvas inside the frame.
u.Workspace.Pack(u.Canvas, ui.Pack{
Anchor: ui.N,
})
u.Workspace.Compute(d.Engine)
u.ExpandCanvas(d.Engine)
// Select the first swatch of the palette.
if u.Canvas.Palette != nil && u.Canvas.Palette.ActiveSwatch != nil {
u.selectedSwatch = u.Canvas.Palette.ActiveSwatch.Name
}
return u return u
} }
@ -60,7 +73,7 @@ func (u *EditorUI) Loop(ev *events.State) {
ev.CursorY.Now, ev.CursorY.Now,
) )
u.StatusPaletteText = fmt.Sprintf("Swatch: %s", u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
u.Scene.drawing.Palette.ActiveSwatch, u.Canvas.Palette.ActiveSwatch,
) )
// Statusbar filename label. // Statusbar filename label.
@ -80,6 +93,7 @@ func (u *EditorUI) Loop(ev *events.State) {
u.MenuBar.Compute(u.d.Engine) 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)
u.Canvas.Loop(ev)
} }
// Present the UI to the screen. // Present the UI to the screen.
@ -93,6 +107,46 @@ func (u *EditorUI) Present(e render.Engine) {
u.Palette.Present(e, u.Palette.Point()) u.Palette.Present(e, u.Palette.Point())
u.MenuBar.Present(e, u.MenuBar.Point()) u.MenuBar.Present(e, u.MenuBar.Point())
u.StatusBar.Present(e, u.StatusBar.Point()) u.StatusBar.Present(e, u.StatusBar.Point())
u.Workspace.Present(e, u.Workspace.Point())
}
// SetupWorkspace configures the main Workspace frame that takes up the full
// window apart from toolbars. The Workspace has a single child element, the
// Canvas, so it can easily full-screen it or center it for Doodad editing.
func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame {
frame := ui.NewFrame("Workspace")
// Position and size the frame around the other main widgets.
frame.MoveTo(render.NewPoint(
0,
u.MenuBar.Size().H,
))
frame.Resize(render.NewRect(
d.width-u.Palette.Size().W,
d.height-u.MenuBar.Size().H-u.StatusBar.Size().H,
))
frame.Compute(d.Engine)
return frame
}
// SetupCanvas configures the main drawing canvas in the editor.
func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
drawing := uix.NewCanvas(balance.ChunkSize, true)
drawing.Palette = level.DefaultPalette()
if len(drawing.Palette.Swatches) > 0 {
drawing.SetSwatch(drawing.Palette.Swatches[0])
}
return drawing
}
// ExpandCanvas manually expands the Canvas to fill the frame, to work around
// UI packing bugs. Ideally I would use `Expand: true` when packing the Canvas
// in its frame, but that would artificially expand the Canvas also when it
// _wanted_ to be smaller, as in Doodad Editing Mode.
func (u *EditorUI) ExpandCanvas(e render.Engine) {
u.Canvas.Resize(u.Workspace.Size())
u.Workspace.Compute(e)
} }
// SetupMenuBar sets up the menu bar. // SetupMenuBar sets up the menu bar.
@ -214,7 +268,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.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.ConfigureTitle(balance.TitleConfig)
window.TitleBar().Font = balance.TitleFont window.TitleBar().Font = balance.TitleFont
@ -232,17 +285,18 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
// Handler function for the radio buttons being clicked. // Handler function for the radio buttons being clicked.
onClick := func(p render.Point) { onClick := func(p render.Point) {
name := u.selectedSwatch name := u.selectedSwatch
swatch, ok := u.Scene.drawing.Palette.Get(name) swatch, ok := u.Canvas.Palette.Get(name)
if !ok { if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name) log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return return
} }
log.Info("Set swatch: %s", swatch) log.Info("Set swatch: %s", swatch)
u.Scene.drawing.SetSwatch(swatch) u.Canvas.SetSwatch(swatch)
} }
// Draw the radio buttons for the palette. // Draw the radio buttons for the palette.
for _, swatch := range u.Scene.drawing.Palette.Swatches { if u.Canvas != nil && u.Canvas.Palette != nil {
for _, swatch := range u.Canvas.Palette.Swatches {
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
Text: swatch.Name, Text: swatch.Name,
Font: balance.StatusFont, Font: balance.StatusFont,
@ -259,6 +313,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
PadY: 4, PadY: 4,
}) })
} }
}
return window return window
} }
@ -309,25 +364,25 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
filenameLabel.Configure(style) 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.E,
PadX: 1, PadX: 1,
}) })
// TODO: right-aligned labels clip out of bounds // TODO: right-aligned labels clip out of bounds
// extraLabel := ui.NewLabel(ui.Label{ extraLabel := ui.NewLabel(ui.Label{
// Text: "blah", Text: "blah",
// Font: balance.StatusFont, Font: balance.StatusFont,
// }) })
// extraLabel.Configure(ui.Config{ extraLabel.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,
// }) })
// extraLabel.Compute(d.Engine) extraLabel.Compute(d.Engine)
// frame.Pack(extraLabel, ui.Pack{ frame.Pack(extraLabel, ui.Pack{
// Anchor: ui.E, Anchor: ui.E,
// }) })
frame.Resize(render.Rect{ frame.Resize(render.Rect{
W: d.width, W: d.width,

104
lib/debugging/debugging.go Normal file
View File

@ -0,0 +1,104 @@
// Package debugging contains useful methods for debugging the app, safely
// isolated from the rest of the app's packages.
package debugging
import (
"fmt"
"runtime"
"strings"
)
// Configurable variables for the stack tracer functions.
var (
// StackDepth is the depth that Callers() will crawl up the call stack. This
// variable is configurable.
StackDepth = 20
// StopAt is the function name to stop the tracebacks at. Set to a blank
// string to not stop and trace all the way up to `runtime.goexit` or
// wherever.
StopAt = "main.main"
)
// Minimum depth given to runtime.Caller() so that the call stacks will exclude
// the call to debugging.Caller() itself -- so this debug module won't debug its
// own function calls in the tracebacks.
const minDepth = 2
// Caller returns the filename and line number that called the calling
// function.
func Caller() string {
if pc, file, no, ok := runtime.Caller(minDepth); ok {
frames := runtime.CallersFrames([]uintptr{pc})
frame, _ := frames.Next()
if frame.Function != "" {
return fmt.Sprintf("%s#%d: %s()",
frame.File,
frame.Line,
frame.Function,
)
}
return fmt.Sprintf("%s#%d",
file,
no,
)
}
return "[no caller information]"
}
// Callers returns an array of all the callers of the current function.
func Callers() []string {
var (
callers []string
pc = make([]uintptr, StackDepth)
count = runtime.Callers(minDepth, pc)
)
pc = pc[:count] // only pass valid program counters to CallersFrames
var frames = runtime.CallersFrames(pc)
_ = frames
// Loop to get frames of the call stack.
for {
frame, more := frames.Next()
callers = append(callers, fmt.Sprintf("%s#%d: %s()",
frame.File,
frame.Line,
frame.Function,
))
if StopAt != "" && frame.Function == StopAt {
break
}
if !more {
break
}
}
return callers
}
// StringifyCallers pretty-prints the Callers as a single string with newlines.
func StringifyCallers() string {
callers := Callers()
var result []string
for i, caller := range callers {
if i == 0 {
continue // StringifyCallers() would be the first row, skip it.
}
result = append(result, fmt.Sprintf("%d: %s", i, caller))
}
return strings.Join(result, "\n")
}
// PrintCallers prints the stringified callers directly to STDOUT.
func PrintCallers() {
fmt.Println("Call stack (most recent/current function first):")
for i, caller := range Callers() {
if i == 0 {
continue // PrintCallers() would be the first row, skip it.
}
fmt.Printf("%d: %s\n", i, caller)
}
}

View File

@ -31,12 +31,12 @@ type Renderer struct {
} }
// New creates the SDL renderer. // New creates the SDL renderer.
func New(title string, width, height int32) *Renderer { func New(title string, width, height int) *Renderer {
return &Renderer{ return &Renderer{
events: events.New(), events: events.New(),
title: title, title: title,
width: width, width: int32(width),
height: height, height: int32(height),
} }
} }

View File

@ -82,6 +82,7 @@ func (w *Button) SetText(text string) error {
// Present the button. // Present the button.
func (w *Button) Present(e render.Engine, P render.Point) { func (w *Button) Present(e render.Engine, P render.Point) {
w.Compute(e) w.Compute(e)
w.MoveTo(P)
var ( var (
S = w.Size() S = w.Size()
ChildSize = w.child.Size() ChildSize = w.child.Size()

View File

@ -69,7 +69,14 @@ func (w *Frame) Present(e render.Engine, P render.Point) {
P.X+p.X+w.BoxThickness(1), P.X+p.X+w.BoxThickness(1),
P.Y+p.Y+w.BoxThickness(1), P.Y+p.Y+w.BoxThickness(1),
) )
child.MoveTo(moveTo) // if child.ID() == "Canvas" {
// log.Debug("Frame X=%d Child X=%d Box=%d Point=%s", P.X, p.X, w.BoxThickness(1), p)
// log.Debug("Frame Y=%d Child Y=%d Box=%d MoveTo=%s", P.Y, p.Y, w.BoxThickness(1), moveTo)
// }
// child.MoveTo(moveTo) // TODO: if uncommented the child will creep down the parent each tick
// if child.ID() == "Canvas" {
// log.Debug("New Point: %s", child.Point())
// }
child.Present(e, moveTo) child.Present(e, moveTo)
} }
} }

View File

@ -32,12 +32,12 @@ func (w *Frame) computePacked(e render.Engine) {
xDirection int32 = 1 xDirection int32 = 1
) )
if anchor.IsSouth() { if anchor.IsSouth() { // TODO: these need tuning
y = frameSize.H y = frameSize.H - w.BoxThickness(4)
yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2 yDirection = -1 * w.BoxThickness(4) // parent + child BoxThickness(1) = 2
} else if anchor == E { } else if anchor == E {
x = frameSize.W x = frameSize.W - w.BoxThickness(4)
xDirection = -1 // - w.BoxThickness(2) xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
} }
for _, packedWidget := range w.packs[anchor] { for _, packedWidget := range w.packs[anchor] {
@ -64,10 +64,10 @@ func (w *Frame) computePacked(e render.Engine) {
} }
if anchor.IsSouth() { if anchor.IsSouth() {
y -= size.H + pack.PadY y -= size.H - pack.PadY
} }
if anchor.IsEast() { if anchor.IsEast() {
x -= size.W + pack.PadX x -= size.W - pack.PadX
} }
child.MoveTo(render.NewPoint(x, y)) child.MoveTo(render.NewPoint(x, y))
@ -80,7 +80,7 @@ func (w *Frame) computePacked(e render.Engine) {
} }
visited = append(visited, packedWidget) visited = append(visited, packedWidget)
if pack.Expand { if pack.Expand { // TODO: don't fuck with children of fixed size
expanded = append(expanded, packedWidget) expanded = append(expanded, packedWidget)
} }
} }
@ -131,10 +131,6 @@ func (w *Frame) computePacked(e render.Engine) {
moved bool moved bool
) )
if w.String() == "Frame<Row0; 3 widgets>" {
log.Debug("%s>%s: pack.FillX=%d resize=%s innerFrameSize=%s", w, child, pack.FillX, resize, innerFrameSize)
}
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() { if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
if pack.FillX && resize.W < innerFrameSize.W { if pack.FillX && resize.W < innerFrameSize.W {
resize.W = innerFrameSize.W - w.BoxThickness(2) resize.W = innerFrameSize.W - w.BoxThickness(2)
@ -175,7 +171,6 @@ func (w *Frame) computePacked(e render.Engine) {
} }
if resized && size != resize { if resized && size != resize {
// log.Debug("%s/%s: resize to: %s", w, child, resize)
child.Resize(resize) child.Resize(resize)
child.Compute(e) child.Compute(e)
} }
@ -288,6 +283,9 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
C.FillY = true C.FillY = true
} }
// Adopt the child widget so it can access the Frame.
child.Adopt(w)
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{ w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child, widget: child,
pack: C, pack: C,

38
ui/functions.go Normal file
View File

@ -0,0 +1,38 @@
package ui
import "git.kirsle.net/apps/doodle/render"
// AbsolutePosition computes a widget's absolute X,Y position on the
// window on screen by crawling its parent widget tree.
func AbsolutePosition(w Widget) render.Point {
abs := w.Point()
var (
node = w
ok bool
)
for {
node, ok = node.Parent()
if !ok { // reached the top of the tree
return abs
}
abs.Add(node.Point())
}
}
// AbsoluteRect returns a Rect() offset with the absolute position.
func AbsoluteRect(w Widget) render.Rect {
var (
P = AbsolutePosition(w)
R = w.Rect()
)
return render.Rect{
X: P.X,
Y: P.Y,
W: R.W + P.X,
H: R.H, // TODO: the Canvas in EditMode lets you draw pixels
// below the status bar if we do `+ R.Y` here.
}
}

View File

@ -56,6 +56,11 @@ type Widget interface {
OutlineSize() int32 // Outline size (default 0) OutlineSize() int32 // Outline size (default 0)
SetOutlineSize(int32) // SetOutlineSize(int32) //
// Container widgets like Frames can wire up associations between the
// child widgets and the parent.
Parent() (parent Widget, ok bool)
Adopt(parent Widget) // for the container to assign itself the parent
// Run any render computations; by the end the widget must know its // Run any render computations; by the end the widget must know its
// Width and Height. For example the Label widget will render itself onto // Width and Height. For example the Label widget will render itself onto
// an SDL Surface and then it will know its bounding box, but not before. // an SDL Surface and then it will know its bounding box, but not before.
@ -105,6 +110,8 @@ type BaseWidget struct {
outlineColor render.Color outlineColor render.Color
outlineSize int32 outlineSize int32
handlers map[Event][]func(render.Point) handlers map[Event][]func(render.Point)
hasParent bool
parent Widget
} }
// SetID sets a string name for your widget, helpful for debugging purposes. // SetID sets a string name for your widget, helpful for debugging purposes.
@ -250,6 +257,25 @@ func (w *BaseWidget) BoxThickness(m int32) int32 {
return (w.Margin() * m) + (w.BorderSize() * m) + (w.OutlineSize() * m) return (w.Margin() * m) + (w.BorderSize() * m) + (w.OutlineSize() * m)
} }
// Parent returns the parent widget, like a Frame, and a boolean indicating
// whether the widget had a parent.
func (w *BaseWidget) Parent() (Widget, bool) {
return w.parent, w.hasParent
}
// Adopt sets the widget's parent. This function is called by container
// widgets like Frame when they add a child widget to their care.
// Pass a nil parent to unset the parent.
func (w *BaseWidget) Adopt(parent Widget) {
if parent == nil {
w.hasParent = false
w.parent = nil
} else {
w.hasParent = true
w.parent = parent
}
}
// DrawBox draws the border and outline. // DrawBox draws the border and outline.
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) { func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
var ( var (

View File

@ -38,6 +38,9 @@ func NewCanvas(size int, editable bool) *Canvas {
chunks: level.NewChunker(size), chunks: level.NewChunker(size),
} }
w.setup() w.setup()
w.IDFunc(func() string {
return "Canvas"
})
return w return w
} }
@ -81,10 +84,9 @@ func (w *Canvas) setup() {
// Loop is called on the scene's event loop to handle mouse interaction with // Loop is called on the scene's event loop to handle mouse interaction with
// the canvas, i.e. to edit it. // the canvas, i.e. to edit it.
func (w *Canvas) Loop(ev *events.State) error { func (w *Canvas) Loop(ev *events.State) error {
var ( // Get the absolute position of the canvas on screen to accurately match
P = w.Point() // it up to mouse clicks.
_ = P var P = ui.AbsolutePosition(w)
)
if w.Scrollable { if w.Scrollable {
// Arrow keys to scroll the view. // Arrow keys to scroll the view.
@ -106,7 +108,7 @@ func (w *Canvas) Loop(ev *events.State) error {
// Only care if the cursor is over our space. // Only care if the cursor is over our space.
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if !cursor.Inside(w.Rect()) { if !cursor.Inside(ui.AbsoluteRect(w)) {
return nil return nil
} }
@ -117,7 +119,6 @@ func (w *Canvas) Loop(ev *events.State) error {
// 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)
lastPixel := w.lastPixel lastPixel := w.lastPixel
cursor := render.Point{ cursor := render.Point{
X: ev.CursorX.Now - P.X + w.Scroll.X, X: ev.CursorX.Now - P.X + w.Scroll.X,
@ -193,7 +194,7 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
S = w.Size() S = w.Size()
Viewport = w.Viewport() Viewport = w.Viewport()
) )
w.MoveTo(p) // w.MoveTo(p) // TODO: when uncommented the canvas will creep down the Workspace frame in EditorMode
w.DrawBox(e, p) w.DrawBox(e, p)
e.DrawBox(w.Background(), render.Rect{ e.DrawBox(w.Background(), render.Rect{
X: p.X + w.BoxThickness(1), X: p.X + w.BoxThickness(1),

14
uix/log.go Normal file
View File

@ -0,0 +1,14 @@
package uix
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("uix")
log.Configure(&golog.Config{
Level: golog.DebugLevel,
Theme: golog.DarkTheme,
Colors: golog.ExtendedColor,
})
}