Compare commits
1 Commits
master
...
stash-ui-r
Author | SHA1 | Date | |
---|---|---|---|
8fb579e66e |
9
lib/ui/constants.go
Normal file
9
lib/ui/constants.go
Normal 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
104
lib/ui/eg/layout/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import (
|
||||||
type Frame struct {
|
type Frame struct {
|
||||||
Name string
|
Name string
|
||||||
BaseWidget
|
BaseWidget
|
||||||
packs map[Anchor][]packedWidget
|
packs map[Side][]packedWidget
|
||||||
widgets []Widget
|
widgets []Widget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ type Frame struct {
|
||||||
func NewFrame(name string) *Frame {
|
func NewFrame(name string) *Frame {
|
||||||
w := &Frame{
|
w := &Frame{
|
||||||
Name: name,
|
Name: name,
|
||||||
packs: map[Anchor][]packedWidget{},
|
packs: map[Side][]packedWidget{},
|
||||||
widgets: []Widget{},
|
widgets: []Widget{},
|
||||||
}
|
}
|
||||||
w.IDFunc(func() string {
|
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.
|
// Setup ensures all the Frame's data is initialized and not null.
|
||||||
func (w *Frame) Setup() {
|
func (w *Frame) Setup() {
|
||||||
if w.packs == nil {
|
if w.packs == nil {
|
||||||
w.packs = map[Anchor][]packedWidget{}
|
w.packs = map[Side][]packedWidget{}
|
||||||
}
|
}
|
||||||
if w.widgets == nil {
|
if w.widgets == nil {
|
||||||
w.widgets = []Widget{}
|
w.widgets = []Widget{}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"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
|
// Side of the parent to anchor the position to, like N, SE, W. Default
|
||||||
// is Center.
|
// is Center.
|
||||||
Anchor Anchor
|
Anchor Anchor
|
||||||
|
Side Side
|
||||||
|
|
||||||
// If the widget is smaller than its allocated space, grow the widget
|
// If the widget is smaller than its allocated space, grow the widget
|
||||||
// to fill its space in the Frame.
|
// to fill its space in the Frame.
|
||||||
Fill bool
|
Fill string // "x", "y", "both" or "" for none
|
||||||
FillX bool
|
fillX bool
|
||||||
FillY bool
|
fillY bool
|
||||||
|
|
||||||
Padding int32 // Equal padding on X and Y.
|
Padding int32 // Equal padding on X and Y.
|
||||||
PadX int32
|
PadX int32
|
||||||
|
@ -30,8 +33,8 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the pack list for this anchor?
|
// Initialize the pack list for this anchor?
|
||||||
if _, ok := w.packs[C.Anchor]; !ok {
|
if _, ok := w.packs[C.Side]; !ok {
|
||||||
w.packs[C.Anchor] = []packedWidget{}
|
w.packs[C.Side] = []packedWidget{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Padding: if the user only provided Padding add it to both
|
// 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.PadX += C.Padding
|
||||||
C.PadY += C.Padding
|
C.PadY += C.Padding
|
||||||
|
|
||||||
// Fill: true implies both directions.
|
// Cache the full X and Y booleans.
|
||||||
if C.Fill {
|
C.fillX = C.Fill == FillX || C.Fill == FillBoth
|
||||||
C.FillX = true
|
C.fillY = C.Fill == FillY || C.Fill == FillBoth
|
||||||
C.FillY = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adopt the child widget so it can access the Frame.
|
// Adopt the child widget so it can access the Frame.
|
||||||
child.Adopt(w)
|
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,
|
widget: child,
|
||||||
pack: C,
|
pack: C,
|
||||||
})
|
})
|
||||||
|
@ -72,10 +73,10 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
expanded = []packedWidget{}
|
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.
|
// reserve to contain all of their widgets.
|
||||||
for anchor := AnchorMin; anchor <= AnchorMax; anchor++ {
|
for side := SideMin; side <= SideMax; side++ {
|
||||||
if _, ok := w.packs[anchor]; !ok {
|
if _, ok := w.packs[side]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,15 +87,15 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
xDirection int32 = 1
|
xDirection int32 = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
if anchor.IsSouth() { // TODO: these need tuning
|
if side == Bottom { // TODO: these need tuning
|
||||||
y = frameSize.H - w.BoxThickness(4)
|
y = frameSize.H - w.BoxThickness(4)
|
||||||
yDirection = -1 * w.BoxThickness(4) // parent + child BoxThickness(1) = 2
|
yDirection = -1 * w.BoxThickness(4) // parent + child BoxThickness(1) = 2
|
||||||
} else if anchor == E {
|
} else if side == Right {
|
||||||
x = frameSize.W - w.BoxThickness(4)
|
x = frameSize.W - w.BoxThickness(4)
|
||||||
xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
|
xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, packedWidget := range w.packs[anchor] {
|
for _, packedWidget := range w.packs[side] {
|
||||||
|
|
||||||
child := packedWidget.widget
|
child := packedWidget.widget
|
||||||
pack := packedWidget.pack
|
pack := packedWidget.pack
|
||||||
|
@ -121,19 +122,17 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
maxHeight = yStep + size.H + (pack.PadY * 2)
|
maxHeight = yStep + size.H + (pack.PadY * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if anchor.IsSouth() {
|
if side == Bottom {
|
||||||
y -= size.H - pack.PadY
|
y -= size.H - pack.PadY
|
||||||
}
|
} else if side == Right {
|
||||||
if anchor.IsEast() {
|
|
||||||
x -= size.W - pack.PadX
|
x -= size.W - pack.PadX
|
||||||
}
|
}
|
||||||
|
|
||||||
child.MoveTo(render.NewPoint(x, y))
|
child.MoveTo(render.NewPoint(x, y))
|
||||||
|
|
||||||
if anchor.IsNorth() {
|
if side == Top {
|
||||||
y += size.H + pack.PadY
|
y += size.H + pack.PadY
|
||||||
}
|
} else if side == Left {
|
||||||
if anchor == W {
|
|
||||||
x += size.W + pack.PadX
|
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
|
// If we have extra space in the Frame and any expanding widgets, let the
|
||||||
// expanding widgets grow and share the remaining space.
|
// expanding widgets grow and share the remaining space.
|
||||||
computedSize := render.NewRect(maxWidth, maxHeight)
|
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) {
|
if len(expanded) > 0 && !frameSize.IsZero() && frameSize.Bigger(computedSize) {
|
||||||
// Divy up the size available.
|
// Divy up the size available.
|
||||||
growBy := render.Rect{
|
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),
|
H: ((frameSize.H - computedSize.H) / int32(len(expanded))), // - w.BoxThickness(2),
|
||||||
}
|
}
|
||||||
for _, pw := range expanded {
|
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)
|
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.
|
// in their space.
|
||||||
innerFrameSize := render.NewRect(
|
innerFrameSize := render.NewRect(
|
||||||
frameSize.W-w.BoxThickness(2),
|
frameSize.W-w.BoxThickness(2),
|
||||||
|
@ -189,8 +196,8 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
moved bool
|
moved bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
|
if pack.Side == Top || pack.Side == Bottom {
|
||||||
if pack.FillX && resize.W < innerFrameSize.W {
|
if pack.fillX && resize.W < innerFrameSize.W {
|
||||||
resize.W = innerFrameSize.W - w.BoxThickness(2)
|
resize.W = innerFrameSize.W - w.BoxThickness(2)
|
||||||
resized = true
|
resized = true
|
||||||
}
|
}
|
||||||
|
@ -205,8 +212,8 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
|
|
||||||
moved = true
|
moved = true
|
||||||
}
|
}
|
||||||
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
|
} else if pack.Side == Left || pack.Side == Right {
|
||||||
if pack.FillY && resize.H < innerFrameSize.H {
|
if pack.fillY && resize.H < innerFrameSize.H {
|
||||||
resize.H = innerFrameSize.H - w.BoxThickness(2) // BoxThickness(2) for parent + child
|
resize.H = innerFrameSize.H - w.BoxThickness(2) // BoxThickness(2) for parent + child
|
||||||
// point.Y -= (w.BoxThickness(4) + child.BoxThickness(2))
|
// point.Y -= (w.BoxThickness(4) + child.BoxThickness(2))
|
||||||
moved = true
|
moved = true
|
||||||
|
@ -225,11 +232,12 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
moved = true
|
moved = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
panic("unsupported pack.Anchor")
|
panic("unsupported pack.Side")
|
||||||
}
|
}
|
||||||
|
|
||||||
if resized && size != resize {
|
if resized && size != resize {
|
||||||
child.Resize(resize)
|
fmt.Printf("fill: resize %s to %s\n", child.ID(), resize)
|
||||||
|
child.ResizeAuto(resize)
|
||||||
child.Compute(e)
|
child.Compute(e)
|
||||||
}
|
}
|
||||||
if moved {
|
if moved {
|
||||||
|
@ -238,7 +246,7 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if !w.FixedSize() {
|
// if !w.FixedSize() {
|
||||||
w.Resize(render.NewRect(
|
w.ResizeAuto(render.NewRect(
|
||||||
frameSize.W-w.BoxThickness(2),
|
frameSize.W-w.BoxThickness(2),
|
||||||
frameSize.H-w.BoxThickness(2),
|
frameSize.H-w.BoxThickness(2),
|
||||||
))
|
))
|
||||||
|
@ -248,7 +256,10 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
// Anchor is a cardinal direction.
|
// Anchor is a cardinal direction.
|
||||||
type Anchor uint8
|
type Anchor uint8
|
||||||
|
|
||||||
// Anchor values.
|
// Side of a parent widget to pack children against.
|
||||||
|
type Side uint8
|
||||||
|
|
||||||
|
// Anchor and Side constants.
|
||||||
const (
|
const (
|
||||||
Center Anchor = iota
|
Center Anchor = iota
|
||||||
N
|
N
|
||||||
|
@ -259,12 +270,20 @@ const (
|
||||||
SW
|
SW
|
||||||
W
|
W
|
||||||
NW
|
NW
|
||||||
|
|
||||||
|
Top Side = iota
|
||||||
|
Left
|
||||||
|
Right
|
||||||
|
Bottom
|
||||||
)
|
)
|
||||||
|
|
||||||
// Range of Anchor values.
|
// Range of Anchor and Side values.
|
||||||
const (
|
const (
|
||||||
AnchorMin = Center
|
AnchorMin = Center
|
||||||
AnchorMax = NW
|
AnchorMax = NW
|
||||||
|
|
||||||
|
SideMin = Top
|
||||||
|
SideMax = Bottom
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsNorth returns if the anchor is N, NE or NW.
|
// IsNorth returns if the anchor is N, NE or NW.
|
||||||
|
|
|
@ -91,7 +91,7 @@ func (w *Label) Compute(e render.Engine) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if !w.FixedSize() {
|
if !w.FixedSize() {
|
||||||
w.resizeAuto(render.Rect{
|
w.ResizeAuto(render.Rect{
|
||||||
W: maxRect.W + (padX * 2),
|
W: maxRect.W + (padX * 2),
|
||||||
H: maxRect.H + (padY * 2),
|
H: maxRect.H + (padY * 2),
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/events"
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
@ -29,12 +30,13 @@ const (
|
||||||
// interaction events such as mouse hovers and clicks in their general
|
// interaction events such as mouse hovers and clicks in their general
|
||||||
// vicinity.
|
// vicinity.
|
||||||
type Supervisor struct {
|
type Supervisor struct {
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
serial int // ID number of each widget added in order
|
targetFPS int
|
||||||
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
|
serial int // ID number of each widget added in order
|
||||||
hovering map[int]interface{} // map of widgets under the cursor
|
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
|
||||||
clicked map[int]interface{} // map of widgets being clicked
|
hovering map[int]interface{} // map of widgets under the cursor
|
||||||
dd *DragDrop
|
clicked map[int]interface{} // map of widgets being clicked
|
||||||
|
dd *DragDrop
|
||||||
}
|
}
|
||||||
|
|
||||||
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
||||||
|
@ -46,10 +48,11 @@ type WidgetSlot struct {
|
||||||
// NewSupervisor creates a supervisor.
|
// NewSupervisor creates a supervisor.
|
||||||
func NewSupervisor() *Supervisor {
|
func NewSupervisor() *Supervisor {
|
||||||
return &Supervisor{
|
return &Supervisor{
|
||||||
widgets: map[int]WidgetSlot{},
|
targetFPS: 1000 / 60,
|
||||||
hovering: map[int]interface{}{},
|
widgets: map[int]WidgetSlot{},
|
||||||
clicked: map[int]interface{}{},
|
hovering: map[int]interface{}{},
|
||||||
dd: NewDragDrop(),
|
clicked: map[int]interface{}{},
|
||||||
|
dd: NewDragDrop(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +78,43 @@ var (
|
||||||
ErrStopPropagation = errors.New("stop all event propagation")
|
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.
|
// Loop to check events and pass them to managed widgets.
|
||||||
//
|
//
|
||||||
// Useful errors returned by this may be:
|
// Useful errors returned by this may be:
|
||||||
|
@ -211,12 +251,15 @@ func (s *Supervisor) Present(e render.Engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a widget to be supervised.
|
// Add a widget to be supervised.
|
||||||
func (s *Supervisor) Add(w Widget) {
|
func (s *Supervisor) Add(w ...Widget) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
s.widgets[s.serial] = WidgetSlot{
|
|
||||||
id: s.serial,
|
for _, child := range w {
|
||||||
widget: w,
|
s.widgets[s.serial] = WidgetSlot{
|
||||||
|
id: s.serial,
|
||||||
|
widget: child,
|
||||||
|
}
|
||||||
|
s.serial++
|
||||||
}
|
}
|
||||||
s.serial++
|
|
||||||
s.lock.Unlock()
|
s.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||||
)
|
)
|
||||||
|
@ -24,11 +26,13 @@ type Widget interface {
|
||||||
Point() render.Point
|
Point() render.Point
|
||||||
MoveTo(render.Point)
|
MoveTo(render.Point)
|
||||||
MoveBy(render.Point)
|
MoveBy(render.Point)
|
||||||
Size() render.Rect // Return the Width and Height of the widget.
|
Size() render.Rect // Return the Width and Height of the widget.
|
||||||
FixedSize() bool // Return whether the size is fixed (true) or automatic (false)
|
FixedSize() bool // Return whether the size is fixed (true) or automatic (false)
|
||||||
BoxSize() render.Rect // Return the full size including the border and outline.
|
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)
|
Resize(render.Rect)
|
||||||
ResizeBy(render.Rect)
|
ResizeBy(render.Rect)
|
||||||
|
ResizeAuto(render.Rect)
|
||||||
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
||||||
|
|
||||||
Handle(Event, func(render.Point))
|
Handle(Event, func(render.Point))
|
||||||
|
@ -107,6 +111,8 @@ type BaseWidget struct {
|
||||||
hidden bool
|
hidden bool
|
||||||
width int32
|
width int32
|
||||||
height int32
|
height int32
|
||||||
|
fixedWidth int32 // values manually configured by the user,
|
||||||
|
fixedHeight int32 // and do not change
|
||||||
point render.Point
|
point render.Point
|
||||||
margin int32
|
margin int32
|
||||||
background render.Color
|
background render.Color
|
||||||
|
@ -152,9 +158,11 @@ func (w *BaseWidget) Configure(c Config) {
|
||||||
w.fixedSize = !c.AutoResize
|
w.fixedSize = !c.AutoResize
|
||||||
if c.Width != 0 {
|
if c.Width != 0 {
|
||||||
w.width = c.Width
|
w.width = c.Width
|
||||||
|
w.fixedWidth = w.width
|
||||||
}
|
}
|
||||||
if c.Height != 0 {
|
if c.Height != 0 {
|
||||||
w.height = c.Height
|
w.height = c.Height
|
||||||
|
w.fixedHeight = w.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +243,12 @@ func (w *BaseWidget) FixedSize() bool {
|
||||||
return w.fixedSize
|
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.
|
// Resize sets the size of the widget to the .W and .H attributes of a rect.
|
||||||
func (w *BaseWidget) Resize(v render.Rect) {
|
func (w *BaseWidget) Resize(v render.Rect) {
|
||||||
w.fixedSize = true
|
w.fixedSize = true
|
||||||
|
@ -249,10 +263,17 @@ func (w *BaseWidget) ResizeBy(v render.Rect) {
|
||||||
w.height += v.H
|
w.height += v.H
|
||||||
}
|
}
|
||||||
|
|
||||||
// resizeAuto sets the size of the widget but doesn't set the fixedSize flag.
|
// ResizeAuto sets the size of the widget but doesn't set the fixedSize flag.
|
||||||
func (w *BaseWidget) resizeAuto(v render.Rect) {
|
func (w *BaseWidget) ResizeAuto(v render.Rect) {
|
||||||
w.width = v.W
|
if w.fixedWidth == 0 {
|
||||||
w.height = v.H
|
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.
|
// BoxThickness returns the full sum of the padding, border and outline.
|
||||||
|
|
|
@ -50,8 +50,8 @@ func NewWindow(title string) *Window {
|
||||||
Background: render.Blue,
|
Background: render.Blue,
|
||||||
})
|
})
|
||||||
w.body.Pack(titleBar, Pack{
|
w.body.Pack(titleBar, Pack{
|
||||||
Anchor: N,
|
Side: Top,
|
||||||
Fill: true,
|
Fill: FillX,
|
||||||
})
|
})
|
||||||
w.titleBar = titleBar
|
w.titleBar = titleBar
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ func NewWindow(title string) *Window {
|
||||||
Background: render.Grey,
|
Background: render.Grey,
|
||||||
})
|
})
|
||||||
w.body.Pack(content, Pack{
|
w.body.Pack(content, Pack{
|
||||||
Anchor: N,
|
Side: Top,
|
||||||
Fill: true,
|
Fill: FillBoth,
|
||||||
})
|
})
|
||||||
w.content = content
|
w.content = content
|
||||||
|
|
||||||
|
@ -81,6 +81,11 @@ func (w *Window) TitleBar() *Label {
|
||||||
return w.titleBar
|
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
|
// Configure the widget. Color and style changes are passed down to the inner
|
||||||
// content frame of the window.
|
// content frame of the window.
|
||||||
func (w *Window) Configure(C Config) {
|
func (w *Window) Configure(C Config) {
|
||||||
|
@ -100,7 +105,23 @@ func (w *Window) ConfigureTitle(C Config) {
|
||||||
|
|
||||||
// Compute the window.
|
// Compute the window.
|
||||||
func (w *Window) Compute(e render.Engine) {
|
func (w *Window) Compute(e render.Engine) {
|
||||||
|
var size = w.Size()
|
||||||
|
|
||||||
w.body.Compute(e)
|
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.
|
// Present the window.
|
||||||
|
|
|
@ -16,7 +16,7 @@ type GUITestScene struct {
|
||||||
|
|
||||||
// Private widgets.
|
// Private widgets.
|
||||||
Frame *ui.Frame
|
Frame *ui.Frame
|
||||||
Window *ui.Frame
|
Window *ui.Window
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name of the scene.
|
// Name of the scene.
|
||||||
|
@ -28,8 +28,7 @@ func (s *GUITestScene) Name() string {
|
||||||
func (s *GUITestScene) Setup(d *Doodle) error {
|
func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
s.Supervisor = ui.NewSupervisor()
|
s.Supervisor = ui.NewSupervisor()
|
||||||
|
|
||||||
window := ui.NewFrame("window")
|
window := ui.NewWindow("Widget Toolkit")
|
||||||
s.Window = window
|
|
||||||
window.Configure(ui.Config{
|
window.Configure(ui.Config{
|
||||||
Width: 750,
|
Width: 750,
|
||||||
Height: 450,
|
Height: 450,
|
||||||
|
@ -37,33 +36,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
BorderStyle: ui.BorderRaised,
|
BorderStyle: ui.BorderRaised,
|
||||||
BorderSize: 2,
|
BorderSize: 2,
|
||||||
})
|
})
|
||||||
|
s.Window = window
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Left Frame
|
// Left Frame
|
||||||
leftFrame := ui.NewFrame("Left Frame")
|
leftFrame := ui.NewFrame("Left Frame")
|
||||||
|
@ -73,9 +46,9 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
BorderSize: 4,
|
BorderSize: 4,
|
||||||
Width: 100,
|
Width: 100,
|
||||||
})
|
})
|
||||||
body.Pack(leftFrame, ui.Pack{
|
window.Pack(leftFrame, ui.Pack{
|
||||||
Anchor: ui.W,
|
Side: ui.Left,
|
||||||
FillY: true,
|
FillY: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Some left frame buttons.
|
// Some left frame buttons.
|
||||||
|
@ -89,9 +62,9 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
})
|
})
|
||||||
s.Supervisor.Add(btn)
|
s.Supervisor.Add(btn)
|
||||||
leftFrame.Pack(btn, ui.Pack{
|
leftFrame.Pack(btn, ui.Pack{
|
||||||
Anchor: ui.N,
|
Side: ui.Top,
|
||||||
FillX: true,
|
FillX: true,
|
||||||
PadY: 2,
|
PadY: 2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +74,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
Background: render.White,
|
Background: render.White,
|
||||||
BorderSize: 0,
|
BorderSize: 0,
|
||||||
})
|
})
|
||||||
body.Pack(frame, ui.Pack{
|
window.Pack(frame, ui.Pack{
|
||||||
Anchor: ui.W,
|
Side: ui.Left,
|
||||||
Expand: true,
|
Expand: true,
|
||||||
Fill: true,
|
Fill: true,
|
||||||
})
|
})
|
||||||
|
@ -115,9 +88,9 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
BorderSize: 2,
|
BorderSize: 2,
|
||||||
Width: 80,
|
Width: 80,
|
||||||
})
|
})
|
||||||
body.Pack(rightFrame, ui.Pack{
|
window.Pack(rightFrame, ui.Pack{
|
||||||
Anchor: ui.W,
|
Side: ui.Right,
|
||||||
Fill: true,
|
Fill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// A grid of buttons.
|
// A grid of buttons.
|
||||||
|
@ -139,7 +112,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
d.Flash("%s clicked", btn)
|
d.Flash("%s clicked", btn)
|
||||||
})
|
})
|
||||||
rowFrame.Pack(btn, ui.Pack{
|
rowFrame.Pack(btn, ui.Pack{
|
||||||
Anchor: ui.W,
|
Side: ui.Left,
|
||||||
Expand: true,
|
Expand: true,
|
||||||
FillX: true,
|
FillX: true,
|
||||||
})
|
})
|
||||||
|
@ -147,8 +120,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
})(row, col, rowFrame)
|
})(row, col, rowFrame)
|
||||||
}
|
}
|
||||||
rightFrame.Pack(rowFrame, ui.Pack{
|
rightFrame.Pack(rowFrame, ui.Pack{
|
||||||
Anchor: ui.N,
|
Side: ui.Top,
|
||||||
Fill: true,
|
Fill: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +133,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
Color: render.Black,
|
Color: render.Black,
|
||||||
},
|
},
|
||||||
}), ui.Pack{
|
}), ui.Pack{
|
||||||
Anchor: ui.NW,
|
Side: ui.Top,
|
||||||
|
Anchor: ui.W,
|
||||||
Padding: 2,
|
Padding: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -172,7 +146,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
frame.Pack(cb, ui.Pack{
|
frame.Pack(cb, ui.Pack{
|
||||||
Anchor: ui.NW,
|
Side: ui.Top,
|
||||||
|
Anchor: ui.W,
|
||||||
Padding: 4,
|
Padding: 4,
|
||||||
})
|
})
|
||||||
cb.Supervise(s.Supervisor)
|
cb.Supervise(s.Supervisor)
|
||||||
|
@ -184,7 +159,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
Color: render.Red,
|
Color: render.Red,
|
||||||
},
|
},
|
||||||
}), ui.Pack{
|
}), ui.Pack{
|
||||||
Anchor: ui.SE,
|
Side: ui.Bottom,
|
||||||
|
Anchor: ui.E,
|
||||||
Padding: 8,
|
Padding: 8,
|
||||||
})
|
})
|
||||||
frame.Pack(ui.NewLabel(ui.Label{
|
frame.Pack(ui.NewLabel(ui.Label{
|
||||||
|
@ -194,7 +170,8 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
Color: render.Blue,
|
Color: render.Blue,
|
||||||
},
|
},
|
||||||
}), ui.Pack{
|
}), ui.Pack{
|
||||||
Anchor: ui.SE,
|
Side: ui.Bottom,
|
||||||
|
Anchor: ui.E,
|
||||||
Padding: 8,
|
Padding: 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -204,7 +181,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
||||||
Background: render.Grey,
|
Background: render.Grey,
|
||||||
})
|
})
|
||||||
window.Pack(btnFrame, ui.Pack{
|
window.Pack(btnFrame, ui.Pack{
|
||||||
Anchor: ui.N,
|
Side: ui.Top,
|
||||||
})
|
})
|
||||||
|
|
||||||
button1 := ui.NewButton("Button1", ui.NewLabel(ui.Label{
|
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{
|
btnFrame.Pack(button1, ui.Pack{
|
||||||
Anchor: align,
|
Side: ui.Left,
|
||||||
Padding: 20,
|
Padding: 20,
|
||||||
})
|
})
|
||||||
btnFrame.Pack(button2, ui.Pack{
|
btnFrame.Pack(button2, ui.Pack{
|
||||||
Anchor: align,
|
Side: ui.Left,
|
||||||
Padding: 20,
|
Padding: 20,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user