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