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.
bitmap-cache
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:
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.
.PHONY: test
test:

View File

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

View File

@ -5,6 +5,7 @@ import (
"runtime"
"git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render/sdl"
)
@ -13,13 +14,15 @@ var Build string
// Command line args
var (
debug bool
edit bool
debug bool
edit bool
guitest bool
)
func init() {
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(&guitest, "guitest", false, "Enter the GUI Test scene.")
}
func main() {
@ -35,13 +38,15 @@ func main() {
// SDL engine.
engine := sdl.New(
"Doodle v"+doodle.Version,
800,
600,
balance.Width,
balance.Height,
)
app := doodle.New(debug, engine)
app.SetupEngine()
if filename != "" {
if guitest {
app.Goto(&doodle.GUITestScene{})
} else if filename != "" {
if edit {
app.EditFile(filename)
} else {

View File

@ -34,6 +34,8 @@ func (c Command) Run(d *Doodle) error {
return c.Edit(d)
case "play":
return c.Play(d)
case "close":
return c.Close(d)
case "exit":
case "quit":
return c.Quit()
@ -65,6 +67,13 @@ func (c Command) New(d *Doodle) error {
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.
func (c Command) Help(d *Doodle) error {
if len(c.Args) == 0 {

View File

@ -23,9 +23,13 @@ var (
// Profile Directory settings.
var (
ConfigDirectoryName = "doodle"
ProfileDirectory string
LevelDirectory string
DoodadDirectory string
ProfileDirectory string
LevelDirectory string
DoodadDirectory string
CacheDirectory string
FontDirectory string
// Regexp to match simple filenames for maps and doodads.
reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`)
@ -38,10 +42,19 @@ const (
)
func init() {
// Profile directory contains the user's levels and doodads.
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
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

View File

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

View File

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

View File

@ -7,5 +7,5 @@ import "git.kirsle.net/apps/doodle/uix"
// GetDrawing returns the 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/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/uix"
)
// EditorUI manages the user interface for the Editor Scene.
@ -24,6 +26,8 @@ type EditorUI struct {
// Widgets
Supervisor *ui.Supervisor
Canvas *uix.Canvas
Workspace *ui.Frame
MenuBar *ui.Frame
Palette *ui.Window
StatusBar *ui.Frame
@ -40,14 +44,23 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusFilenameText: "Filename: <none>",
}
// Select the first swatch of the palette.
if u.Scene.drawing.Palette.ActiveSwatch != nil {
u.selectedSwatch = u.Scene.drawing.Palette.ActiveSwatch.Name
}
u.Canvas = u.SetupCanvas(d)
u.MenuBar = u.SetupMenuBar(d)
u.StatusBar = u.SetupStatusBar(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
}
@ -60,7 +73,7 @@ func (u *EditorUI) Loop(ev *events.State) {
ev.CursorY.Now,
)
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
u.Scene.drawing.Palette.ActiveSwatch,
u.Canvas.Palette.ActiveSwatch,
)
// Statusbar filename label.
@ -80,6 +93,7 @@ func (u *EditorUI) Loop(ev *events.State) {
u.MenuBar.Compute(u.d.Engine)
u.StatusBar.Compute(u.d.Engine)
u.Palette.Compute(u.d.Engine)
u.Canvas.Loop(ev)
}
// 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.MenuBar.Present(e, u.MenuBar.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.
@ -214,7 +268,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
// SetupPalette sets up the palette panel.
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
log.Error("SetupPalette Window")
window := ui.NewWindow("Palette")
window.ConfigureTitle(balance.TitleConfig)
window.TitleBar().Font = balance.TitleFont
@ -232,32 +285,34 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
// Handler function for the radio buttons being clicked.
onClick := func(p render.Point) {
name := u.selectedSwatch
swatch, ok := u.Scene.drawing.Palette.Get(name)
swatch, ok := u.Canvas.Palette.Get(name)
if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return
}
log.Info("Set swatch: %s", swatch)
u.Scene.drawing.SetSwatch(swatch)
u.Canvas.SetSwatch(swatch)
}
// Draw the radio buttons for the palette.
for _, swatch := range u.Scene.drawing.Palette.Swatches {
label := ui.NewLabel(ui.Label{
Text: swatch.Name,
Font: balance.StatusFont,
})
label.Font.Color = swatch.Color.Darken(40)
if u.Canvas != nil && u.Canvas.Palette != nil {
for _, swatch := range u.Canvas.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.selectedSwatch, swatch.Name, label)
btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn)
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn)
window.Pack(btn, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
window.Pack(btn, ui.Pack{
Anchor: ui.N,
Fill: true,
PadY: 4,
})
}
}
return window
@ -309,25 +364,25 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
filenameLabel.Configure(style)
filenameLabel.Compute(d.Engine)
frame.Pack(filenameLabel, ui.Pack{
Anchor: ui.W,
Anchor: ui.E,
PadX: 1,
})
// 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,
// })
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,

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.
func New(title string, width, height int32) *Renderer {
func New(title string, width, height int) *Renderer {
return &Renderer{
events: events.New(),
title: title,
width: width,
height: height,
width: int32(width),
height: int32(height),
}
}

View File

@ -82,6 +82,7 @@ func (w *Button) SetText(text string) error {
// Present the button.
func (w *Button) Present(e render.Engine, P render.Point) {
w.Compute(e)
w.MoveTo(P)
var (
S = w.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.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)
}
}

View File

@ -32,12 +32,12 @@ func (w *Frame) computePacked(e render.Engine) {
xDirection int32 = 1
)
if anchor.IsSouth() {
y = frameSize.H
yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2
if anchor.IsSouth() { // TODO: these need tuning
y = frameSize.H - w.BoxThickness(4)
yDirection = -1 * w.BoxThickness(4) // parent + child BoxThickness(1) = 2
} else if anchor == E {
x = frameSize.W
xDirection = -1 // - w.BoxThickness(2)
x = frameSize.W - w.BoxThickness(4)
xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
}
for _, packedWidget := range w.packs[anchor] {
@ -64,10 +64,10 @@ func (w *Frame) computePacked(e render.Engine) {
}
if anchor.IsSouth() {
y -= size.H + pack.PadY
y -= size.H - pack.PadY
}
if anchor.IsEast() {
x -= size.W + pack.PadX
x -= size.W - pack.PadX
}
child.MoveTo(render.NewPoint(x, y))
@ -80,7 +80,7 @@ func (w *Frame) computePacked(e render.Engine) {
}
visited = append(visited, packedWidget)
if pack.Expand {
if pack.Expand { // TODO: don't fuck with children of fixed size
expanded = append(expanded, packedWidget)
}
}
@ -131,10 +131,6 @@ func (w *Frame) computePacked(e render.Engine) {
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.FillX && resize.W < innerFrameSize.W {
resize.W = innerFrameSize.W - w.BoxThickness(2)
@ -175,7 +171,6 @@ func (w *Frame) computePacked(e render.Engine) {
}
if resized && size != resize {
// log.Debug("%s/%s: resize to: %s", w, child, resize)
child.Resize(resize)
child.Compute(e)
}
@ -288,6 +283,9 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
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{
widget: child,
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)
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
// 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.
@ -105,6 +110,8 @@ type BaseWidget struct {
outlineColor render.Color
outlineSize int32
handlers map[Event][]func(render.Point)
hasParent bool
parent Widget
}
// 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)
}
// 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.
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
var (

View File

@ -38,6 +38,9 @@ func NewCanvas(size int, editable bool) *Canvas {
chunks: level.NewChunker(size),
}
w.setup()
w.IDFunc(func() string {
return "Canvas"
})
return w
}
@ -81,10 +84,9 @@ func (w *Canvas) setup() {
// Loop is called on the scene's event loop to handle mouse interaction with
// the canvas, i.e. to edit it.
func (w *Canvas) Loop(ev *events.State) error {
var (
P = w.Point()
_ = P
)
// Get the absolute position of the canvas on screen to accurately match
// it up to mouse clicks.
var P = ui.AbsolutePosition(w)
if w.Scrollable {
// 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.
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if !cursor.Inside(w.Rect()) {
if !cursor.Inside(ui.AbsoluteRect(w)) {
return nil
}
@ -117,7 +119,6 @@ func (w *Canvas) Loop(ev *events.State) error {
// Clicking? Log all the pixels while doing so.
if ev.Button1.Now {
// log.Warn("Button1: %+v", ev.Button1)
lastPixel := w.lastPixel
cursor := render.Point{
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()
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)
e.DrawBox(w.Background(), render.Rect{
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,
})
}