Add CheckButton and CheckBox with Bound Booleans

CheckButton is a generic component based on Button that additionally
takes a *bool variable to manage. When the CheckButton is clicked or
unclicked, it will toggle the bool var and its border style will "stick"
in or out depending on the state.

Checkbox is a Frame widget that wraps a CheckButton and another child
widget, such as a Label. Interacting with the child widget will forward
all of its mouse events to the CheckButton, so that the Label could be
clicked instead of just the box itself.
chunks
Noah 2018-08-01 19:52:09 -07:00
parent cbef5a46cb
commit 316456ef03
5 changed files with 436 additions and 285 deletions

View File

@ -144,6 +144,20 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Anchor: ui.NW,
Padding: 2,
})
cb := ui.NewCheckbox("Overlay",
&DebugOverlay,
ui.NewLabel(render.Text{
Text: "Toggle Debug Overlay",
Size: 14,
Color: render.Black,
}),
)
frame.Pack(cb, ui.Pack{
Anchor: ui.NW,
Padding: 4,
})
cb.Supervise(s.Supervisor)
frame.Pack(ui.NewLabel(render.Text{
Text: "Like Tk!",
Size: 16,

73
ui/check_button.go Normal file
View File

@ -0,0 +1,73 @@
package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
)
// CheckButton is a button that is bound to a boolean variable and stays clicked
// once pressed, until clicked again to release.
type CheckButton struct {
Button
BoolVar *bool
}
// 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)
})
var borderStyle BorderStyle = BorderRaised
if w.BoolVar != nil {
if *w.BoolVar == true {
borderStyle = BorderSunken
}
}
w.Configure(Config{
Padding: 4,
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("MouseDown", func(p render.Point) {
if w.BoolVar != nil {
if *w.BoolVar {
*w.BoolVar = false
w.SetBorderStyle(BorderRaised)
} else {
*w.BoolVar = true
w.SetBorderStyle(BorderSunken)
}
}
})
return w
}

46
ui/checkbox.go Normal file
View File

@ -0,0 +1,46 @@
package ui
import "git.kirsle.net/apps/doodle/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 {
// Our custom checkbutton widget.
mark := NewFrame(name + "_mark")
w := &Checkbox{
button: NewCheckButton(name+"_button", boolVar, mark),
child: child,
}
w.Frame.Setup()
// Forward clicks on the child widget to the CheckButton.
for _, e := range []string{"MouseOver", "MouseOut", "MouseUp", "MouseDown"} {
func(e string) {
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
}
// Supervise the checkbutton inside the widget.
func (w *Checkbox) Supervise(s *Supervisor) {
s.Add(w.button)
s.Add(w.child)
}

View File

@ -30,172 +30,19 @@ func NewFrame(name string) *Frame {
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{}
}
}
// Compute the size of the Frame.
func (w *Frame) Compute(e render.Engine) {
var (
frameSize = w.Size()
// 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
yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2
} else if anchor == E {
x = frameSize.W
xDirection = -1 - w.BoxThickness(2)
}
for _, packedWidget := range w.packs[anchor] {
child := packedWidget.widget
pack := packedWidget.pack
child.Compute(e)
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 * 2)
}
if anchor.IsEast() {
x -= size.W + (pack.PadX * 2)
}
child.MoveTo(render.Point{
X: x + pack.PadX,
Y: y + pack.PadY,
})
if anchor.IsNorth() {
y += size.H + (pack.PadY * 2)
}
if anchor == W {
x += size.W + (pack.PadX * 2)
}
visited = append(visited, packedWidget)
if pack.Expand {
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)
}
// Rescan all the widgets in this anchor to re-center them
// in their space.
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 < frameSize.W {
resize.W = frameSize.W - w.BoxThickness(2)
resized = true
}
if resize.W < frameSize.W-w.BoxThickness(4) {
if pack.Anchor.IsCenter() {
point.X = (frameSize.W / 2) - (resize.W / 2)
} else if pack.Anchor.IsWest() {
point.X = pack.PadX
} else if pack.Anchor.IsEast() {
point.X = frameSize.W - resize.W - pack.PadX
}
moved = true
}
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
if pack.FillY && resize.H < frameSize.H {
resize.H = frameSize.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 < frameSize.H {
if pack.Anchor.IsMiddle() {
point.Y = (frameSize.H / 2) - (resize.H / 2)
} else if pack.Anchor.IsNorth() {
point.Y = pack.PadY - w.BoxThickness(4)
} else if pack.Anchor.IsSouth() {
point.Y = frameSize.H - resize.H - pack.PadY
}
moved = true
}
} else {
log.Error("unsupported pack.Anchor")
}
if resized && size != resize {
child.Resize(resize)
child.Compute(e)
}
if moved {
child.MoveTo(point)
}
}
if !w.FixedSize() {
w.Resize(frameSize)
}
w.computePacked(e)
}
// Present the Frame.
@ -226,124 +73,3 @@ func (w *Frame) Present(e render.Engine) {
child.Present(e)
}
}
// 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.
}
// 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
}
// 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
}
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
type packLayout struct {
widgets []packedWidget
}
type packedWidget struct {
widget Widget
pack Pack
fill uint8
}
// packedWidget.fill values
const (
fillNone uint8 = iota
fillX
fillY
fillBoth
)

292
ui/frame_pack.go Normal file
View File

@ -0,0 +1,292 @@
package ui
import "git.kirsle.net/apps/doodle/render"
// computePacked processes all the Pack layout widgets in the Frame.
func (w *Frame) computePacked(e render.Engine) {
var (
frameSize = w.Size()
// 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
yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2
} else if anchor == E {
x = frameSize.W
xDirection = -1 - w.BoxThickness(2)
}
for _, packedWidget := range w.packs[anchor] {
child := packedWidget.widget
pack := packedWidget.pack
child.Compute(e)
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 * 2)
}
if anchor.IsEast() {
x -= size.W + (pack.PadX * 2)
}
child.MoveTo(render.Point{
X: x + pack.PadX,
Y: y + pack.PadY,
})
if anchor.IsNorth() {
y += size.H + (pack.PadY * 2)
}
if anchor == W {
x += size.W + (pack.PadX * 2)
}
visited = append(visited, packedWidget)
if pack.Expand {
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)
}
// Rescan all the widgets in this anchor to re-center them
// in their space.
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 < frameSize.W {
resize.W = frameSize.W - w.BoxThickness(2)
resized = true
}
if resize.W < frameSize.W-w.BoxThickness(4) {
if pack.Anchor.IsCenter() {
point.X = (frameSize.W / 2) - (resize.W / 2)
} else if pack.Anchor.IsWest() {
point.X = pack.PadX
} else if pack.Anchor.IsEast() {
point.X = frameSize.W - resize.W - pack.PadX
}
moved = true
}
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
if pack.FillY && resize.H < frameSize.H {
resize.H = frameSize.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 < frameSize.H {
if pack.Anchor.IsMiddle() {
point.Y = (frameSize.H / 2) - (resize.H / 2)
} else if pack.Anchor.IsNorth() {
point.Y = pack.PadY - w.BoxThickness(4)
} else if pack.Anchor.IsSouth() {
point.Y = frameSize.H - resize.H - pack.PadY
}
moved = true
}
} else {
log.Error("unsupported pack.Anchor")
}
if resized && size != resize {
child.Resize(resize)
child.Compute(e)
}
if moved {
child.MoveTo(point)
}
}
if !w.FixedSize() {
w.Resize(frameSize)
}
}
// 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.
}
// 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
}
// 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
}
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
type packLayout struct {
widgets []packedWidget
}
type packedWidget struct {
widget Widget
pack Pack
fill uint8
}
// packedWidget.fill values
const (
fillNone uint8 = iota
fillX
fillY
fillBoth
)