Code Layout Refactor
* All private Doodle source code into the pkg/ folder. * Potentially public code into the lib/ folder. * Centralize the logger into a subpackage.
This commit is contained in:
commit
7661f9873c
122
button.go
Normal file
122
button.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||
)
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
BaseWidget
|
||||
child Widget
|
||||
|
||||
// Private options.
|
||||
hovering bool
|
||||
clicked bool
|
||||
}
|
||||
|
||||
// NewButton creates a new Button.
|
||||
func NewButton(name string, child Widget) *Button {
|
||||
w := &Button{
|
||||
child: child,
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Button<%s>", name)
|
||||
})
|
||||
|
||||
w.Configure(Config{
|
||||
BorderSize: 2,
|
||||
BorderStyle: BorderRaised,
|
||||
OutlineSize: 1,
|
||||
OutlineColor: theme.ButtonOutlineColor,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Handle(MouseOver, func(p render.Point) {
|
||||
w.hovering = true
|
||||
w.SetBackground(theme.ButtonHoverColor)
|
||||
})
|
||||
w.Handle(MouseOut, func(p render.Point) {
|
||||
w.hovering = false
|
||||
w.SetBackground(theme.ButtonBackgroundColor)
|
||||
})
|
||||
|
||||
w.Handle(MouseDown, func(p render.Point) {
|
||||
w.clicked = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
})
|
||||
w.Handle(MouseUp, func(p render.Point) {
|
||||
w.clicked = false
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Children returns the button's child widget.
|
||||
func (w *Button) Children() []Widget {
|
||||
return []Widget{w.child}
|
||||
}
|
||||
|
||||
// Compute the size of the button.
|
||||
func (w *Button) Compute(e render.Engine) {
|
||||
// Compute the size of the inner widget first.
|
||||
w.child.Compute(e)
|
||||
|
||||
// Auto-resize only if we haven't been given a fixed size.
|
||||
if !w.FixedSize() {
|
||||
size := w.child.Size()
|
||||
w.Resize(render.Rect{
|
||||
W: size.W + w.BoxThickness(2),
|
||||
H: size.H + w.BoxThickness(2),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SetText conveniently sets the button text, for Label children only.
|
||||
func (w *Button) SetText(text string) error {
|
||||
if label, ok := w.child.(*Label); ok {
|
||||
label.Text = text
|
||||
}
|
||||
return errors.New("child is not a Label widget")
|
||||
}
|
||||
|
||||
// Present the button.
|
||||
func (w *Button) Present(e render.Engine, P render.Point) {
|
||||
if w.Hidden() {
|
||||
return
|
||||
}
|
||||
|
||||
w.Compute(e)
|
||||
w.MoveTo(P)
|
||||
var (
|
||||
S = w.Size()
|
||||
ChildSize = w.child.Size()
|
||||
)
|
||||
|
||||
// Draw the widget's border and everything.
|
||||
w.DrawBox(e, P)
|
||||
|
||||
// Offset further if we are currently sunken.
|
||||
var clickOffset int32
|
||||
if w.clicked {
|
||||
clickOffset++
|
||||
}
|
||||
|
||||
// Where to place the child widget.
|
||||
moveTo := render.Point{
|
||||
X: P.X + w.BoxThickness(1) + clickOffset,
|
||||
Y: P.Y + w.BoxThickness(1) + clickOffset,
|
||||
}
|
||||
|
||||
// If we're bigger than we need to be, center the child widget.
|
||||
if S.Bigger(ChildSize) {
|
||||
moveTo.X = P.X + (S.W / 2) - (ChildSize.W / 2)
|
||||
}
|
||||
|
||||
// Draw the text label inside.
|
||||
w.child.Present(e, moveTo)
|
||||
}
|
118
check_button.go
Normal file
118
check_button.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||
)
|
||||
|
||||
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
||||
// Button and holds a boolean or string pointer (boolean for checkbox,
|
||||
// string for radio).
|
||||
type CheckButton struct {
|
||||
Button
|
||||
BoolVar *bool
|
||||
StringVar *string
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewCheckButton creates a new CheckButton.
|
||||
func NewCheckButton(name string, boolVar *bool, child Widget) *CheckButton {
|
||||
w := &CheckButton{
|
||||
BoolVar: boolVar,
|
||||
}
|
||||
w.Button.child = child
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("CheckButton<%s %+v>", name, w.BoolVar)
|
||||
})
|
||||
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// NewRadioButton creates a CheckButton bound to a string variable.
|
||||
func NewRadioButton(name string, stringVar *string, value string, child Widget) *CheckButton {
|
||||
w := &CheckButton{
|
||||
StringVar: stringVar,
|
||||
Value: value,
|
||||
}
|
||||
w.Button.child = child
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf(`RadioButton<%s "%s" %s>`, name, w.Value, strconv.FormatBool(*w.StringVar == w.Value))
|
||||
})
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// Compute to re-evaluate the button state (in the case of radio buttons where
|
||||
// a different button will affect the state of this one when clicked).
|
||||
func (w *CheckButton) Compute(e render.Engine) {
|
||||
if w.StringVar != nil {
|
||||
// Radio button, always re-assign the border style in case a sister
|
||||
// radio button has changed the value.
|
||||
if *w.StringVar == w.Value {
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
} else {
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
}
|
||||
}
|
||||
w.Button.Compute(e)
|
||||
}
|
||||
|
||||
// setup the common things between checkboxes and radioboxes.
|
||||
func (w *CheckButton) setup() {
|
||||
var borderStyle BorderStyle = BorderRaised
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar == true {
|
||||
borderStyle = BorderSunken
|
||||
}
|
||||
}
|
||||
|
||||
w.Configure(Config{
|
||||
BorderSize: 2,
|
||||
BorderStyle: borderStyle,
|
||||
OutlineSize: 1,
|
||||
OutlineColor: theme.ButtonOutlineColor,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Handle(MouseOver, func(p render.Point) {
|
||||
w.hovering = true
|
||||
w.SetBackground(theme.ButtonHoverColor)
|
||||
})
|
||||
w.Handle(MouseOut, func(p render.Point) {
|
||||
w.hovering = false
|
||||
w.SetBackground(theme.ButtonBackgroundColor)
|
||||
})
|
||||
|
||||
w.Handle(MouseDown, func(p render.Point) {
|
||||
w.clicked = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
})
|
||||
w.Handle(MouseUp, func(p render.Point) {
|
||||
w.clicked = false
|
||||
})
|
||||
|
||||
w.Handle(Click, func(p render.Point) {
|
||||
var sunken bool
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar {
|
||||
*w.BoolVar = false
|
||||
} else {
|
||||
*w.BoolVar = true
|
||||
sunken = true
|
||||
}
|
||||
} else if w.StringVar != nil {
|
||||
*w.StringVar = w.Value
|
||||
sunken = true
|
||||
}
|
||||
|
||||
if sunken {
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
} else {
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
}
|
||||
})
|
||||
}
|
65
checkbox.go
Normal file
65
checkbox.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package ui
|
||||
|
||||
import "git.kirsle.net/apps/doodle/lib/render"
|
||||
|
||||
// Checkbox combines a CheckButton with a widget like a Label.
|
||||
type Checkbox struct {
|
||||
Frame
|
||||
button *CheckButton
|
||||
child Widget
|
||||
}
|
||||
|
||||
// NewCheckbox creates a new Checkbox.
|
||||
func NewCheckbox(name string, boolVar *bool, child Widget) *Checkbox {
|
||||
return makeCheckbox(name, boolVar, nil, "", child)
|
||||
}
|
||||
|
||||
// NewRadiobox creates a new Checkbox in radio mode.
|
||||
func NewRadiobox(name string, stringVar *string, value string, child Widget) *Checkbox {
|
||||
return makeCheckbox(name, nil, stringVar, value, child)
|
||||
}
|
||||
|
||||
// makeCheckbox constructs an appropriate type of checkbox.
|
||||
func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, child Widget) *Checkbox {
|
||||
// Our custom checkbutton widget.
|
||||
mark := NewFrame(name + "_mark")
|
||||
|
||||
w := &Checkbox{
|
||||
child: child,
|
||||
}
|
||||
if boolVar != nil {
|
||||
w.button = NewCheckButton(name+"_button", boolVar, mark)
|
||||
} else if stringVar != nil {
|
||||
w.button = NewRadioButton(name+"_button", stringVar, value, mark)
|
||||
}
|
||||
w.Frame.Setup()
|
||||
|
||||
// Forward clicks on the child widget to the CheckButton.
|
||||
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
|
||||
func(e Event) {
|
||||
w.child.Handle(e, func(p render.Point) {
|
||||
w.button.Event(e, p)
|
||||
})
|
||||
}(e)
|
||||
}
|
||||
|
||||
w.Pack(w.button, Pack{
|
||||
Anchor: W,
|
||||
})
|
||||
w.Pack(w.child, Pack{
|
||||
Anchor: W,
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Child returns the child widget.
|
||||
func (w *Checkbox) Child() Widget {
|
||||
return w.child
|
||||
}
|
||||
|
||||
// Supervise the checkbutton inside the widget.
|
||||
func (w *Checkbox) Supervise(s *Supervisor) {
|
||||
s.Add(w.button)
|
||||
s.Add(w.child)
|
||||
}
|
23
debug.go
Normal file
23
debug.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package ui
|
||||
|
||||
import "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()}
|
||||
)
|
||||
|
||||
for _, child := range node.Children() {
|
||||
lines = append(lines, crawl(depth+1, child)...)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
return crawl(0, root)
|
||||
}
|
28
dragdrop.go
Normal file
28
dragdrop.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package ui
|
||||
|
||||
// DragDrop is a state machine to manage draggable UI components.
|
||||
type DragDrop struct {
|
||||
isDragging bool
|
||||
}
|
||||
|
||||
// NewDragDrop initializes the DragDrop struct. Normally your Supervisor
|
||||
// will manage the drag/drop object, but you can use your own if you don't
|
||||
// use a Supervisor.
|
||||
func NewDragDrop() *DragDrop {
|
||||
return &DragDrop{}
|
||||
}
|
||||
|
||||
// IsDragging returns whether the drag state is active.
|
||||
func (dd *DragDrop) IsDragging() bool {
|
||||
return dd.isDragging
|
||||
}
|
||||
|
||||
// Start the drag state.
|
||||
func (dd *DragDrop) Start() {
|
||||
dd.isDragging = true
|
||||
}
|
||||
|
||||
// Stop dragging.
|
||||
func (dd *DragDrop) Stop() {
|
||||
dd.isDragging = false
|
||||
}
|
91
frame.go
Normal file
91
frame.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Frame is a widget that contains other widgets.
|
||||
type Frame struct {
|
||||
Name string
|
||||
BaseWidget
|
||||
packs map[Anchor][]packedWidget
|
||||
widgets []Widget
|
||||
}
|
||||
|
||||
// NewFrame creates a new Frame.
|
||||
func NewFrame(name string) *Frame {
|
||||
w := &Frame{
|
||||
Name: name,
|
||||
packs: map[Anchor][]packedWidget{},
|
||||
widgets: []Widget{},
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Frame<%s>",
|
||||
name,
|
||||
)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
if w.widgets == nil {
|
||||
w.widgets = []Widget{}
|
||||
}
|
||||
}
|
||||
|
||||
// Children returns all of the child widgets.
|
||||
func (w *Frame) Children() []Widget {
|
||||
return w.widgets
|
||||
}
|
||||
|
||||
// Compute the size of the Frame.
|
||||
func (w *Frame) Compute(e render.Engine) {
|
||||
w.computePacked(e)
|
||||
}
|
||||
|
||||
// Present the Frame.
|
||||
func (w *Frame) Present(e render.Engine, P render.Point) {
|
||||
if w.Hidden() {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
S = w.Size()
|
||||
)
|
||||
|
||||
// Draw the widget's border and everything.
|
||||
w.DrawBox(e, P)
|
||||
|
||||
// Draw the background color.
|
||||
e.DrawBox(w.Background(), render.Rect{
|
||||
X: P.X + w.BoxThickness(1),
|
||||
Y: P.Y + w.BoxThickness(1),
|
||||
W: S.W - w.BoxThickness(2),
|
||||
H: S.H - w.BoxThickness(2),
|
||||
})
|
||||
|
||||
// Draw the widgets.
|
||||
for _, child := range w.widgets {
|
||||
// child.Compute(e)
|
||||
p := child.Point()
|
||||
moveTo := render.NewPoint(
|
||||
P.X+p.X+w.BoxThickness(1),
|
||||
P.Y+p.Y+w.BoxThickness(1),
|
||||
)
|
||||
// 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)
|
||||
}
|
||||
}
|
318
frame_pack.go
Normal file
318
frame_pack.go
Normal file
|
@ -0,0 +1,318 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Pack provides configuration fields for Frame.Pack().
|
||||
type Pack struct {
|
||||
// Side of the parent to anchor the position to, like N, SE, W. Default
|
||||
// is Center.
|
||||
Anchor Anchor
|
||||
|
||||
// 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
|
||||
|
||||
Padding int32 // Equal padding on X and Y.
|
||||
PadX int32
|
||||
PadY int32
|
||||
Expand bool // Widget should grow its allocated space to better fill the parent.
|
||||
}
|
||||
|
||||
// Pack a widget along a side of the frame.
|
||||
func (w *Frame) Pack(child Widget, config ...Pack) {
|
||||
var C Pack
|
||||
if len(config) > 0 {
|
||||
C = config[0]
|
||||
}
|
||||
|
||||
// Initialize the pack list for this anchor?
|
||||
if _, ok := w.packs[C.Anchor]; !ok {
|
||||
w.packs[C.Anchor] = []packedWidget{}
|
||||
}
|
||||
|
||||
// Padding: if the user only provided Padding add it to both
|
||||
// the X and Y value. If the user additionally provided the X
|
||||
// and Y value, it will add to the base padding as you'd expect.
|
||||
C.PadX += C.Padding
|
||||
C.PadY += C.Padding
|
||||
|
||||
// Fill: true implies both directions.
|
||||
if C.Fill {
|
||||
C.FillX = true
|
||||
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,
|
||||
})
|
||||
w.widgets = append(w.widgets, child)
|
||||
}
|
||||
|
||||
// computePacked processes all the Pack layout widgets in the Frame.
|
||||
func (w *Frame) computePacked(e render.Engine) {
|
||||
var (
|
||||
frameSize = w.BoxSize()
|
||||
|
||||
// maxWidth and maxHeight are always the computed minimum dimensions
|
||||
// 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.
|
||||
maxWidth int32
|
||||
maxHeight int32
|
||||
visited = []packedWidget{}
|
||||
expanded = []packedWidget{}
|
||||
)
|
||||
|
||||
// Iterate through all anchored directions 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 {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
x int32
|
||||
y int32
|
||||
yDirection int32 = 1
|
||||
xDirection int32 = 1
|
||||
)
|
||||
|
||||
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 - w.BoxThickness(4)
|
||||
xDirection = -1 - w.BoxThickness(4) // - w.BoxThickness(2)
|
||||
}
|
||||
|
||||
for _, packedWidget := range w.packs[anchor] {
|
||||
|
||||
child := packedWidget.widget
|
||||
pack := packedWidget.pack
|
||||
child.Compute(e)
|
||||
|
||||
if child.Hidden() {
|
||||
continue
|
||||
}
|
||||
|
||||
x += pack.PadX
|
||||
y += pack.PadY
|
||||
|
||||
var (
|
||||
// point = child.Point()
|
||||
size = child.Size()
|
||||
yStep = y * yDirection
|
||||
xStep = x * xDirection
|
||||
)
|
||||
|
||||
if xStep+size.W+(pack.PadX*2) > maxWidth {
|
||||
maxWidth = xStep + size.W + (pack.PadX * 2)
|
||||
}
|
||||
if yStep+size.H+(pack.PadY*2) > maxHeight {
|
||||
maxHeight = yStep + size.H + (pack.PadY * 2)
|
||||
}
|
||||
|
||||
if anchor.IsSouth() {
|
||||
y -= size.H - pack.PadY
|
||||
}
|
||||
if anchor.IsEast() {
|
||||
x -= size.W - pack.PadX
|
||||
}
|
||||
|
||||
child.MoveTo(render.NewPoint(x, y))
|
||||
|
||||
if anchor.IsNorth() {
|
||||
y += size.H + pack.PadY
|
||||
}
|
||||
if anchor == W {
|
||||
x += size.W + pack.PadX
|
||||
}
|
||||
|
||||
visited = append(visited, packedWidget)
|
||||
if pack.Expand { // TODO: don't fuck with children of fixed size
|
||||
expanded = append(expanded, packedWidget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Divy up the size available.
|
||||
growBy := render.Rect{
|
||||
W: ((frameSize.W - computedSize.W) / int32(len(expanded))), // - w.BoxThickness(2),
|
||||
H: ((frameSize.H - computedSize.H) / int32(len(expanded))), // - w.BoxThickness(2),
|
||||
}
|
||||
for _, pw := range expanded {
|
||||
pw.widget.ResizeBy(growBy)
|
||||
pw.widget.Compute(e)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not using a fixed Frame size, use the dynamically computed one.
|
||||
if !w.FixedSize() {
|
||||
frameSize = render.NewRect(maxWidth, maxHeight)
|
||||
} else {
|
||||
// If either of the sizes were left zero, use the dynamically computed one.
|
||||
if frameSize.W == 0 {
|
||||
frameSize.W = maxWidth
|
||||
}
|
||||
if frameSize.H == 0 {
|
||||
frameSize.H = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Rescan all the widgets in this anchor to re-center them
|
||||
// in their space.
|
||||
innerFrameSize := render.NewRect(
|
||||
frameSize.W-w.BoxThickness(2),
|
||||
frameSize.H-w.BoxThickness(2),
|
||||
)
|
||||
for _, pw := range visited {
|
||||
var (
|
||||
child = pw.widget
|
||||
pack = pw.pack
|
||||
point = child.Point()
|
||||
size = child.Size()
|
||||
resize = size
|
||||
resized bool
|
||||
moved bool
|
||||
)
|
||||
|
||||
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
|
||||
if pack.FillX && resize.W < innerFrameSize.W {
|
||||
resize.W = innerFrameSize.W - w.BoxThickness(2)
|
||||
resized = true
|
||||
}
|
||||
if resize.W < innerFrameSize.W-w.BoxThickness(4) {
|
||||
if pack.Anchor.IsCenter() {
|
||||
point.X = (innerFrameSize.W / 2) - (resize.W / 2)
|
||||
} else if pack.Anchor.IsWest() {
|
||||
point.X = pack.PadX
|
||||
} else if pack.Anchor.IsEast() {
|
||||
point.X = innerFrameSize.W - resize.W - pack.PadX
|
||||
}
|
||||
|
||||
moved = true
|
||||
}
|
||||
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
|
||||
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
|
||||
resized = true
|
||||
}
|
||||
|
||||
// Vertically align the widgets.
|
||||
if resize.H < innerFrameSize.H {
|
||||
if pack.Anchor.IsMiddle() {
|
||||
point.Y = (innerFrameSize.H / 2) - (resize.H / 2) - w.BoxThickness(1)
|
||||
} else if pack.Anchor.IsNorth() {
|
||||
point.Y = pack.PadY - w.BoxThickness(4)
|
||||
} else if pack.Anchor.IsSouth() {
|
||||
point.Y = innerFrameSize.H - resize.H - pack.PadY
|
||||
}
|
||||
moved = true
|
||||
}
|
||||
} else {
|
||||
panic("unsupported pack.Anchor")
|
||||
}
|
||||
|
||||
if resized && size != resize {
|
||||
child.Resize(resize)
|
||||
child.Compute(e)
|
||||
}
|
||||
if moved {
|
||||
child.MoveTo(point)
|
||||
}
|
||||
}
|
||||
|
||||
// if !w.FixedSize() {
|
||||
w.Resize(render.NewRect(
|
||||
frameSize.W-w.BoxThickness(2),
|
||||
frameSize.H-w.BoxThickness(2),
|
||||
))
|
||||
// }
|
||||
}
|
||||
|
||||
// Anchor is a cardinal direction.
|
||||
type Anchor uint8
|
||||
|
||||
// Anchor values.
|
||||
const (
|
||||
Center Anchor = iota
|
||||
N
|
||||
NE
|
||||
E
|
||||
SE
|
||||
S
|
||||
SW
|
||||
W
|
||||
NW
|
||||
)
|
||||
|
||||
// Range of Anchor values.
|
||||
const (
|
||||
AnchorMin = Center
|
||||
AnchorMax = NW
|
||||
)
|
||||
|
||||
// IsNorth returns if the anchor is N, NE or NW.
|
||||
func (a Anchor) IsNorth() bool {
|
||||
return a == N || a == NE || a == NW
|
||||
}
|
||||
|
||||
// IsSouth returns if the anchor is S, SE or SW.
|
||||
func (a Anchor) IsSouth() bool {
|
||||
return a == S || a == SE || a == SW
|
||||
}
|
||||
|
||||
// IsEast returns if the anchor is E, NE or SE.
|
||||
func (a Anchor) IsEast() bool {
|
||||
return a == E || a == NE || a == SE
|
||||
}
|
||||
|
||||
// IsWest returns if the anchor is W, NW or SW.
|
||||
func (a Anchor) IsWest() bool {
|
||||
return a == W || a == NW || a == SW
|
||||
}
|
||||
|
||||
// IsCenter returns if the anchor is Center, N or S, to determine
|
||||
// whether to align text as centered for North/South anchors.
|
||||
func (a Anchor) IsCenter() bool {
|
||||
return a == Center || a == N || a == S
|
||||
}
|
||||
|
||||
// IsMiddle returns if the anchor is Center, E or W, to determine
|
||||
// whether to align text as middled for East/West anchors.
|
||||
func (a Anchor) IsMiddle() bool {
|
||||
return a == Center || a == W || a == E
|
||||
}
|
||||
|
||||
type packLayout struct {
|
||||
widgets []packedWidget
|
||||
}
|
||||
|
||||
type packedWidget struct {
|
||||
widget Widget
|
||||
pack Pack
|
||||
fill uint8
|
||||
}
|
||||
|
||||
// packedWidget.fill values
|
||||
const (
|
||||
fillNone uint8 = iota
|
||||
fillX
|
||||
fillY
|
||||
fillBoth
|
||||
)
|
38
functions.go
Normal file
38
functions.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package ui
|
||||
|
||||
import "git.kirsle.net/apps/doodle/lib/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.
|
||||
}
|
||||
}
|
82
image.go
Normal file
82
image.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// ImageType for supported image formats.
|
||||
type ImageType string
|
||||
|
||||
// Supported image formats.
|
||||
const (
|
||||
BMP ImageType = "bmp"
|
||||
PNG = "png"
|
||||
)
|
||||
|
||||
// Image is a widget that is backed by an image file.
|
||||
type Image struct {
|
||||
BaseWidget
|
||||
|
||||
// Configurable fields for the constructor.
|
||||
Type ImageType
|
||||
texture render.Texturer
|
||||
}
|
||||
|
||||
// NewImage creates a new Image.
|
||||
func NewImage(c Image) *Image {
|
||||
w := &Image{
|
||||
Type: c.Type,
|
||||
}
|
||||
if w.Type == "" {
|
||||
w.Type = BMP
|
||||
}
|
||||
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf(`Image<"%s">`, w.Type)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// OpenImage initializes an Image with a given file name.
|
||||
//
|
||||
// The file extension is important and should be a supported ImageType.
|
||||
func OpenImage(e render.Engine, filename string) (*Image, error) {
|
||||
w := &Image{}
|
||||
switch strings.ToLower(filepath.Ext(filename)) {
|
||||
case ".bmp":
|
||||
w.Type = BMP
|
||||
case ".png":
|
||||
w.Type = PNG
|
||||
default:
|
||||
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
|
||||
}
|
||||
|
||||
tex, err := e.NewBitmap(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.texture = tex
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Compute the widget.
|
||||
func (w *Image) Compute(e render.Engine) {
|
||||
w.Resize(w.texture.Size())
|
||||
}
|
||||
|
||||
// Present the widget.
|
||||
func (w *Image) Present(e render.Engine, p render.Point) {
|
||||
size := w.texture.Size()
|
||||
dst := render.Rect{
|
||||
X: p.X,
|
||||
Y: p.Y,
|
||||
W: size.W,
|
||||
H: size.H,
|
||||
}
|
||||
e.Copy(w.texture, size, dst)
|
||||
}
|
125
label.go
Normal file
125
label.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// DefaultFont is the default font settings used for a Label.
|
||||
var DefaultFont = render.Text{
|
||||
Size: 12,
|
||||
Color: render.Black,
|
||||
}
|
||||
|
||||
// Label is a simple text label widget.
|
||||
type Label struct {
|
||||
BaseWidget
|
||||
|
||||
// Configurable fields for the constructor.
|
||||
Text string
|
||||
TextVariable *string
|
||||
Font render.Text
|
||||
|
||||
width int32
|
||||
height int32
|
||||
lineHeight int
|
||||
}
|
||||
|
||||
// NewLabel creates a new label.
|
||||
func NewLabel(c Label) *Label {
|
||||
w := &Label{
|
||||
Text: c.Text,
|
||||
TextVariable: c.TextVariable,
|
||||
Font: DefaultFont,
|
||||
}
|
||||
if !c.Font.IsZero() {
|
||||
w.Font = c.Font
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf(`Label<"%s">`, w.text().Text)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// text returns the label's displayed text, coming from the TextVariable if
|
||||
// available or else the Text attribute instead.
|
||||
func (w *Label) text() render.Text {
|
||||
if w.TextVariable != nil {
|
||||
w.Font.Text = *w.TextVariable
|
||||
return w.Font
|
||||
}
|
||||
w.Font.Text = w.Text
|
||||
return w.Font
|
||||
}
|
||||
|
||||
// Value returns the current text value displayed in the widget, whether it was
|
||||
// the hardcoded value or a TextVariable.
|
||||
func (w *Label) Value() string {
|
||||
return w.text().Text
|
||||
}
|
||||
|
||||
// Compute the size of the label widget.
|
||||
func (w *Label) Compute(e render.Engine) {
|
||||
text := w.text()
|
||||
lines := strings.Split(text.Text, "\n")
|
||||
|
||||
// Max rect to encompass all lines of text.
|
||||
var maxRect = render.Rect{}
|
||||
for _, line := range lines {
|
||||
text.Text = line // only this line at this time.
|
||||
rect, err := e.ComputeTextRect(text)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error
|
||||
return
|
||||
}
|
||||
|
||||
if rect.W > maxRect.W {
|
||||
maxRect.W = rect.W
|
||||
}
|
||||
maxRect.H += rect.H
|
||||
w.lineHeight = int(rect.H)
|
||||
}
|
||||
|
||||
var (
|
||||
padX = w.Font.Padding + w.Font.PadX
|
||||
padY = w.Font.Padding + w.Font.PadY
|
||||
)
|
||||
|
||||
if !w.FixedSize() {
|
||||
w.resizeAuto(render.Rect{
|
||||
W: maxRect.W + (padX * 2),
|
||||
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.
|
||||
func (w *Label) Present(e render.Engine, P render.Point) {
|
||||
if w.Hidden() {
|
||||
return
|
||||
}
|
||||
|
||||
border := w.BoxThickness(1)
|
||||
|
||||
var (
|
||||
text = w.text()
|
||||
padX = w.Font.Padding + w.Font.PadX
|
||||
padY = w.Font.Padding + w.Font.PadY
|
||||
)
|
||||
|
||||
w.DrawBox(e, P)
|
||||
for i, line := range strings.Split(text.Text, "\n") {
|
||||
text.Text = line
|
||||
e.DrawText(text, render.Point{
|
||||
X: P.X + border + padX,
|
||||
Y: P.Y + border + padY + int32(i*w.lineHeight),
|
||||
})
|
||||
}
|
||||
}
|
14
log.go
Normal file
14
log.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package ui
|
||||
|
||||
import "github.com/kirsle/golog"
|
||||
|
||||
var log *golog.Logger
|
||||
|
||||
func init() {
|
||||
log = golog.GetLogger("ui")
|
||||
log.Configure(&golog.Config{
|
||||
Level: golog.DebugLevel,
|
||||
Theme: golog.DarkTheme,
|
||||
Colors: golog.ExtendedColor,
|
||||
})
|
||||
}
|
222
supervisor.go
Normal file
222
supervisor.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Event is a named event that the supervisor will send.
|
||||
type Event int
|
||||
|
||||
// Events.
|
||||
const (
|
||||
NullEvent Event = iota
|
||||
MouseOver
|
||||
MouseOut
|
||||
MouseDown
|
||||
MouseUp
|
||||
Click
|
||||
KeyDown
|
||||
KeyUp
|
||||
KeyPress
|
||||
Drop
|
||||
)
|
||||
|
||||
// Supervisor keeps track of widgets of interest to notify them about
|
||||
// interaction events such as mouse hovers and clicks in their general
|
||||
// vicinity.
|
||||
type Supervisor struct {
|
||||
lock sync.RWMutex
|
||||
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
|
||||
clicked map[int]interface{} // map of widgets being clicked
|
||||
dd *DragDrop
|
||||
}
|
||||
|
||||
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
||||
type WidgetSlot struct {
|
||||
id int
|
||||
widget Widget
|
||||
}
|
||||
|
||||
// NewSupervisor creates a supervisor.
|
||||
func NewSupervisor() *Supervisor {
|
||||
return &Supervisor{
|
||||
widgets: map[int]WidgetSlot{},
|
||||
hovering: map[int]interface{}{},
|
||||
clicked: map[int]interface{}{},
|
||||
dd: NewDragDrop(),
|
||||
}
|
||||
}
|
||||
|
||||
// DragStart sets the drag state.
|
||||
func (s *Supervisor) DragStart() {
|
||||
s.dd.Start()
|
||||
}
|
||||
|
||||
// DragStop stops the drag state.
|
||||
func (s *Supervisor) DragStop() {
|
||||
s.dd.Stop()
|
||||
}
|
||||
|
||||
// IsDragging returns whether the drag state is enabled.
|
||||
func (s *Supervisor) IsDragging() bool {
|
||||
return s.dd.IsDragging()
|
||||
}
|
||||
|
||||
// Error messages that may be returned by Supervisor.Loop()
|
||||
var (
|
||||
// The caller should STOP forwarding any mouse or keyboard events to any
|
||||
// other handles for the remainder of this tick.
|
||||
ErrStopPropagation = errors.New("stop all event propagation")
|
||||
)
|
||||
|
||||
// Loop to check events and pass them to managed widgets.
|
||||
//
|
||||
// Useful errors returned by this may be:
|
||||
// - ErrStopPropagation
|
||||
func (s *Supervisor) Loop(ev *events.State) error {
|
||||
var (
|
||||
XY = render.Point{
|
||||
X: ev.CursorX.Now,
|
||||
Y: ev.CursorY.Now,
|
||||
}
|
||||
)
|
||||
|
||||
// See if we are hovering over any widgets.
|
||||
hovering, outside := s.Hovering(XY)
|
||||
|
||||
// If we are dragging something around, do not trigger any mouse events
|
||||
// to other widgets but DO notify any widget we dropped on top of!
|
||||
if s.dd.IsDragging() {
|
||||
if !ev.Button1.Now && !ev.Button2.Now {
|
||||
// The mouse has been released. TODO: make mouse button important?
|
||||
for _, child := range hovering {
|
||||
child.widget.Event(Drop, XY)
|
||||
}
|
||||
s.DragStop()
|
||||
}
|
||||
return ErrStopPropagation
|
||||
}
|
||||
|
||||
for _, child := range hovering {
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
)
|
||||
if w.Hidden() {
|
||||
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
||||
// anyway, but I don't know why. Adding this check for safety.
|
||||
continue
|
||||
}
|
||||
|
||||
// Cursor has intersected the widget.
|
||||
if _, ok := s.hovering[id]; !ok {
|
||||
w.Event(MouseOver, XY)
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
_, isClicked := s.clicked[id]
|
||||
if ev.Button1.Now {
|
||||
if !isClicked {
|
||||
w.Event(MouseDown, XY)
|
||||
s.clicked[id] = nil
|
||||
}
|
||||
} else if isClicked {
|
||||
w.Event(MouseUp, XY)
|
||||
w.Event(Click, XY)
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
for _, child := range outside {
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
)
|
||||
|
||||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
w.Event(MouseOut, XY)
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
w.Event(MouseUp, XY)
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hovering returns all of the widgets managed by Supervisor that are under
|
||||
// the mouse cursor. Returns the set of widgets below the cursor and the set
|
||||
// of widgets not below the cursor.
|
||||
func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
|
||||
var XY = cursor // for shorthand
|
||||
hovering = []WidgetSlot{}
|
||||
outside = []WidgetSlot{}
|
||||
|
||||
// Check all the widgets under our care.
|
||||
for child := range s.Widgets() {
|
||||
var (
|
||||
w = child.widget
|
||||
P = w.Point()
|
||||
S = w.Size()
|
||||
P2 = render.Point{
|
||||
X: P.X + S.W,
|
||||
Y: P.Y + S.H,
|
||||
}
|
||||
)
|
||||
|
||||
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
|
||||
// Cursor intersects the widget.
|
||||
hovering = append(hovering, child)
|
||||
} else {
|
||||
outside = append(outside, child)
|
||||
}
|
||||
}
|
||||
|
||||
return hovering, outside
|
||||
}
|
||||
|
||||
// Widgets returns a channel of widgets managed by the supervisor in the order
|
||||
// they were added.
|
||||
func (s *Supervisor) Widgets() <-chan WidgetSlot {
|
||||
pipe := make(chan WidgetSlot)
|
||||
go func() {
|
||||
for i := 0; i < s.serial; i++ {
|
||||
if w, ok := s.widgets[i]; ok {
|
||||
pipe <- w
|
||||
}
|
||||
}
|
||||
close(pipe)
|
||||
}()
|
||||
return pipe
|
||||
}
|
||||
|
||||
// Present all widgets managed by the supervisor.
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
// Add a widget to be supervised.
|
||||
func (s *Supervisor) Add(w Widget) {
|
||||
s.lock.Lock()
|
||||
s.widgets[s.serial] = WidgetSlot{
|
||||
id: s.serial,
|
||||
widget: w,
|
||||
}
|
||||
s.serial++
|
||||
s.lock.Unlock()
|
||||
}
|
12
theme/theme.go
Normal file
12
theme/theme.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package theme
|
||||
|
||||
import "git.kirsle.net/apps/doodle/lib/render"
|
||||
|
||||
// Color schemes.
|
||||
var (
|
||||
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||
ButtonOutlineColor = render.Black
|
||||
|
||||
BorderColorOffset int32 = 40
|
||||
)
|
495
widget.go
Normal file
495
widget.go
Normal file
|
@ -0,0 +1,495 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui/theme"
|
||||
)
|
||||
|
||||
// BorderStyle options for widget.SetBorderStyle()
|
||||
type BorderStyle string
|
||||
|
||||
// Styles for a widget border.
|
||||
const (
|
||||
BorderNone BorderStyle = ""
|
||||
BorderSolid BorderStyle = "solid"
|
||||
BorderRaised = "raised"
|
||||
BorderSunken = "sunken"
|
||||
)
|
||||
|
||||
// Widget is a user interface element.
|
||||
type Widget interface {
|
||||
ID() string // Get the widget's string ID.
|
||||
IDFunc(func() string) // Set a function that returns the widget's ID.
|
||||
String() string
|
||||
Point() render.Point
|
||||
MoveTo(render.Point)
|
||||
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)
|
||||
BoxSize() render.Rect // Return the full size including the border and outline.
|
||||
Resize(render.Rect)
|
||||
ResizeBy(render.Rect)
|
||||
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
||||
|
||||
Handle(Event, func(render.Point))
|
||||
Event(Event, render.Point) // called internally to trigger an event
|
||||
|
||||
// Thickness of the padding + border + outline.
|
||||
BoxThickness(multiplier int32) int32
|
||||
DrawBox(render.Engine, render.Point)
|
||||
|
||||
// Widget configuration getters.
|
||||
Margin() int32 // Margin away from other widgets
|
||||
SetMargin(int32) //
|
||||
Background() render.Color // Background color
|
||||
SetBackground(render.Color) //
|
||||
Foreground() render.Color // Foreground color
|
||||
SetForeground(render.Color) //
|
||||
BorderStyle() BorderStyle // Border style: none, raised, sunken
|
||||
SetBorderStyle(BorderStyle) //
|
||||
BorderColor() render.Color // Border color (default is Background)
|
||||
SetBorderColor(render.Color) //
|
||||
BorderSize() int32 // Border size (default 0)
|
||||
SetBorderSize(int32) //
|
||||
OutlineColor() render.Color // Outline color (default Invisible)
|
||||
SetOutlineColor(render.Color) //
|
||||
OutlineSize() int32 // Outline size (default 0)
|
||||
SetOutlineSize(int32) //
|
||||
|
||||
// Visibility
|
||||
Hide()
|
||||
Show()
|
||||
Hidden() bool
|
||||
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
Compute(render.Engine)
|
||||
|
||||
// Render the final widget onto the drawing engine.
|
||||
Present(render.Engine, render.Point)
|
||||
}
|
||||
|
||||
// Config holds common base widget configs for quick configuration.
|
||||
type Config struct {
|
||||
// Size management. If you provide a non-zero value for Width and Height,
|
||||
// the widget will be resized and the "fixedSize" flag is set, meaning it
|
||||
// will not re-compute its size dynamically. To set the size while also
|
||||
// keeping the auto-resize property, pass AutoResize=true too. This is
|
||||
// mainly used internally when widgets are calculating their automatic sizes.
|
||||
AutoResize bool
|
||||
Width int32
|
||||
Height int32
|
||||
Margin int32
|
||||
MarginX int32
|
||||
MarginY int32
|
||||
Background render.Color
|
||||
Foreground render.Color
|
||||
BorderSize int32
|
||||
BorderStyle BorderStyle
|
||||
BorderColor render.Color
|
||||
OutlineSize int32
|
||||
OutlineColor render.Color
|
||||
}
|
||||
|
||||
// BaseWidget holds common functionality for all widgets, such as managing
|
||||
// their widths and heights.
|
||||
type BaseWidget struct {
|
||||
id string
|
||||
idFunc func() string
|
||||
fixedSize bool
|
||||
hidden bool
|
||||
width int32
|
||||
height int32
|
||||
point render.Point
|
||||
margin int32
|
||||
background render.Color
|
||||
foreground render.Color
|
||||
borderStyle BorderStyle
|
||||
borderColor render.Color
|
||||
borderSize int32
|
||||
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.
|
||||
func (w *BaseWidget) SetID(id string) {
|
||||
w.id = id
|
||||
}
|
||||
|
||||
// ID returns the ID that the widget calls itself by.
|
||||
func (w *BaseWidget) ID() string {
|
||||
if w.idFunc == nil {
|
||||
w.IDFunc(func() string {
|
||||
return "Widget<Untitled>"
|
||||
})
|
||||
}
|
||||
return w.idFunc()
|
||||
}
|
||||
|
||||
// IDFunc sets an ID function.
|
||||
func (w *BaseWidget) IDFunc(fn func() string) {
|
||||
w.idFunc = fn
|
||||
}
|
||||
|
||||
func (w *BaseWidget) String() string {
|
||||
return w.ID()
|
||||
}
|
||||
|
||||
// Configure the base widget with all the common properties at once. Any
|
||||
// property left as the zero value will not update the widget.
|
||||
func (w *BaseWidget) Configure(c Config) {
|
||||
if c.Width != 0 || c.Height != 0 {
|
||||
w.fixedSize = !c.AutoResize
|
||||
if c.Width != 0 {
|
||||
w.width = c.Width
|
||||
}
|
||||
if c.Height != 0 {
|
||||
w.height = c.Height
|
||||
}
|
||||
}
|
||||
|
||||
if c.Margin != 0 {
|
||||
w.margin = c.Margin
|
||||
}
|
||||
if c.Background != render.Invisible {
|
||||
w.background = c.Background
|
||||
}
|
||||
if c.Foreground != render.Invisible {
|
||||
w.foreground = c.Foreground
|
||||
}
|
||||
if c.BorderColor != render.Invisible {
|
||||
w.borderColor = c.BorderColor
|
||||
}
|
||||
if c.OutlineColor != render.Invisible {
|
||||
w.outlineColor = c.OutlineColor
|
||||
}
|
||||
|
||||
if c.BorderSize != 0 {
|
||||
w.borderSize = c.BorderSize
|
||||
}
|
||||
if c.BorderStyle != BorderNone {
|
||||
w.borderStyle = c.BorderStyle
|
||||
}
|
||||
if c.OutlineSize != 0 {
|
||||
w.outlineSize = c.OutlineSize
|
||||
}
|
||||
}
|
||||
|
||||
// Rect returns the widget's absolute rectangle, the combined Size and Point.
|
||||
func (w *BaseWidget) Rect() render.Rect {
|
||||
return render.Rect{
|
||||
X: w.point.X,
|
||||
Y: w.point.Y,
|
||||
W: w.width,
|
||||
H: w.height,
|
||||
}
|
||||
}
|
||||
|
||||
// Point returns the X,Y position of the widget on the window.
|
||||
func (w *BaseWidget) Point() render.Point {
|
||||
return w.point
|
||||
}
|
||||
|
||||
// MoveTo updates the X,Y position to the new point.
|
||||
func (w *BaseWidget) MoveTo(v render.Point) {
|
||||
w.point = v
|
||||
}
|
||||
|
||||
// MoveBy adds the X,Y values to the widget's current position.
|
||||
func (w *BaseWidget) MoveBy(v render.Point) {
|
||||
w.point.X += v.X
|
||||
w.point.Y += v.Y
|
||||
}
|
||||
|
||||
// Size returns the box with W and H attributes containing the size of the
|
||||
// widget. The X,Y attributes of the box are ignored and zero.
|
||||
func (w *BaseWidget) Size() render.Rect {
|
||||
return render.Rect{
|
||||
W: w.width,
|
||||
H: w.height,
|
||||
}
|
||||
}
|
||||
|
||||
// BoxSize returns the full rendered size of the widget including its box
|
||||
// thickness (border, padding and outline).
|
||||
func (w *BaseWidget) BoxSize() render.Rect {
|
||||
return render.Rect{
|
||||
W: w.width + w.BoxThickness(2),
|
||||
H: w.height + w.BoxThickness(2),
|
||||
}
|
||||
}
|
||||
|
||||
// FixedSize returns whether the widget's size has been hard-coded by the user
|
||||
// (true) or if it automatically resizes based on its contents (false).
|
||||
func (w *BaseWidget) FixedSize() bool {
|
||||
return w.fixedSize
|
||||
}
|
||||
|
||||
// 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
|
||||
w.width = v.W
|
||||
w.height = v.H
|
||||
}
|
||||
|
||||
// ResizeBy resizes by a relative amount.
|
||||
func (w *BaseWidget) ResizeBy(v render.Rect) {
|
||||
w.fixedSize = true
|
||||
w.width += v.W
|
||||
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) {
|
||||
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
|
||||
func (w *BaseWidget) BoxThickness(m int32) int32 {
|
||||
if m == 0 {
|
||||
m = 1
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Children returns the widget's children, to be implemented by containers.
|
||||
// The default implementation returns an empty slice.
|
||||
func (w *BaseWidget) Children() []Widget {
|
||||
return []Widget{}
|
||||
}
|
||||
|
||||
// Hide the widget from being rendered.
|
||||
func (w *BaseWidget) Hide() {
|
||||
w.hidden = true
|
||||
}
|
||||
|
||||
// Show the widget.
|
||||
func (w *BaseWidget) Show() {
|
||||
w.hidden = false
|
||||
}
|
||||
|
||||
// Hidden returns whether the widget is hidden. If this widget is not hidden,
|
||||
// but it has a parent, this will recursively crawl the parents to see if any
|
||||
// of them are hidden.
|
||||
func (w *BaseWidget) Hidden() bool {
|
||||
if w.hidden {
|
||||
return true
|
||||
}
|
||||
|
||||
if parent, ok := w.Parent(); ok {
|
||||
return parent.Hidden()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// DrawBox draws the border and outline.
|
||||
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
|
||||
var (
|
||||
S = w.Size()
|
||||
outline = w.OutlineSize()
|
||||
border = w.BorderSize()
|
||||
borderColor = w.BorderColor()
|
||||
highlight = borderColor.Lighten(theme.BorderColorOffset)
|
||||
shadow = borderColor.Darken(theme.BorderColorOffset)
|
||||
color render.Color
|
||||
box = render.Rect{
|
||||
X: P.X,
|
||||
Y: P.Y,
|
||||
W: S.W,
|
||||
H: S.H,
|
||||
}
|
||||
)
|
||||
|
||||
if borderColor == render.Invisible {
|
||||
borderColor = render.Red
|
||||
}
|
||||
|
||||
// Draw the outline layer as the full size of the widget.
|
||||
if outline > 0 && w.OutlineColor() != render.Invisible {
|
||||
e.DrawBox(w.OutlineColor(), render.Rect{
|
||||
X: P.X,
|
||||
Y: P.Y,
|
||||
W: S.W,
|
||||
H: S.H,
|
||||
})
|
||||
}
|
||||
box.X += outline
|
||||
box.Y += outline
|
||||
box.W -= outline * 2
|
||||
box.H -= outline * 2
|
||||
|
||||
// Highlight on the top left edge.
|
||||
if border > 0 {
|
||||
if w.BorderStyle() == BorderRaised {
|
||||
color = highlight
|
||||
} else if w.BorderStyle() == BorderSunken {
|
||||
color = shadow
|
||||
} else {
|
||||
color = borderColor
|
||||
}
|
||||
e.DrawBox(color, box)
|
||||
}
|
||||
|
||||
// Shadow on the bottom right edge.
|
||||
box.X += border
|
||||
box.Y += border
|
||||
box.W -= border
|
||||
box.H -= border
|
||||
if w.BorderSize() > 0 {
|
||||
if w.BorderStyle() == BorderRaised {
|
||||
color = shadow
|
||||
} else if w.BorderStyle() == BorderSunken {
|
||||
color = highlight
|
||||
} else {
|
||||
color = borderColor
|
||||
}
|
||||
e.DrawBox(color, box)
|
||||
}
|
||||
|
||||
// Background color of the button.
|
||||
box.W -= border
|
||||
box.H -= border
|
||||
if w.Background() != render.Invisible {
|
||||
e.DrawBox(w.Background(), box)
|
||||
}
|
||||
}
|
||||
|
||||
// Margin returns the margin width.
|
||||
func (w *BaseWidget) Margin() int32 {
|
||||
return w.margin
|
||||
}
|
||||
|
||||
// SetMargin sets the margin width.
|
||||
func (w *BaseWidget) SetMargin(v int32) {
|
||||
w.margin = v
|
||||
}
|
||||
|
||||
// Background returns the background color.
|
||||
func (w *BaseWidget) Background() render.Color {
|
||||
return w.background
|
||||
}
|
||||
|
||||
// SetBackground sets the color.
|
||||
func (w *BaseWidget) SetBackground(c render.Color) {
|
||||
w.background = c
|
||||
}
|
||||
|
||||
// Foreground returns the foreground color.
|
||||
func (w *BaseWidget) Foreground() render.Color {
|
||||
return w.foreground
|
||||
}
|
||||
|
||||
// SetForeground sets the color.
|
||||
func (w *BaseWidget) SetForeground(c render.Color) {
|
||||
w.foreground = c
|
||||
}
|
||||
|
||||
// BorderStyle returns the border style.
|
||||
func (w *BaseWidget) BorderStyle() BorderStyle {
|
||||
return w.borderStyle
|
||||
}
|
||||
|
||||
// SetBorderStyle sets the border style.
|
||||
func (w *BaseWidget) SetBorderStyle(v BorderStyle) {
|
||||
w.borderStyle = v
|
||||
}
|
||||
|
||||
// BorderColor returns the border color, or defaults to the background color.
|
||||
func (w *BaseWidget) BorderColor() render.Color {
|
||||
if w.borderColor == render.Invisible {
|
||||
return w.Background()
|
||||
}
|
||||
return w.borderColor
|
||||
}
|
||||
|
||||
// SetBorderColor sets the border color.
|
||||
func (w *BaseWidget) SetBorderColor(c render.Color) {
|
||||
w.borderColor = c
|
||||
}
|
||||
|
||||
// BorderSize returns the border thickness.
|
||||
func (w *BaseWidget) BorderSize() int32 {
|
||||
return w.borderSize
|
||||
}
|
||||
|
||||
// SetBorderSize sets the border thickness.
|
||||
func (w *BaseWidget) SetBorderSize(v int32) {
|
||||
w.borderSize = v
|
||||
}
|
||||
|
||||
// OutlineColor returns the background color.
|
||||
func (w *BaseWidget) OutlineColor() render.Color {
|
||||
return w.outlineColor
|
||||
}
|
||||
|
||||
// SetOutlineColor sets the color.
|
||||
func (w *BaseWidget) SetOutlineColor(c render.Color) {
|
||||
w.outlineColor = c
|
||||
}
|
||||
|
||||
// OutlineSize returns the outline thickness.
|
||||
func (w *BaseWidget) OutlineSize() int32 {
|
||||
return w.outlineSize
|
||||
}
|
||||
|
||||
// SetOutlineSize sets the outline thickness.
|
||||
func (w *BaseWidget) SetOutlineSize(v int32) {
|
||||
w.outlineSize = v
|
||||
}
|
||||
|
||||
// Event is called internally by Doodle to trigger an event.
|
||||
func (w *BaseWidget) Event(event Event, p render.Point) {
|
||||
if handlers, ok := w.handlers[event]; ok {
|
||||
for _, fn := range handlers {
|
||||
fn(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle an event in the widget.
|
||||
func (w *BaseWidget) Handle(event Event, fn func(render.Point)) {
|
||||
if w.handlers == nil {
|
||||
w.handlers = map[Event][]func(render.Point){}
|
||||
}
|
||||
|
||||
if _, ok := w.handlers[event]; !ok {
|
||||
w.handlers[event] = []func(render.Point){}
|
||||
}
|
||||
|
||||
w.handlers[event] = append(w.handlers[event], fn)
|
||||
}
|
||||
|
||||
// OnMouseOut should be overridden on widgets who want this event.
|
||||
func (w *BaseWidget) OnMouseOut(render.Point) {}
|
114
window.go
Normal file
114
window.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
)
|
||||
|
||||
// Window is a frame with a title bar.
|
||||
type Window struct {
|
||||
BaseWidget
|
||||
Title string
|
||||
Active bool
|
||||
|
||||
// Private widgets.
|
||||
body *Frame
|
||||
titleBar *Label
|
||||
content *Frame
|
||||
}
|
||||
|
||||
// NewWindow creates a new window.
|
||||
func NewWindow(title string) *Window {
|
||||
w := &Window{
|
||||
Title: title,
|
||||
body: NewFrame("body:" + title),
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Window<%s>",
|
||||
w.Title,
|
||||
)
|
||||
})
|
||||
|
||||
w.body.Configure(Config{
|
||||
Background: render.Grey,
|
||||
BorderSize: 2,
|
||||
BorderStyle: BorderRaised,
|
||||
})
|
||||
|
||||
// Title bar widget.
|
||||
titleBar := NewLabel(Label{
|
||||
TextVariable: &w.Title,
|
||||
Font: render.Text{
|
||||
Color: render.White,
|
||||
Size: 10,
|
||||
Stroke: render.DarkBlue,
|
||||
Padding: 2,
|
||||
},
|
||||
})
|
||||
titleBar.Configure(Config{
|
||||
Background: render.Blue,
|
||||
})
|
||||
w.body.Pack(titleBar, Pack{
|
||||
Anchor: N,
|
||||
Fill: true,
|
||||
})
|
||||
w.titleBar = titleBar
|
||||
|
||||
// Window content frame.
|
||||
content := NewFrame("content:" + title)
|
||||
content.Configure(Config{
|
||||
Background: render.Grey,
|
||||
})
|
||||
w.body.Pack(content, Pack{
|
||||
Anchor: N,
|
||||
Fill: true,
|
||||
})
|
||||
w.content = content
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Children returns the window's child widgets.
|
||||
func (w *Window) Children() []Widget {
|
||||
return []Widget{
|
||||
w.body,
|
||||
}
|
||||
}
|
||||
|
||||
// TitleBar returns the title bar widget.
|
||||
func (w *Window) TitleBar() *Label {
|
||||
return w.titleBar
|
||||
}
|
||||
|
||||
// Configure the widget. Color and style changes are passed down to the inner
|
||||
// content frame of the window.
|
||||
func (w *Window) Configure(C Config) {
|
||||
w.BaseWidget.Configure(C)
|
||||
w.body.Configure(C)
|
||||
|
||||
// Don't pass dimensions down any further than the body.
|
||||
C.Width = 0
|
||||
C.Height = 0
|
||||
w.content.Configure(C)
|
||||
}
|
||||
|
||||
// ConfigureTitle configures the title bar widget.
|
||||
func (w *Window) ConfigureTitle(C Config) {
|
||||
w.titleBar.Configure(C)
|
||||
}
|
||||
|
||||
// Compute the window.
|
||||
func (w *Window) Compute(e render.Engine) {
|
||||
w.body.Compute(e)
|
||||
}
|
||||
|
||||
// Present the window.
|
||||
func (w *Window) Present(e render.Engine, P render.Point) {
|
||||
w.body.Present(e, P)
|
||||
}
|
||||
|
||||
// Pack a widget into the window's frame.
|
||||
func (w *Window) Pack(child Widget, config ...Pack) {
|
||||
w.content.Pack(child, config...)
|
||||
}
|
Loading…
Reference in New Issue
Block a user