From 4ba563d48d6f1f3ca20d1855a0567fc08343ebb9 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 28 Dec 2019 23:56:00 -0800 Subject: [PATCH] Supervisor, Frame Pack and Misc Fixes * Button: do not call MoveTo inside of Compute(). * Label: do not call MoveTo inside of Compute(). * MainWindow: add OnLoop callback function support so you can run custom code each loop and react to the event.State before the UI updates. * Supervisor: locate widgets using AbsolutePosition() instead of w.Point() to play nice with Frame and Window packed widgets. * Widget interface: rename Adopt() to SetParent() which makes more sense for what the function actually does. * Window: set itself as the parent of the body Frame so that the Supervisor can locate widgets anywhere inside a window's frames. Frame packing fixes: * Widgets with Expand:true grow their space with ResizeAuto to preserve the FixedSize() boolean, instead of being hard-resized to fill the Frame. * Widgets that Fill their space are resized with ResizeAuto too. Outstanding bugs: * Labels don't expand (window title bars, etc.) --- README.md | 22 ++-------------------- button.go | 1 - debug.go | 19 ++++++++++++++++--- frame_pack.go | 46 ++++++++++++++++++++++++++++++++-------------- functions.go | 4 +++- label.go | 5 ----- main_window.go | 39 ++++++++++++++++++++++++++++++--------- supervisor.go | 12 +++++++----- widget.go | 19 ++++++------------- window.go | 3 +++ 10 files changed, 99 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index b3d33e2..8ecac25 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ applications (SDL2, for Linux, MacOS and Windows) as well as web browsers > _(Screenshot is from Project: Doodle's GUITest debug screen showing a_ > _Window, several Frames, Labels, Buttons and a Checkbox widget.)_ -It is very much a **work in progress** and it's a bit buggy. See the -[Known Issues](#known-issues) at the bottom of this document. +It is very much a **work in progress** and may contain bugs and its API may +change as bugs are fixed or features added. This library is being developed in conjunction with my drawing-based maze game, [Project: Doodle](https://www.kirsle.net/doodle). The rendering engine @@ -214,24 +214,6 @@ MainWindow includes its own Supervisor: just call the `.Add(Widget)` method to add interactive widgets to the supervisor. The MainLoop() of the window calls Supervisor.Loop() automatically. -# Known Issues - -The frame packing algorithm (frame_pack.go) is currently very buggy and in -need of a re-write. Some examples of issues with it: - -* Currently, when the Frame is iterating over packed widgets to decide their - location and size, it explicitly calls MoveTo() and Resize() giving them - their pixel-coordinates, relative to the Frame's own position. - * When Frames nest other Frames this becomes more of an issue. - * The Supervisor sometimes can't determine the correct position of a - button packed inside of nested frames. It currently checks the - Point() of the button (set by its parent Frame) and this doesn't - account for the grandparent frame's position. Using the - AbsolutePosition() helper function (which recursively crawls up a - widget tree) also yields incorrect results, as the position of each - Frame is _added_ to the position of the Button which throws it off even - further. - # License MIT. diff --git a/button.go b/button.go index 0de40bd..4eda12b 100644 --- a/button.go +++ b/button.go @@ -91,7 +91,6 @@ 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/debug.go b/debug.go index 2111668..4ce10d0 100644 --- a/debug.go +++ b/debug.go @@ -1,15 +1,28 @@ package ui -import "strings" +import ( + "fmt" + "strings" +) // WidgetTree returns a string representing the tree of widgets starting // at a given widget. func WidgetTree(root Widget) []string { var crawl func(int, Widget) []string + crawl = func(depth int, node Widget) []string { var ( - prefix = strings.Repeat(" ", depth) - lines = []string{prefix + node.ID()} + prefix = strings.Repeat(" ", depth) + size = node.Size() + width = size.W + height = size.H + fixedSize = node.FixedSize() + + lines = []string{ + fmt.Sprintf("%s%s P:%s S:%dx%d (fixedSize: %+v)", + prefix, node.ID(), node.Point(), width, height, fixedSize, + ), + } ) for _, child := range node.Children() { diff --git a/frame_pack.go b/frame_pack.go index 7878f01..38c7767 100644 --- a/frame_pack.go +++ b/frame_pack.go @@ -47,7 +47,7 @@ func (w *Frame) Pack(child Widget, config ...Pack) { } // Adopt the child widget so it can access the Frame. - child.Adopt(w) + child.SetParent(w) w.packs[C.Side] = append(w.packs[C.Side], packedWidget{ widget: child, @@ -65,7 +65,7 @@ func (w *Frame) computePacked(e render.Engine) { // that the Frame must be to contain all of its children. If the Frame // was configured with an explicit Size, the Frame will be that Size, // but we still calculate how much space the widgets _actually_ take - // so we can expand them to fill remaining space in fixed size widgets. + // so we can expand them to fill remaining space in fixed size Frames. maxWidth int maxHeight int visited = []packedWidget{} @@ -82,8 +82,8 @@ func (w *Frame) computePacked(e render.Engine) { var ( x int y int - yDirection int = 1 - xDirection int = 1 + yDirection = 1 + xDirection = 1 ) if side.IsSouth() { @@ -128,12 +128,15 @@ func (w *Frame) computePacked(e render.Engine) { x -= size.W - pack.PadX } + // NOTE: we place the child's position relative to the Frame's + // position. So a child placed at the top/left of the Frame gets + // an x,y near zero regardless of the Frame's position. child.MoveTo(render.NewPoint(x, y)) if side.IsNorth() { y += size.H + pack.PadY } - if side == W { + if side.IsWest() { x += size.W + pack.PadX } @@ -147,14 +150,20 @@ func (w *Frame) computePacked(e render.Engine) { // If we have extra space in the Frame and any expanding widgets, let the // expanding widgets grow and share the remaining space. computedSize := render.NewRect(maxWidth, maxHeight) - if len(expanded) > 0 && !frameSize.IsZero() && frameSize.Bigger(computedSize) { + if len(expanded) > 0 && !frameSize.IsZero() { // && frameSize.Bigger(computedSize) { // Divy up the size available. growBy := render.Rect{ W: ((frameSize.W - computedSize.W) / len(expanded)) - w.BoxThickness(4), H: ((frameSize.H - computedSize.H) / len(expanded)) - w.BoxThickness(4), } for _, pw := range expanded { - pw.widget.ResizeBy(growBy) + // Grow the widget but maintain its auto-size flag, in case the widget + // was not given an explicit size before. + size := pw.widget.Size() + pw.widget.ResizeAuto(render.Rect{ + W: size.W + growBy.W, + H: size.H + growBy.H, + }) pw.widget.Compute(e) } } @@ -190,10 +199,16 @@ func (w *Frame) computePacked(e render.Engine) { ) if pack.Side.IsNorth() || pack.Side.IsSouth() { + // Aligned to the top or bottom. If the widget Fills horizontally, + // resize it so its Width matches the frame's Width. if pack.FillX && resize.W < innerFrameSize.W { - resize.W = innerFrameSize.W - w.BoxThickness(2) + resize.W = innerFrameSize.W - w.BoxThickness(2) // TODO: child.BoxThickness instead?? resized = true } + + // If it does not Fill horizontally and there is extra horizontal + // space, center the widget inside the space. TODO: Anchor option + // could align the widget to the left or right instead of center. if resize.W < innerFrameSize.W-w.BoxThickness(4) { if pack.Side.IsCenter() { point.X = (innerFrameSize.W / 2) - (resize.W / 2) @@ -206,19 +221,19 @@ func (w *Frame) computePacked(e render.Engine) { moved = true } } else if pack.Side.IsWest() || pack.Side.IsEast() { + // Similar logic to the above, but widget is packed against the + // left or right edge. Handle vertical Fill to grow the widget. if pack.FillY && resize.H < innerFrameSize.H { - resize.H = innerFrameSize.H - w.BoxThickness(2) // BoxThickness(2) for parent + child - // point.Y -= (w.BoxThickness(4) + child.BoxThickness(2)) - moved = true + resize.H = innerFrameSize.H - w.BoxThickness(2) // TODO: child.BoxThickness instead?? resized = true } // Vertically align the widgets. if resize.H < innerFrameSize.H { if pack.Side.IsMiddle() { - point.Y = (innerFrameSize.H / 2) - (resize.H / 2) - w.BoxThickness(1) + point.Y = (innerFrameSize.H / 2) - (resize.H / 2) // - w.BoxThickness(1) } else if pack.Side.IsNorth() { - point.Y = pack.PadY - w.BoxThickness(4) + point.Y = pack.PadY // - w.BoxThickness(4) } else if pack.Side.IsSouth() { point.Y = innerFrameSize.H - resize.H - pack.PadY } @@ -229,7 +244,7 @@ func (w *Frame) computePacked(e render.Engine) { } if resized && size != resize { - child.Resize(resize) + child.ResizeAuto(resize) child.Compute(e) } if moved { @@ -237,6 +252,9 @@ func (w *Frame) computePacked(e render.Engine) { } } + // TODO: the Frame should ResizeAuto so it doesn't mark fixedSize=true. + // Currently there's a bug where frames will grow when the window grows but + // never shrink again when the window shrinks. // if !w.FixedSize() { w.Resize(render.NewRect( frameSize.W-w.BoxThickness(2), diff --git a/functions.go b/functions.go index 9689cd9..c235acd 100644 --- a/functions.go +++ b/functions.go @@ -1,6 +1,8 @@ package ui -import "git.kirsle.net/go/render" +import ( + "git.kirsle.net/go/render" +) // AbsolutePosition computes a widget's absolute X,Y position on the // window on screen by crawling its parent widget tree. diff --git a/label.go b/label.go index 1255f24..a654116 100644 --- a/label.go +++ b/label.go @@ -101,11 +101,6 @@ func (w *Label) Compute(e render.Engine) { H: maxRect.H + (padY * 2), }) } - - w.MoveTo(render.Point{ - X: maxRect.X + w.BoxThickness(1), - Y: maxRect.Y + w.BoxThickness(1), - }) } // Present the label widget. diff --git a/main_window.go b/main_window.go index 37f054a..3678c9d 100644 --- a/main_window.go +++ b/main_window.go @@ -7,6 +7,7 @@ import ( "time" "git.kirsle.net/go/render" + "git.kirsle.net/go/render/event" "git.kirsle.net/go/render/sdl" ) @@ -17,20 +18,22 @@ var ( // MainWindow is the parent window of a UI application. type MainWindow struct { - Engine render.Engine - supervisor *Supervisor - frame *Frame - w int - h int + Engine render.Engine + supervisor *Supervisor + frame *Frame + loopCallbacks []func(*event.State) + w int + h int } // NewMainWindow initializes the MainWindow. You should probably only have one // of these per application. func NewMainWindow(title string) (*MainWindow, error) { mw := &MainWindow{ - w: 800, - h: 600, - supervisor: NewSupervisor(), + w: 800, + h: 600, + supervisor: NewSupervisor(), + loopCallbacks: []func(*event.State){}, } mw.Engine = sdl.New( @@ -64,6 +67,11 @@ func (mw *MainWindow) Pack(w Widget, pack Pack) { mw.frame.Pack(w, pack) } +// Frame returns the window's main frame, if needed. +func (mw *MainWindow) Frame() *Frame { + return mw.frame +} + // resized handles the window being resized. func (mw *MainWindow) resized() { mw.frame.Resize(render.Rect{ @@ -82,6 +90,14 @@ func (mw *MainWindow) Present() { mw.supervisor.Present(mw.Engine) } +// OnLoop registers a function to be called on every loop of the main window. +// This enables your application to register global event handlers or whatnot. +// The function is called between the event polling and the updating of any UI +// elements. +func (mw *MainWindow) OnLoop(callback func(*event.State)) { + mw.loopCallbacks = append(mw.loopCallbacks, callback) +} + // MainLoop starts the main event loop and blocks until there's an error. func (mw *MainWindow) MainLoop() error { for true { @@ -114,11 +130,16 @@ func (mw *MainWindow) Loop() error { } } + // Ping any loop callbacks. + for _, cb := range mw.loopCallbacks { + cb(ev) + } + mw.frame.Compute(mw.Engine) // Render the child widgets. mw.supervisor.Loop(ev) - mw.supervisor.Present(mw.Engine) + mw.frame.Present(mw.Engine, mw.frame.Point()) mw.Engine.Present() // Delay to maintain target frames per second. diff --git a/supervisor.go b/supervisor.go index fe7f7c9..76b68ee 100644 --- a/supervisor.go +++ b/supervisor.go @@ -2,6 +2,7 @@ package ui import ( "errors" + "fmt" "sync" "git.kirsle.net/go/render" @@ -165,7 +166,7 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl for child := range s.Widgets() { var ( w = child.widget - P = w.Point() + P = AbsolutePosition(w) S = w.Size() P2 = render.Point{ X: P.X + S.W, @@ -204,10 +205,11 @@ func (s *Supervisor) Present(e render.Engine) { s.lock.RLock() defer s.lock.RUnlock() - for child := range s.Widgets() { - var w = child.widget - w.Present(e, w.Point()) - } + fmt.Println("!!! ui.Supervisor.Present() is deprecated") + // for child := range s.Widgets() { + // var w = child.widget + // w.Present(e, w.Point()) + // } } // Add a widget to be supervised. diff --git a/widget.go b/widget.go index b8c52d5..97681d7 100644 --- a/widget.go +++ b/widget.go @@ -1,8 +1,6 @@ package ui import ( - "fmt" - "git.kirsle.net/go/render" "git.kirsle.net/go/ui/theme" ) @@ -67,8 +65,8 @@ type Widget interface { // 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 - Children() []Widget // for containers to return their children + SetParent(parent Widget) // for the container to assign itself the parent + Children() []Widget // for containers to return their children // Run any render computations; by the end the widget must know its // Width and Height. For example the Label widget will render itself onto @@ -254,18 +252,13 @@ func (w *BaseWidget) ResizeBy(v render.Rect) { // ResizeAuto sets the size of the widget but doesn't set the fixedSize flag. func (w *BaseWidget) ResizeAuto(v render.Rect) { - if w.ID() == "Frame" { - fmt.Printf("%s: ResizeAuto Called: %+v\n", - w.ID(), - v, - ) - } w.width = v.W w.height = v.H } // BoxThickness returns the full sum of the padding, border and outline. -// m = multiplier, i.e., 1 or 2 +// m = multiplier, i.e., 1 or 2. If m=1 this returns the box thickness of one +// edge of the widget, if m=2 it would account for both edges of the widget. func (w *BaseWidget) BoxThickness(m int) int { if m == 0 { m = 1 @@ -279,10 +272,10 @@ func (w *BaseWidget) Parent() (Widget, bool) { return w.parent, w.hasParent } -// Adopt sets the widget's parent. This function is called by container +// SetParent 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) { +func (w *BaseWidget) SetParent(parent Widget) { if parent == nil { w.hasParent = false w.parent = nil diff --git a/window.go b/window.go index 2073c18..c57d913 100644 --- a/window.go +++ b/window.go @@ -66,6 +66,9 @@ func NewWindow(title string) *Window { }) w.content = content + // Set up parent/child relationships + w.body.SetParent(w) + return w }