WIP Labels

chunks
Noah 2018-07-31 17:18:13 -07:00
parent 11df6cbda9
commit 2e36d9ca85
11 changed files with 750 additions and 51 deletions

View File

@ -63,7 +63,8 @@ func (d *Doodle) Run() error {
// Set up the default scene.
if d.Scene == nil {
d.Goto(&MainScene{})
d.Goto(&GUITestScene{})
// d.Goto(&MainScene{})
}
log.Info("Enter Main Loop")

159
guitest_scene.go Normal file
View File

@ -0,0 +1,159 @@
package doodle
import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// GUITestScene implements the main menu of Doodle.
type GUITestScene struct {
Supervisor *ui.Supervisor
frame *ui.Frame
window *ui.Frame
}
// Name of the scene.
func (s *GUITestScene) Name() string {
return "Main"
}
// Setup the scene.
func (s *GUITestScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor()
window := ui.NewFrame()
s.window = window
window.Configure(ui.Config{
Width: 400,
Height: 400,
Background: render.Grey,
BorderStyle: ui.BorderRaised,
BorderSize: 2,
})
titleBar := ui.NewLabel(render.Text{
Text: "Alert",
Size: 12,
Color: render.White,
Stroke: render.Black,
})
titleBar.Configure(ui.Config{
Background: render.Blue,
OutlineSize: 1,
OutlineColor: render.Black,
})
window.Pack(titleBar, ui.Pack{
Anchor: ui.N,
FillX: true,
})
msgFrame := ui.NewFrame()
msgFrame.Configure(ui.Config{
Background: render.Grey,
BorderStyle: ui.BorderRaised,
BorderSize: 1,
})
window.Pack(msgFrame, ui.Pack{
Anchor: ui.N,
Fill: true,
Padding: 4,
})
btnFrame := ui.NewFrame()
btnFrame.Configure(ui.Config{
Background: render.DarkRed,
})
window.Pack(btnFrame, ui.Pack{
Anchor: ui.N,
Padding: 4,
})
msg := ui.NewLabel(render.Text{
Text: "Hello World!",
Size: 14,
Color: render.Black,
})
msgFrame.Pack(msg, ui.Pack{
Anchor: ui.NW,
Padding: 2,
})
button1 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button1.SetBackground(render.Blue)
button1.Handle("Click", func(p render.Point) {
d.NewMap()
})
log.Info("Button1 bg: %s", button1.Background())
button2 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button2.SetText("Load Map")
var align = ui.W
btnFrame.Pack(button1, ui.Pack{
Anchor: align,
Padding: 20,
Fill: true,
})
btnFrame.Pack(button2, ui.Pack{
Anchor: align,
Padding: 20,
Fill: true,
})
s.Supervisor.Add(button1)
s.Supervisor.Add(button2)
return nil
}
// Loop the editor scene.
func (s *GUITestScene) Loop(d *Doodle, ev *events.State) error {
s.Supervisor.Loop(ev)
return nil
}
// Draw the pixels on this frame.
func (s *GUITestScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
label := ui.NewLabel(render.Text{
Text: "GUITest Doodle v" + Version,
Size: 26,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
})
label.Compute(d.Engine)
label.MoveTo(render.Point{
X: (d.width / 2) - (label.Size().W / 2),
Y: 40,
})
label.Present(d.Engine)
s.window.Compute(d.Engine)
s.window.MoveTo(render.Point{
X: (d.width / 2) - (s.window.Size().W / 2),
Y: 100,
})
s.window.Present(d.Engine)
s.Supervisor.Present(d.Engine)
return nil
}
// Destroy the scene.
func (s *GUITestScene) Destroy() error {
return nil
}

View File

@ -9,6 +9,7 @@ import (
// MainScene implements the main menu of Doodle.
type MainScene struct {
Supervisor *ui.Supervisor
frame *ui.Frame
}
// Name of the scene.
@ -20,16 +21,27 @@ func (s *MainScene) Name() string {
func (s *MainScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor()
frame := ui.NewFrame()
s.frame = frame
s.frame.Configure(ui.Config{
// Width: 400,
// Height: 200,
Background: render.Purple,
BorderStyle: ui.BorderSolid,
BorderSize: 1,
BorderColor: render.Blue,
})
button1 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button1.Compute(d.Engine)
button1.MoveTo(render.Point{
X: (d.width / 2) - (button1.Size().W / 2),
Y: 200,
})
// button1.Compute(d.Engine)
// button1.MoveTo(render.Point{
// X: (d.width / 2) - (button1.Size().W / 2),
// Y: 200,
// })
button1.Handle("Click", func(p render.Point) {
d.NewMap()
})
@ -40,10 +52,22 @@ func (s *MainScene) Setup(d *Doodle) error {
Color: render.Black,
}))
button2.SetText("Load Map")
button2.Compute(d.Engine)
button2.MoveTo(render.Point{
X: (d.width / 2) - (button2.Size().W / 2),
Y: 260,
// button2.Compute(d.Engine)
// button2.MoveTo(render.Point{
// X: (d.width / 2) - (button2.Size().W / 2),
// Y: 260,
// })
var align = ui.E
frame.Pack(button1, ui.Pack{
Anchor: align,
Padding: 12,
Fill: true,
})
frame.Pack(button2, ui.Pack{
Anchor: align,
Padding: 12,
Fill: true,
})
s.Supervisor.Add(button1)
@ -77,6 +101,13 @@ func (s *MainScene) Draw(d *Doodle) error {
})
label.Present(d.Engine)
s.frame.Compute(d.Engine)
s.frame.MoveTo(render.Point{
X: (d.width / 2) - (s.frame.Size().W / 2),
Y: 200,
})
s.frame.Present(d.Engine)
s.Supervisor.Present(d.Engine)
return nil

View File

@ -89,6 +89,16 @@ func (c Color) Add(r, g, b, a int32) Color {
}
}
// Lighten a color value.
func (c Color) Lighten(v int32) Color {
return c.Add(v, v, v, 0)
}
// Darken a color value.
func (c Color) Darken(v int32) Color {
return c.Add(-v, -v, -v, 0)
}
// Point holds an X,Y coordinate value.
type Point struct {
X int32

View File

@ -1,6 +1,8 @@
package ui
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
)
@ -21,12 +23,14 @@ func NewButton(label Label) *Button {
Label: label,
}
w.SetPadding(4)
w.SetBorderSize(2)
w.SetBorderStyle(BorderRaised)
w.SetOutlineSize(1)
w.SetOutlineColor(theme.ButtonOutlineColor)
w.SetBackground(theme.ButtonBackgroundColor)
w.Configure(Config{
Padding: 4,
BorderSize: 2,
BorderStyle: BorderRaised,
OutlineSize: 1,
OutlineColor: theme.ButtonOutlineColor,
Background: theme.ButtonBackgroundColor,
})
w.Handle("MouseOver", func(p render.Point) {
w.hovering = true
@ -46,6 +50,10 @@ func NewButton(label Label) *Button {
w.SetBorderStyle(BorderRaised)
})
w.IDFunc(func() string {
return fmt.Sprintf("Button<%s>", w.Label.Text.Text)
})
return w
}

325
ui/frame.go Normal file
View File

@ -0,0 +1,325 @@
package ui
import (
"fmt"
"time"
"git.kirsle.net/apps/doodle/render"
)
// Frame is a widget that contains other widgets.
type Frame struct {
BaseWidget
packs map[Anchor][]packedWidget
widgets []Widget
}
// NewFrame creates a new Frame.
func NewFrame() *Frame {
return &Frame{
packs: map[Anchor][]packedWidget{},
widgets: []Widget{},
}
}
func (w *Frame) String() string {
return fmt.Sprintf("Frame<%d widgets>",
len(w.widgets),
)
}
// Compute the size of the Frame.
func (w *Frame) Compute(e render.Engine) {
var (
frameSize = w.Size()
maxWidth int32
maxHeight int32
visited = []packedWidget{}
)
// Initialize the dimensions?
if w.FixedSize() {
maxWidth = frameSize.W
maxHeight = frameSize.H
}
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(1)
} else if anchor == E {
x = frameSize.W
xDirection = -1
}
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 !w.FixedSize() {
log.Warn("not fixed")
if xStep+size.W+(pack.PadX*2) > maxWidth {
var old = maxWidth
maxWidth = xStep + size.W + (pack.PadX * 2)
log.Error("%s %s Upgrading maxWidth %d -> %d (size %s) xstep %d",
w,
child,
old,
maxWidth,
size,
xStep,
)
time.Sleep(5)
}
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)
}
}
// 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()
resized bool
moved bool
)
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
if pack.FillX && size.W < maxWidth {
size.W = maxWidth - w.BoxThickness(2)
resized = true
}
if size.W < maxWidth {
if pack.Anchor.IsCenter() {
point.X = (maxWidth / 2) - (size.W / 2)
} else if pack.Anchor.IsWest() {
point.X = pack.PadX
} else if pack.Anchor.IsEast() {
point.X = maxWidth - size.W - pack.PadX
}
moved = true
}
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
if pack.FillY && size.H < maxHeight {
size.H = maxHeight - w.BoxThickness(2)
resized = true
}
if size.H < maxHeight {
if pack.Anchor.IsMiddle() {
point.Y = (maxHeight / 2) - (size.H / 2)
} else if pack.Anchor.IsNorth() {
point.Y = pack.PadY
} else if pack.Anchor.IsSouth() {
point.Y = maxHeight - size.H - pack.PadY
}
moved = true
}
} else {
log.Error("unsupported pack.Anchor")
}
if resized {
child.Resize(size)
}
if moved {
child.MoveTo(point)
}
}
if !w.FixedSize() {
w.Resize(render.Rect{
W: maxWidth + w.BoxThickness(2),
H: maxHeight + w.BoxThickness(2),
})
}
}
// Present the Frame.
func (w *Frame) Present(e render.Engine) {
var (
P = w.Point()
S = w.Size()
)
// Draw the widget's border and everything.
w.DrawBox(e)
// 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 {
p := child.Point()
child.MoveTo(render.NewPoint(
P.X+p.X+w.BoxThickness(1),
P.Y+p.Y+w.BoxThickness(1),
))
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 to fill any remaining space.
}
// 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
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
)

View File

@ -1,6 +1,10 @@
package ui
import "git.kirsle.net/apps/doodle/render"
import (
"fmt"
"git.kirsle.net/apps/doodle/render"
)
// Label is a simple text label widget.
type Label struct {
@ -12,20 +16,40 @@ type Label struct {
// NewLabel creates a new label.
func NewLabel(t render.Text) *Label {
return &Label{
w := &Label{
Text: t,
}
w.Configure(Config{
Padding: 4,
})
w.IDFunc(func() string {
return fmt.Sprintf("Label<%s>", w.Text.Text)
})
return w
}
// Compute the size of the label widget.
func (w *Label) Compute(e render.Engine) {
rect, err := e.ComputeTextRect(w.Text)
w.Resize(rect)
_ = rect
_ = err
rect, _ := e.ComputeTextRect(w.Text)
w.Resize(render.Rect{
W: rect.W + w.Padding(),
H: rect.H + w.Padding(),
})
w.MoveTo(render.Point{
X: rect.X + w.BoxThickness(1),
Y: rect.Y + w.BoxThickness(1),
})
}
// Present the label widget.
func (w *Label) Present(e render.Engine) {
e.DrawText(w.Text, w.Point())
var (
P = w.Point()
border = w.BoxThickness(1)
)
w.DrawBox(e)
e.DrawText(w.Text, render.Point{
X: P.X + border,
Y: P.Y + border,
})
}

14
ui/log.go Normal file
View File

@ -0,0 +1,14 @@
package ui
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("ui")
log.Configure(&golog.Config{
Level: golog.DebugLevel,
Theme: golog.DarkTheme,
Colors: golog.ExtendedColor,
})
}

View File

@ -84,7 +84,8 @@ func (s *Supervisor) Present(e render.Engine) {
defer s.lock.RUnlock()
for _, w := range s.widgets {
w.Present(e)
// w.Present(e)
_ = w
}
}

View File

@ -7,4 +7,6 @@ var (
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
ButtonOutlineColor = render.Black
BorderColorOffset int32 = 40
)

View File

@ -2,6 +2,7 @@ package ui
import (
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui/theme"
)
// BorderStyle options for widget.SetBorderStyle()
@ -16,10 +17,14 @@ const (
// 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)
Resize(render.Rect)
Handle(string, func(render.Point))
@ -56,9 +61,34 @@ type Widget interface {
Present(render.Engine)
}
// 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
Padding int32
PadX int32
PadY 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
width int32
height int32
point render.Point
@ -73,6 +103,66 @@ type BaseWidget struct {
handlers map[string][]func(render.Point)
}
// 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
w.width = c.Width
w.height = c.Height
}
if c.Padding != 0 {
w.padding = c.Padding
}
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 != BorderSolid {
w.borderStyle = c.BorderStyle
}
if c.OutlineSize != 0 {
w.outlineSize = c.OutlineSize
}
}
// Point returns the X,Y position of the widget on the window.
func (w *BaseWidget) Point() render.Point {
return w.point
@ -98,8 +188,21 @@ func (w *BaseWidget) Size() render.Rect {
}
}
// 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
}
// resizeAuto sets the size of the widget but doesn't set the fixedSize flag.
func (w *BaseWidget) resizeAuto(v render.Rect) {
w.width = v.W
w.height = v.H
}
@ -121,8 +224,8 @@ func (w *BaseWidget) DrawBox(e render.Engine) {
outline = w.OutlineSize()
border = w.BorderSize()
borderColor = w.BorderColor()
highlight = borderColor.Add(20, 20, 20, 0)
shadow = borderColor.Add(-20, -20, -20, 0)
highlight = borderColor.Lighten(theme.BorderColorOffset)
shadow = borderColor.Darken(theme.BorderColorOffset)
color render.Color
box = render.Rect{
X: P.X,
@ -132,46 +235,67 @@ func (w *BaseWidget) DrawBox(e render.Engine) {
}
)
if borderColor == render.Invisible {
borderColor = render.Red
}
// Draw the outline layer as the full size of the widget.
e.DrawBox(w.OutlineColor(), render.Rect{
X: P.X - outline,
Y: P.Y - outline,
W: S.W + (outline * 2),
H: S.H + (outline * 2),
})
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 w.BorderStyle() == BorderRaised {
color = highlight
} else if w.BorderStyle() == BorderSunken {
color = shadow
} else {
color = borderColor
if border > 0 {
if w.BorderStyle() == BorderRaised {
color = highlight
} else if w.BorderStyle() == BorderSunken {
color = shadow
} else {
color = borderColor
}
e.DrawBox(color, box)
}
e.DrawBox(color, box)
box.W = S.W
// Shadow on the bottom right edge.
box.X += border
box.Y += border
box.W -= border
box.H -= border
if w.BorderStyle() == BorderRaised {
color = shadow
} else if w.BorderStyle() == BorderSunken {
color = highlight
} else {
color = borderColor
if w.BorderSize() > 0 {
if w.BorderStyle() == BorderRaised {
color = shadow
} else if w.BorderStyle() == BorderSunken {
color = highlight
} else {
color = borderColor
}
e.DrawBox(color, box)
}
e.DrawBox(color.Add(-20, -20, -20, 0), box)
// Background color of the button.
box.W -= border
box.H -= border
// if w.hovering {
// e.DrawBox(render.Yellow, box)
// } else {
e.DrawBox(color, box)
if w.Background() != render.Invisible {
e.DrawBox(w.Background(), box)
}
// log.Info("Widget %s background color: %s", w, w.Background())
// XXX: color effective area
// box.X += w.Padding()
// box.Y += w.Padding()
// box.W -= w.Padding() * 2
// box.H -= w.Padding() * 2
// e.DrawBox(render.RGBA(0, 255, 255, 153), box)
}
// Padding returns the padding width.