Split lib/ui into its own separate repo
This commit is contained in:
parent
a060330450
commit
c1353d1c0f
122
lib/ui/button.go
122
lib/ui/button.go
|
@ -1,122 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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)
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import "git.kirsle.net/go/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)
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package layout
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello world")
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/lib/ui/eg/layout"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello world")
|
|
||||||
layout.main()
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.kirsle.net/go/render"
|
|
||||||
"git.kirsle.net/apps/doodle/lib/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
mw, err := ui.NewMainWindow("UI Toolkit Demo")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
leftFrame := ui.NewFrame("Left Frame")
|
|
||||||
leftFrame.Configure(ui.Config{
|
|
||||||
Width: 200,
|
|
||||||
BorderSize: 1,
|
|
||||||
BorderStyle: ui.BorderRaised,
|
|
||||||
Background: render.Grey,
|
|
||||||
})
|
|
||||||
mw.Pack(leftFrame, ui.Pack{
|
|
||||||
Anchor: ui.W,
|
|
||||||
FillY: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
mainFrame := ui.NewFrame("Main Frame")
|
|
||||||
mainFrame.Configure(ui.Config{
|
|
||||||
Background: render.RGBA(255, 255, 255, 180),
|
|
||||||
})
|
|
||||||
mw.Pack(mainFrame, ui.Pack{
|
|
||||||
Anchor: ui.W,
|
|
||||||
Expand: true,
|
|
||||||
PadX: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
label := ui.NewLabel(ui.Label{
|
|
||||||
Text: "Hello world",
|
|
||||||
})
|
|
||||||
leftFrame.Pack(label, ui.Pack{
|
|
||||||
Anchor: ui.SE,
|
|
||||||
})
|
|
||||||
|
|
||||||
err = mw.MainLoop()
|
|
||||||
if err != nil {
|
|
||||||
panic("MainLoop:" + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,318 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.kirsle.net/go/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() {
|
|
||||||
y = frameSize.H - w.BoxThickness(4)
|
|
||||||
yDirection = -1
|
|
||||||
} else if anchor.IsEast() {
|
|
||||||
x = frameSize.W - w.BoxThickness(4)
|
|
||||||
xDirection = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, packedWidget := range w.packs[anchor] {
|
|
||||||
|
|
||||||
child := packedWidget.widget
|
|
||||||
pack := packedWidget.pack
|
|
||||||
child.Compute(e)
|
|
||||||
|
|
||||||
if child.Hidden() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
x += pack.PadX * xDirection
|
|
||||||
y += pack.PadY * yDirection
|
|
||||||
|
|
||||||
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(4),
|
|
||||||
H: ((frameSize.H - computedSize.H) / int32(len(expanded))) - w.BoxThickness(4),
|
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,38 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import "git.kirsle.net/go/render"
|
|
||||||
|
|
||||||
// AbsolutePosition computes a widget's absolute X,Y position on the
|
|
||||||
// window on screen by crawling its parent widget tree.
|
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageFromTexture creates an Image from a texture.
|
|
||||||
func ImageFromTexture(tex render.Texturer) *Image {
|
|
||||||
return &Image{
|
|
||||||
texture: tex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.LoadTexture(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)
|
|
||||||
}
|
|
133
lib/ui/label.go
133
lib/ui/label.go
|
@ -1,133 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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
|
|
||||||
IntVariable *int
|
|
||||||
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,
|
|
||||||
IntVariable: c.IntVariable,
|
|
||||||
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
|
|
||||||
} else if w.IntVariable != nil {
|
|
||||||
w.Font.Text = fmt.Sprintf("%d", *w.IntVariable)
|
|
||||||
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 {
|
|
||||||
if line == "" {
|
|
||||||
line = "<empty>"
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
// +build !js
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
|
||||||
"git.kirsle.net/go/render/sdl"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Target frames per second for the MainWindow to render at.
|
|
||||||
var (
|
|
||||||
FPS = 60
|
|
||||||
)
|
|
||||||
|
|
||||||
// MainWindow is the parent window of a UI application.
|
|
||||||
type MainWindow struct {
|
|
||||||
engine render.Engine
|
|
||||||
supervisor *Supervisor
|
|
||||||
frame *Frame
|
|
||||||
w int
|
|
||||||
h int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMainWindow initializes the MainWindow. You should probably only have one
|
|
||||||
// of these per application.
|
|
||||||
func NewMainWindow(title string) (*MainWindow, error) {
|
|
||||||
mw := &MainWindow{
|
|
||||||
w: 800,
|
|
||||||
h: 600,
|
|
||||||
supervisor: NewSupervisor(),
|
|
||||||
}
|
|
||||||
|
|
||||||
mw.engine = sdl.New(
|
|
||||||
title,
|
|
||||||
mw.w,
|
|
||||||
mw.h,
|
|
||||||
)
|
|
||||||
if err := mw.engine.Setup(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a default frame to the window.
|
|
||||||
mw.frame = NewFrame("MainWindow Body")
|
|
||||||
mw.frame.SetBackground(render.RGBA(0, 153, 255, 100))
|
|
||||||
mw.Add(mw.frame)
|
|
||||||
|
|
||||||
// Compute initial window size.
|
|
||||||
mw.resized()
|
|
||||||
|
|
||||||
return mw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a child widget to the window.
|
|
||||||
func (mw *MainWindow) Add(w Widget) {
|
|
||||||
mw.supervisor.Add(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pack a child widget into the window's default frame.
|
|
||||||
func (mw *MainWindow) Pack(w Widget, pack Pack) {
|
|
||||||
mw.Add(w)
|
|
||||||
mw.frame.Pack(w, pack)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resized handles the window being resized.
|
|
||||||
func (mw *MainWindow) resized() {
|
|
||||||
mw.frame.Resize(render.Rect{
|
|
||||||
W: int32(mw.w),
|
|
||||||
H: int32(mw.h),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present the window.
|
|
||||||
func (mw *MainWindow) Present() {
|
|
||||||
mw.supervisor.Present(mw.engine)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MainLoop starts the main event loop and blocks until there's an error.
|
|
||||||
func (mw *MainWindow) MainLoop() error {
|
|
||||||
for true {
|
|
||||||
if err := mw.Loop(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop does one loop of the UI.
|
|
||||||
func (mw *MainWindow) Loop() error {
|
|
||||||
mw.engine.Clear(render.White)
|
|
||||||
|
|
||||||
// Record how long this loop took.
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// Poll for events.
|
|
||||||
ev, err := mw.engine.Poll()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("event poll error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ev.WindowResized {
|
|
||||||
w, h := mw.engine.WindowSize()
|
|
||||||
if w != mw.w || h != mw.h {
|
|
||||||
mw.w = w
|
|
||||||
mw.h = h
|
|
||||||
mw.resized()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mw.frame.Compute(mw.engine)
|
|
||||||
|
|
||||||
// Render the child widgets.
|
|
||||||
mw.supervisor.Present(mw.engine)
|
|
||||||
mw.engine.Present()
|
|
||||||
|
|
||||||
// Delay to maintain target frames per second.
|
|
||||||
var delay uint32
|
|
||||||
var targetFPS = 1000 / FPS
|
|
||||||
elapsed := time.Now().Sub(start) / time.Millisecond
|
|
||||||
if targetFPS-int(elapsed) > 0 {
|
|
||||||
delay = uint32(targetFPS - int(elapsed))
|
|
||||||
}
|
|
||||||
mw.engine.Delay(delay)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Menu is a rectangle that holds menu items.
|
|
||||||
type Menu struct {
|
|
||||||
BaseWidget
|
|
||||||
Name string
|
|
||||||
|
|
||||||
body *Frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMenu creates a new Menu. It is hidden by default. Usually you'll
|
|
||||||
// use it with a MenuButton or in a right-click handler.
|
|
||||||
func NewMenu(name string) *Menu {
|
|
||||||
w := &Menu{
|
|
||||||
Name: name,
|
|
||||||
body: NewFrame(name + ":Body"),
|
|
||||||
}
|
|
||||||
w.body.Configure(Config{
|
|
||||||
Width: 150,
|
|
||||||
BorderSize: 12,
|
|
||||||
BorderStyle: BorderRaised,
|
|
||||||
Background: render.Grey,
|
|
||||||
})
|
|
||||||
w.IDFunc(func() string {
|
|
||||||
return fmt.Sprintf("Menu<%s>", w.Name)
|
|
||||||
})
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the menu
|
|
||||||
func (w *Menu) Compute(e render.Engine) {
|
|
||||||
w.body.Compute(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present the menu
|
|
||||||
func (w *Menu) Present(e render.Engine, p render.Point) {
|
|
||||||
w.body.Present(e, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItem quickly adds an item to a menu.
|
|
||||||
func (w *Menu) AddItem(label string, command func()) *MenuItem {
|
|
||||||
menu := NewMenuItem(label, command)
|
|
||||||
w.Pack(menu)
|
|
||||||
return menu
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pack a menu item onto the menu.
|
|
||||||
func (w *Menu) Pack(item *MenuItem) {
|
|
||||||
w.body.Pack(item, Pack{
|
|
||||||
Anchor: NE,
|
|
||||||
// Expand: true,
|
|
||||||
// Padding: 8,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MenuItem is an item in a Menu.
|
|
||||||
type MenuItem struct {
|
|
||||||
Button
|
|
||||||
Label string
|
|
||||||
Accelerator string
|
|
||||||
Command func()
|
|
||||||
button *Button
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMenuItem creates a new menu item.
|
|
||||||
func NewMenuItem(label string, command func()) *MenuItem {
|
|
||||||
w := &MenuItem{
|
|
||||||
Label: label,
|
|
||||||
Command: command,
|
|
||||||
}
|
|
||||||
w.IDFunc(func() string {
|
|
||||||
return fmt.Sprintf("MenuItem<%s>", w.Label)
|
|
||||||
})
|
|
||||||
|
|
||||||
font := DefaultFont
|
|
||||||
font.Color = render.White
|
|
||||||
font.PadX = 12
|
|
||||||
w.Button.child = NewLabel(Label{
|
|
||||||
Text: label,
|
|
||||||
Font: font,
|
|
||||||
})
|
|
||||||
w.Button.Configure(Config{
|
|
||||||
Background: render.Blue,
|
|
||||||
})
|
|
||||||
|
|
||||||
w.Button.Handle(Click, func(p render.Point) {
|
|
||||||
w.Command()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assign the button
|
|
||||||
return w
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
|
||||||
"git.kirsle.net/go/render/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 *event.State) error {
|
|
||||||
var (
|
|
||||||
XY = render.Point{
|
|
||||||
X: int32(ev.CursorX),
|
|
||||||
Y: int32(ev.CursorY),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 && !ev.Button3 {
|
|
||||||
// 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 {
|
|
||||||
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()
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package theme
|
|
||||||
|
|
||||||
import "git.kirsle.net/go/render"
|
|
||||||
|
|
||||||
// Color schemes.
|
|
||||||
var (
|
|
||||||
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
|
||||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
|
||||||
ButtonOutlineColor = render.Black
|
|
||||||
|
|
||||||
BorderColorOffset = 40
|
|
||||||
)
|
|
504
lib/ui/widget.go
504
lib/ui/widget.go
|
@ -1,504 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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)
|
|
||||||
ResizeAuto(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) {
|
|
||||||
if w.ID() == "Frame<Window Body>" {
|
|
||||||
fmt.Printf("%s: ResizeAuto Called: %+v\n",
|
|
||||||
w.ID(),
|
|
||||||
v,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
w.width = v.W
|
|
||||||
w.height = v.H
|
|
||||||
}
|
|
||||||
|
|
||||||
// BoxThickness returns the full sum of the padding, border and outline.
|
|
||||||
// m = multiplier, i.e., 1 or 2
|
|
||||||
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
lib/ui/window.go
114
lib/ui/window.go
|
@ -1,114 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/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