diff --git a/guitest_scene.go b/guitest_scene.go index 8a4e6c2..bfa627e 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -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, diff --git a/ui/check_button.go b/ui/check_button.go new file mode 100644 index 0000000..e49062b --- /dev/null +++ b/ui/check_button.go @@ -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 +} diff --git a/ui/checkbox.go b/ui/checkbox.go new file mode 100644 index 0000000..ab5f16b --- /dev/null +++ b/ui/checkbox.go @@ -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) +} diff --git a/ui/frame.go b/ui/frame.go index 779b7f6..41b43c8 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -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 -) diff --git a/ui/frame_pack.go b/ui/frame_pack.go new file mode 100644 index 0000000..3e56b0b --- /dev/null +++ b/ui/frame_pack.go @@ -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 +)