WIP: Broken UI - Reworking Frame Pack

This commit is contained in:
Noah 2019-04-19 13:51:27 -07:00
parent fb8f4b1029
commit 8fb579e66e
9 changed files with 309 additions and 116 deletions

9
lib/ui/constants.go Normal file
View File

@ -0,0 +1,9 @@
package ui
// Constants for the UI toolkit.
const (
// Pack.Fill values.
FillBoth = "both"
FillX = "x"
FillY = "y"
)

104
lib/ui/eg/layout/main.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"time"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/render/sdl"
"git.kirsle.net/apps/doodle/lib/ui"
)
var TargetFPS = 1000 / 60
func main() {
engine := sdl.New("Test Layout GUI", 1024, 768)
if err := engine.Setup(); err != nil {
panic(err)
}
super := ui.NewSupervisor()
window := ui.NewWindow("Test Window")
window.Configure(ui.Config{
Width: 750,
Height: 400,
Background: render.Grey,
})
window.MoveTo(render.NewPoint(80, 80))
leftPanel := ui.NewFrame("Left Panel")
leftPanel.Configure(ui.Config{
// AutoResize: true,
Width: 200,
Background: render.SkyBlue,
BorderStyle: ui.BorderRaised,
BorderSize: 2,
})
window.Pack(leftPanel, ui.Pack{
Side: ui.Left,
Fill: ui.FillY,
Expand: true,
})
body := ui.NewFrame("Body Panel")
body.Configure(ui.Config{
Background: render.RGBA(255, 0, 0, 64),
BorderStyle: ui.BorderSunken,
BorderSize: 2,
})
window.Pack(body, ui.Pack{
Side: ui.Left,
Expand: true,
})
label1 := ui.NewLabel(ui.Label{
Text: "Hello world!",
Font: render.Text{
Size: 24,
Color: render.Red,
Stroke: render.Purple,
},
})
body.Pack(label1, ui.Pack{
Side: ui.Top,
})
window.Frame().SetBackground(render.Yellow)
window.Compute(engine)
// window.Present(engine, window.Point())
super.Add(window)
super.MainLoop(engine)
//
for true {
start := time.Now()
engine.Clear(render.White)
// poll for events
ev, err := engine.Poll()
if err != nil {
panic(err)
}
// escape key to close the window
if ev.EscapeKey.Now {
break
}
super.Loop(ev)
window.Compute(engine)
window.Present(engine, window.Point())
engine.Present()
// Delay to maintain the target frames per second.
var delay uint32
elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under
delay = uint32(TargetFPS - int(tmp))
}
engine.Delay(delay)
}
}

View File

@ -10,7 +10,7 @@ import (
type Frame struct {
Name string
BaseWidget
packs map[Anchor][]packedWidget
packs map[Side][]packedWidget
widgets []Widget
}
@ -18,7 +18,7 @@ type Frame struct {
func NewFrame(name string) *Frame {
w := &Frame{
Name: name,
packs: map[Anchor][]packedWidget{},
packs: map[Side][]packedWidget{},
widgets: []Widget{},
}
w.IDFunc(func() string {
@ -32,7 +32,7 @@ func NewFrame(name string) *Frame {
// Setup ensures all the Frame's data is initialized and not null.
func (w *Frame) Setup() {
if w.packs == nil {
w.packs = map[Anchor][]packedWidget{}
w.packs = map[Side][]packedWidget{}
}
if w.widgets == nil {
w.widgets = []Widget{}

View File

@ -1,6 +1,8 @@
package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/lib/render"
)
@ -9,12 +11,13 @@ type Pack struct {
// Side of the parent to anchor the position to, like N, SE, W. Default
// is Center.
Anchor Anchor
Side Side
// If the widget is smaller than its allocated space, grow the widget
// to fill its space in the Frame.
Fill bool
FillX bool
FillY bool
Fill string // "x", "y", "both" or "" for none
fillX bool
fillY bool
Padding int32 // Equal padding on X and Y.
PadX int32
@ -30,8 +33,8 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
}
// Initialize the pack list for this anchor?
if _, ok := w.packs[C.Anchor]; !ok {
w.packs[C.Anchor] = []packedWidget{}
if _, ok := w.packs[C.Side]; !ok {
w.packs[C.Side] = []packedWidget{}
}
// Padding: if the user only provided Padding add it to both
@ -40,16 +43,14 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
C.PadX += C.Padding
C.PadY += C.Padding
// Fill: true implies both directions.
if C.Fill {
C.FillX = true
C.FillY = true
}
// Cache the full X and Y booleans.
C.fillX = C.Fill == FillX || C.Fill == FillBoth
C.fillY = C.Fill == FillY || C.Fill == FillBoth
// 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.Side] = append(w.packs[C.Side], packedWidget{
widget: child,
pack: C,
})
@ -72,10 +73,10 @@ func (w *Frame) computePacked(e render.Engine) {
expanded = []packedWidget{}
)
// Iterate through all anchored directions and compute how much space to
// Iterate through all packed sides and compute how much space to
// reserve to contain all of their widgets.
for anchor := AnchorMin; anchor <= AnchorMax; anchor++ {
if _, ok := w.packs[anchor]; !ok {
for side := SideMin; side <= SideMax; side++ {
if _, ok := w.packs[side]; !ok {
continue
}
@ -86,15 +87,15 @@ func (w *Frame) computePacked(e render.Engine) {
xDirection int32 = 1
)
if anchor.IsSouth() { // TODO: these need tuning
if side == Bottom { // TODO: these need tuning
y = frameSize.H - w.BoxThickness(4)
yDirection = -1 * w.BoxThickness(4) // parent + child BoxThickness(1) = 2
} else if anchor == E {
} else if side == Right {
x = frameSize.W - w.BoxThickness(4)
xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
}
for _, packedWidget := range w.packs[anchor] {
for _, packedWidget := range w.packs[side] {
child := packedWidget.widget
pack := packedWidget.pack
@ -121,19 +122,17 @@ func (w *Frame) computePacked(e render.Engine) {
maxHeight = yStep + size.H + (pack.PadY * 2)
}
if anchor.IsSouth() {
if side == Bottom {
y -= size.H - pack.PadY
}
if anchor.IsEast() {
} else if side == Right {
x -= size.W - pack.PadX
}
child.MoveTo(render.NewPoint(x, y))
if anchor.IsNorth() {
if side == Top {
y += size.H + pack.PadY
}
if anchor == W {
} else if side == Left {
x += size.W + pack.PadX
}
@ -147,6 +146,13 @@ 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 w.fixedWidth > 0 {
computedSize.W = w.fixedWidth
}
if w.fixedHeight > 0 {
computedSize.H = w.fixedHeight
}
if len(expanded) > 0 && !frameSize.IsZero() && frameSize.Bigger(computedSize) {
// Divy up the size available.
growBy := render.Rect{
@ -154,7 +160,8 @@ func (w *Frame) computePacked(e render.Engine) {
H: ((frameSize.H - computedSize.H) / int32(len(expanded))), // - w.BoxThickness(2),
}
for _, pw := range expanded {
pw.widget.ResizeBy(growBy)
fmt.Printf("expand %s by %s (comp size %s)\n", pw.widget.ID(), growBy, computedSize)
pw.widget.ResizeAuto(growBy)
pw.widget.Compute(e)
}
}
@ -172,7 +179,7 @@ func (w *Frame) computePacked(e render.Engine) {
}
}
// Rescan all the widgets in this anchor to re-center them
// Rescan all the widgets in this side to re-center them
// in their space.
innerFrameSize := render.NewRect(
frameSize.W-w.BoxThickness(2),
@ -189,8 +196,8 @@ func (w *Frame) computePacked(e render.Engine) {
moved bool
)
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
if pack.FillX && resize.W < innerFrameSize.W {
if pack.Side == Top || pack.Side == Bottom {
if pack.fillX && resize.W < innerFrameSize.W {
resize.W = innerFrameSize.W - w.BoxThickness(2)
resized = true
}
@ -205,8 +212,8 @@ func (w *Frame) computePacked(e render.Engine) {
moved = true
}
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
if pack.FillY && resize.H < innerFrameSize.H {
} else if pack.Side == Left || pack.Side == Right {
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
@ -225,11 +232,12 @@ func (w *Frame) computePacked(e render.Engine) {
moved = true
}
} else {
panic("unsupported pack.Anchor")
panic("unsupported pack.Side")
}
if resized && size != resize {
child.Resize(resize)
fmt.Printf("fill: resize %s to %s\n", child.ID(), resize)
child.ResizeAuto(resize)
child.Compute(e)
}
if moved {
@ -238,7 +246,7 @@ func (w *Frame) computePacked(e render.Engine) {
}
// if !w.FixedSize() {
w.Resize(render.NewRect(
w.ResizeAuto(render.NewRect(
frameSize.W-w.BoxThickness(2),
frameSize.H-w.BoxThickness(2),
))
@ -248,7 +256,10 @@ func (w *Frame) computePacked(e render.Engine) {
// Anchor is a cardinal direction.
type Anchor uint8
// Anchor values.
// Side of a parent widget to pack children against.
type Side uint8
// Anchor and Side constants.
const (
Center Anchor = iota
N
@ -259,12 +270,20 @@ const (
SW
W
NW
Top Side = iota
Left
Right
Bottom
)
// Range of Anchor values.
// Range of Anchor and Side values.
const (
AnchorMin = Center
AnchorMax = NW
SideMin = Top
SideMax = Bottom
)
// IsNorth returns if the anchor is N, NE or NW.

View File

@ -91,7 +91,7 @@ func (w *Label) Compute(e render.Engine) {
)
if !w.FixedSize() {
w.resizeAuto(render.Rect{
w.ResizeAuto(render.Rect{
W: maxRect.W + (padX * 2),
H: maxRect.H + (padY * 2),
})

View File

@ -3,6 +3,7 @@ package ui
import (
"errors"
"sync"
"time"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
@ -30,6 +31,7 @@ const (
// vicinity.
type Supervisor struct {
lock sync.RWMutex
targetFPS int
serial int // ID number of each widget added in order
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
hovering map[int]interface{} // map of widgets under the cursor
@ -46,6 +48,7 @@ type WidgetSlot struct {
// NewSupervisor creates a supervisor.
func NewSupervisor() *Supervisor {
return &Supervisor{
targetFPS: 1000 / 60,
widgets: map[int]WidgetSlot{},
hovering: map[int]interface{}{},
clicked: map[int]interface{}{},
@ -75,6 +78,43 @@ var (
ErrStopPropagation = errors.New("stop all event propagation")
)
// MainLoop starts the UI main loop, for UI-only applications.
func (s *Supervisor) MainLoop(e render.Engine) error {
for true {
start := time.Now()
e.Clear(render.Green)
// Poll for events.
ev, err := e.Poll()
if err != nil {
return err
}
// TODO: escape key to exit the main loop
if ev.EscapeKey.Now {
return nil
}
s.Loop(ev)
// Render the widgets under our care.
s.Present(e)
// Commit the pixels to screen.
e.Present()
// Delay to maintain the target FPS.
var delay uint32
elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond
if s.targetFPS-int(tmp) > 0 {
delay = uint32(s.targetFPS - int(tmp))
}
e.Delay(delay)
}
return nil
}
// Loop to check events and pass them to managed widgets.
//
// Useful errors returned by this may be:
@ -211,12 +251,15 @@ func (s *Supervisor) Present(e render.Engine) {
}
// Add a widget to be supervised.
func (s *Supervisor) Add(w Widget) {
func (s *Supervisor) Add(w ...Widget) {
s.lock.Lock()
for _, child := range w {
s.widgets[s.serial] = WidgetSlot{
id: s.serial,
widget: w,
widget: child,
}
s.serial++
}
s.lock.Unlock()
}

View File

@ -1,6 +1,8 @@
package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui/theme"
)
@ -26,9 +28,11 @@ type Widget interface {
MoveBy(render.Point)
Size() render.Rect // Return the Width and Height of the widget.
FixedSize() bool // Return whether the size is fixed (true) or automatic (false)
FixedSizes() (int32, int32) // Return whether the W and H are fixed in size
BoxSize() render.Rect // Return the full size including the border and outline.
Resize(render.Rect)
ResizeBy(render.Rect)
ResizeAuto(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(Event, func(render.Point))
@ -107,6 +111,8 @@ type BaseWidget struct {
hidden bool
width int32
height int32
fixedWidth int32 // values manually configured by the user,
fixedHeight int32 // and do not change
point render.Point
margin int32
background render.Color
@ -152,9 +158,11 @@ func (w *BaseWidget) Configure(c Config) {
w.fixedSize = !c.AutoResize
if c.Width != 0 {
w.width = c.Width
w.fixedWidth = w.width
}
if c.Height != 0 {
w.height = c.Height
w.fixedHeight = w.height
}
}
@ -235,6 +243,12 @@ func (w *BaseWidget) FixedSize() bool {
return w.fixedSize
}
// FixedSizes returns whether the widget's Width or Height were manually set
// to hard-coded values, respectively.
func (w *BaseWidget) FixedSizes() (int32, int32) {
return w.fixedWidth, w.fixedHeight
}
// Resize sets the size of the widget to the .W and .H attributes of a rect.
func (w *BaseWidget) Resize(v render.Rect) {
w.fixedSize = true
@ -249,11 +263,18 @@ func (w *BaseWidget) ResizeBy(v render.Rect) {
w.height += v.H
}
// resizeAuto sets the size of the widget but doesn't set the fixedSize flag.
func (w *BaseWidget) resizeAuto(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.fixedWidth == 0 {
w.width = v.W
}
if w.fixedHeight == 0 {
w.height = v.H
}
fmt.Printf("ResizeAuto(%s): fixed size=%d,%d new size=%s\n",
w.ID(), w.fixedWidth, w.fixedHeight, w.Size(),
)
}
// BoxThickness returns the full sum of the padding, border and outline.
// m = multiplier, i.e., 1 or 2

View File

@ -50,8 +50,8 @@ func NewWindow(title string) *Window {
Background: render.Blue,
})
w.body.Pack(titleBar, Pack{
Anchor: N,
Fill: true,
Side: Top,
Fill: FillX,
})
w.titleBar = titleBar
@ -61,8 +61,8 @@ func NewWindow(title string) *Window {
Background: render.Grey,
})
w.body.Pack(content, Pack{
Anchor: N,
Fill: true,
Side: Top,
Fill: FillBoth,
})
w.content = content
@ -81,6 +81,11 @@ func (w *Window) TitleBar() *Label {
return w.titleBar
}
// Frame returns the content frame of the window.
func (w *Window) Frame() *Frame {
return w.content
}
// Configure the widget. Color and style changes are passed down to the inner
// content frame of the window.
func (w *Window) Configure(C Config) {
@ -100,7 +105,23 @@ func (w *Window) ConfigureTitle(C Config) {
// Compute the window.
func (w *Window) Compute(e render.Engine) {
var size = w.Size()
w.body.Compute(e)
// Assign a manual Height to the title bar using its naturally computed
// height, but leave the Width empty so the frame packer can stretch it
// horizontally.
w.titleBar.Configure(Config{
Height: w.titleBar.Size().H,
})
// Shrink down the content frame to leave room for the title bar.
w.content.Resize(render.Rect{
W: size.W - w.BoxThickness(2) - w.titleBar.BoxThickness(2),
H: size.H - w.titleBar.Size().H - w.BoxThickness(4) -
((w.titleBar.Font.Padding + w.titleBar.Font.PadY) * 2),
})
}
// Present the window.

View File

@ -16,7 +16,7 @@ type GUITestScene struct {
// Private widgets.
Frame *ui.Frame
Window *ui.Frame
Window *ui.Window
}
// Name of the scene.
@ -28,8 +28,7 @@ func (s *GUITestScene) Name() string {
func (s *GUITestScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor()
window := ui.NewFrame("window")
s.Window = window
window := ui.NewWindow("Widget Toolkit")
window.Configure(ui.Config{
Width: 750,
Height: 450,
@ -37,33 +36,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
BorderStyle: ui.BorderRaised,
BorderSize: 2,
})
// Title Bar
titleBar := ui.NewLabel(ui.Label{
Text: "Widget Toolkit",
Font: render.Text{
Size: 12,
Color: render.White,
Stroke: render.DarkBlue,
},
})
titleBar.Configure(ui.Config{
Background: render.Blue,
})
window.Pack(titleBar, ui.Pack{
Anchor: ui.N,
Fill: true,
})
// Window Body
body := ui.NewFrame("Window Body")
body.Configure(ui.Config{
Background: render.Yellow,
})
window.Pack(body, ui.Pack{
Anchor: ui.N,
Expand: true,
})
s.Window = window
// Left Frame
leftFrame := ui.NewFrame("Left Frame")
@ -73,8 +46,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
BorderSize: 4,
Width: 100,
})
body.Pack(leftFrame, ui.Pack{
Anchor: ui.W,
window.Pack(leftFrame, ui.Pack{
Side: ui.Left,
FillY: true,
})
@ -89,7 +62,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
})
s.Supervisor.Add(btn)
leftFrame.Pack(btn, ui.Pack{
Anchor: ui.N,
Side: ui.Top,
FillX: true,
PadY: 2,
})
@ -101,8 +74,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Background: render.White,
BorderSize: 0,
})
body.Pack(frame, ui.Pack{
Anchor: ui.W,
window.Pack(frame, ui.Pack{
Side: ui.Left,
Expand: true,
Fill: true,
})
@ -115,8 +88,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
BorderSize: 2,
Width: 80,
})
body.Pack(rightFrame, ui.Pack{
Anchor: ui.W,
window.Pack(rightFrame, ui.Pack{
Side: ui.Right,
Fill: true,
})
@ -139,7 +112,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
d.Flash("%s clicked", btn)
})
rowFrame.Pack(btn, ui.Pack{
Anchor: ui.W,
Side: ui.Left,
Expand: true,
FillX: true,
})
@ -147,7 +120,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
})(row, col, rowFrame)
}
rightFrame.Pack(rowFrame, ui.Pack{
Anchor: ui.N,
Side: ui.Top,
Fill: true,
})
}
@ -160,7 +133,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Color: render.Black,
},
}), ui.Pack{
Anchor: ui.NW,
Side: ui.Top,
Anchor: ui.W,
Padding: 2,
})
@ -172,7 +146,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
}),
)
frame.Pack(cb, ui.Pack{
Anchor: ui.NW,
Side: ui.Top,
Anchor: ui.W,
Padding: 4,
})
cb.Supervise(s.Supervisor)
@ -184,7 +159,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Color: render.Red,
},
}), ui.Pack{
Anchor: ui.SE,
Side: ui.Bottom,
Anchor: ui.E,
Padding: 8,
})
frame.Pack(ui.NewLabel(ui.Label{
@ -194,7 +170,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Color: render.Blue,
},
}), ui.Pack{
Anchor: ui.SE,
Side: ui.Bottom,
Anchor: ui.E,
Padding: 8,
})
@ -204,7 +181,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Background: render.Grey,
})
window.Pack(btnFrame, ui.Pack{
Anchor: ui.N,
Side: ui.Top,
})
button1 := ui.NewButton("Button1", ui.NewLabel(ui.Label{
@ -228,13 +205,12 @@ func (s *GUITestScene) Setup(d *Doodle) error {
})
})
var align = ui.W
btnFrame.Pack(button1, ui.Pack{
Anchor: align,
Side: ui.Left,
Padding: 20,
})
btnFrame.Pack(button2, ui.Pack{
Anchor: align,
Side: ui.Left,
Padding: 20,
})