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.)
This commit is contained in:
parent
9d6b172878
commit
4ba563d48d
22
README.md
22
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.
|
||||
|
|
|
@ -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()
|
||||
|
|
19
debug.go
19
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() {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
5
label.go
5
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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
19
widget.go
19
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<Window Body>" {
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue
Block a user