From f18dcf9c2c8e776e3e28f209fe8a57b57d17a647 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 8 Oct 2018 10:38:49 -0700 Subject: [PATCH] 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. --- Makefile | 5 ++ balance/numbers.go | 4 ++ cmd/doodle/main.go | 15 +++-- commands.go | 9 +++ config.go | 21 ++++-- doodle.go | 5 +- editor_scene.go | 54 +++++---------- editor_scene_debug.go | 2 +- editor_ui.go | 131 ++++++++++++++++++++++++++----------- lib/debugging/debugging.go | 104 +++++++++++++++++++++++++++++ render/sdl/sdl.go | 6 +- ui/button.go | 1 + ui/frame.go | 9 ++- ui/frame_pack.go | 24 ++++--- ui/functions.go | 38 +++++++++++ ui/widget.go | 26 ++++++++ uix/canvas.go | 15 +++-- uix/log.go | 14 ++++ 18 files changed, 373 insertions(+), 110 deletions(-) create mode 100644 lib/debugging/debugging.go create mode 100644 ui/functions.go create mode 100644 uix/log.go diff --git a/Makefile b/Makefile index dd8ea37..c300d2d 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/balance/numbers.go b/balance/numbers.go index 38ddf0c..31c1c8f 100644 --- a/balance/numbers.go +++ b/balance/numbers.go @@ -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 diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index b0729c4..0ed83ec 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -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 { diff --git a/commands.go b/commands.go index 7aa5e3a..796f84d 100644 --- a/commands.go +++ b/commands.go @@ -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 { diff --git a/config.go b/config.go index 2e5535c..466bb42 100644 --- a/config.go +++ b/config.go @@ -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 diff --git a/doodle.go b/doodle.go index de1a96e..14942fc 100644 --- a/doodle.go +++ b/doodle.go @@ -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) diff --git a/editor_scene.go b/editor_scene.go index f7870a9..cbb9905 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -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) diff --git a/editor_scene_debug.go b/editor_scene_debug.go index 7e74866..f2ceea0 100644 --- a/editor_scene_debug.go +++ b/editor_scene_debug.go @@ -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 } diff --git a/editor_ui.go b/editor_ui.go index 6714dc5..00dfaba 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -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: ", } - // 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, diff --git a/lib/debugging/debugging.go b/lib/debugging/debugging.go new file mode 100644 index 0000000..d20ca85 --- /dev/null +++ b/lib/debugging/debugging.go @@ -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) + } +} diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go index 89b7252..b23cc4f 100644 --- a/render/sdl/sdl.go +++ b/render/sdl/sdl.go @@ -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), } } diff --git a/ui/button.go b/ui/button.go index a26f237..7e60db7 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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() diff --git a/ui/frame.go b/ui/frame.go index 2c3d218..76735c6 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -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) } } diff --git a/ui/frame_pack.go b/ui/frame_pack.go index f0061d4..d2fb6d2 100644 --- a/ui/frame_pack.go +++ b/ui/frame_pack.go @@ -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" { - 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, diff --git a/ui/functions.go b/ui/functions.go new file mode 100644 index 0000000..bceca8b --- /dev/null +++ b/ui/functions.go @@ -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. + } +} diff --git a/ui/widget.go b/ui/widget.go index 6f07e4a..d5710ab 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -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 ( diff --git a/uix/canvas.go b/uix/canvas.go index 0539909..f6936b1 100644 --- a/uix/canvas.go +++ b/uix/canvas.go @@ -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), diff --git a/uix/log.go b/uix/log.go new file mode 100644 index 0000000..403dc2f --- /dev/null +++ b/uix/log.go @@ -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, + }) +}