Some basic UI controls.

This commit is contained in:
Sander Schobers 2018-08-03 08:46:10 +02:00
parent a761b21775
commit 8157d8b3d8
19 changed files with 1769 additions and 0 deletions

133
ui/checkbox.go Normal file
View File

@ -0,0 +1,133 @@
package ui
import (
"fmt"
"image"
"image/color"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dkit"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type CheckboxValueChangedFn func(bool)
func createOnBitmap(fill, stroke color.Color) *allegro5.Bitmap {
var sz = float64(checkboxSize)
on := image.NewRGBA(image.Rect(0, 0, checkboxSize, checkboxSize))
gc := draw2dimg.NewGraphicContext(on)
gc.SetFillColor(color.Transparent)
gc.Clear()
gc.SetFillColor(fill)
draw2dkit.RoundedRectangle(gc, 0, 0, sz, sz, 3, 3)
gc.Fill()
gc.SetStrokeColor(stroke)
gc.SetLineWidth(2)
gc.MoveTo(2.5, 5.5)
gc.LineTo(5.5, 8.5)
gc.LineTo(10, 3.5)
gc.Stroke()
bmp, err := allegro5.NewBitmapFromImage(on, false)
if nil != err {
return nil
}
return bmp
}
func createOffBitmap(fill, stroke color.Color) *allegro5.Bitmap {
var sz = float64(checkboxSize)
off := image.NewRGBA(image.Rect(0, 0, checkboxSize, checkboxSize))
gc := draw2dimg.NewGraphicContext(off)
gc.SetFillColor(color.Transparent)
gc.Clear()
gc.SetFillColor(stroke)
draw2dkit.RoundedRectangle(gc, 0, 0, sz, sz, 4, 4)
gc.Fill()
gc.SetFillColor(fill)
draw2dkit.RoundedRectangle(gc, 1, 1, sz-1, sz-1, 3, 3)
gc.Fill()
bmp, err := allegro5.NewBitmapFromImage(off, false)
if nil != err {
return nil
}
return bmp
}
type Checkbox struct {
ControlBase
Value bool
Text string
OnChanged CheckboxValueChangedFn
on *allegro5.Bitmap
off *allegro5.Bitmap
}
func (c *Checkbox) Created(ctx Context, p Container) error {
var err = c.ControlBase.Created(ctx, p)
if nil != err {
return err
}
var plt = ctx.Palette()
c.on = createOnBitmap(plt.Primary(), plt.White())
c.off = createOffBitmap(plt.White(), plt.Black())
if nil == c.on || nil == c.off {
return fmt.Errorf("error creating checkboxes")
}
return nil
}
func (c *Checkbox) Handle(ctx Context, ev allegro5.Event) {
var pressed = c.IsPressed
c.ControlBase.Handle(ctx, ev)
switch ev.(type) {
case *allegro5.MouseButtonUpEvent:
if !c.Disabled && pressed && c.IsOver {
c.Value = !c.Value
var onChanged = c.OnChanged
if nil != onChanged {
onChanged(c.Value)
}
}
}
}
func (c *Checkbox) DesiredSize(ctx Context) geom.PointF {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var w = fnt.TextWidth(c.Text)
return geom.PtF(float64(w+2*leftMargin+checkboxSize), 24)
}
func (c *Checkbox) box() *allegro5.Bitmap {
if c.Value {
return c.on
}
return c.off
}
func (c *Checkbox) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = c.Bounds.Min.To32()
min = geom.PointF32{X: min.X + leftMargin, Y: min.Y + topMargin}
var fnt = fonts.Get("default")
var _, textY, _, textH = fnt.TextDims(c.Text)
fnt.Draw(min.X+leftMargin+checkboxSize, min.Y-textY, ctx.Palette().Black(), allegro5.AlignLeft, c.Text)
var checkboxTop = min.Y + textH - checkboxSize
if c.Disabled {
var disabled = ctx.Palette().Disabled()
c.box().DrawOptions(min.X, checkboxTop, allegro5.DrawOptions{Tint: &disabled})
} else {
c.box().Draw(min.X, checkboxTop)
}
c.ControlBase.Render(ctx)
}

283
ui/colors.go Normal file
View File

@ -0,0 +1,283 @@
package ui
import "image/color"
var (
Red50 = &color.RGBA{R: 0xff, G: 0xeb, B: 0xee, A: 0xff}
Red100 = &color.RGBA{R: 0xff, G: 0xcd, B: 0xd2, A: 0xff}
Red200 = &color.RGBA{R: 0xef, G: 0x9a, B: 0x9a, A: 0xff}
Red300 = &color.RGBA{R: 0xe5, G: 0x73, B: 0x73, A: 0xff}
Red400 = &color.RGBA{R: 0xef, G: 0x53, B: 0x50, A: 0xff}
Red500 = &color.RGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff}
Red600 = &color.RGBA{R: 0xe5, G: 0x39, B: 0x35, A: 0xff}
Red700 = &color.RGBA{R: 0xd3, G: 0x2f, B: 0x2f, A: 0xff}
Red800 = &color.RGBA{R: 0xc6, G: 0x28, B: 0x28, A: 0xff}
Red900 = &color.RGBA{R: 0xb7, G: 0x1c, B: 0x1c, A: 0xff}
RedA100 = &color.RGBA{R: 0xff, G: 0x8a, B: 0x80, A: 0xff}
RedA200 = &color.RGBA{R: 0xff, G: 0x52, B: 0x52, A: 0xff}
RedA400 = &color.RGBA{R: 0xff, G: 0x17, B: 0x44, A: 0xff}
RedA700 = &color.RGBA{R: 0xd5, G: 0x00, B: 0x00, A: 0xff}
Pink50 = &color.RGBA{R: 0xfc, G: 0xe4, B: 0xec, A: 0xff}
Pink100 = &color.RGBA{R: 0xf8, G: 0xbb, B: 0xd0, A: 0xff}
Pink200 = &color.RGBA{R: 0xf4, G: 0x8f, B: 0xb1, A: 0xff}
Pink300 = &color.RGBA{R: 0xf0, G: 0x62, B: 0x92, A: 0xff}
Pink400 = &color.RGBA{R: 0xec, G: 0x40, B: 0x7a, A: 0xff}
Pink500 = &color.RGBA{R: 0xe9, G: 0x1e, B: 0x63, A: 0xff}
Pink600 = &color.RGBA{R: 0xd8, G: 0x1b, B: 0x60, A: 0xff}
Pink700 = &color.RGBA{R: 0xc2, G: 0x18, B: 0x5b, A: 0xff}
Pink800 = &color.RGBA{R: 0xad, G: 0x14, B: 0x57, A: 0xff}
Pink900 = &color.RGBA{R: 0x88, G: 0x0e, B: 0x4f, A: 0xff}
PinkA100 = &color.RGBA{R: 0xff, G: 0x80, B: 0xab, A: 0xff}
PinkA200 = &color.RGBA{R: 0xff, G: 0x40, B: 0x81, A: 0xff}
PinkA400 = &color.RGBA{R: 0xf5, G: 0x00, B: 0x57, A: 0xff}
PinkA700 = &color.RGBA{R: 0xc5, G: 0x11, B: 0x62, A: 0xff}
Purple50 = &color.RGBA{R: 0xf3, G: 0xe5, B: 0xf5, A: 0xff}
Purple100 = &color.RGBA{R: 0xe1, G: 0xbe, B: 0xe7, A: 0xff}
Purple200 = &color.RGBA{R: 0xce, G: 0x93, B: 0xd8, A: 0xff}
Purple300 = &color.RGBA{R: 0xba, G: 0x68, B: 0xc8, A: 0xff}
Purple400 = &color.RGBA{R: 0xab, G: 0x47, B: 0xbc, A: 0xff}
Purple500 = &color.RGBA{R: 0x9c, G: 0x27, B: 0xb0, A: 0xff}
Purple600 = &color.RGBA{R: 0x8e, G: 0x24, B: 0xaa, A: 0xff}
Purple700 = &color.RGBA{R: 0x7b, G: 0x1f, B: 0xa2, A: 0xff}
Purple800 = &color.RGBA{R: 0x6a, G: 0x1b, B: 0x9a, A: 0xff}
Purple900 = &color.RGBA{R: 0x4a, G: 0x14, B: 0x8c, A: 0xff}
PurpleA100 = &color.RGBA{R: 0xea, G: 0x80, B: 0xfc, A: 0xff}
PurpleA200 = &color.RGBA{R: 0xe0, G: 0x40, B: 0xfb, A: 0xff}
PurpleA400 = &color.RGBA{R: 0xd5, G: 0x00, B: 0xf9, A: 0xff}
PurpleA700 = &color.RGBA{R: 0xaa, G: 0x00, B: 0xff, A: 0xff}
DeepPurple50 = &color.RGBA{R: 0xed, G: 0xe7, B: 0xf6, A: 0xff}
DeepPurple100 = &color.RGBA{R: 0xd1, G: 0xc4, B: 0xe9, A: 0xff}
DeepPurple200 = &color.RGBA{R: 0xb3, G: 0x9d, B: 0xdb, A: 0xff}
DeepPurple300 = &color.RGBA{R: 0x95, G: 0x75, B: 0xcd, A: 0xff}
DeepPurple400 = &color.RGBA{R: 0x7e, G: 0x57, B: 0xc2, A: 0xff}
DeepPurple500 = &color.RGBA{R: 0x67, G: 0x3a, B: 0xb7, A: 0xff}
DeepPurple600 = &color.RGBA{R: 0x5e, G: 0x35, B: 0xb1, A: 0xff}
DeepPurple700 = &color.RGBA{R: 0x51, G: 0x2d, B: 0xa8, A: 0xff}
DeepPurple800 = &color.RGBA{R: 0x45, G: 0x27, B: 0xa0, A: 0xff}
DeepPurple900 = &color.RGBA{R: 0x31, G: 0x1b, B: 0x92, A: 0xff}
DeepPurpleA100 = &color.RGBA{R: 0xb3, G: 0x88, B: 0xff, A: 0xff}
DeepPurpleA200 = &color.RGBA{R: 0x7c, G: 0x4d, B: 0xff, A: 0xff}
DeepPurpleA400 = &color.RGBA{R: 0x65, G: 0x1f, B: 0xff, A: 0xff}
DeepPurpleA700 = &color.RGBA{R: 0x62, G: 0x00, B: 0xea, A: 0xff}
Indigo50 = &color.RGBA{R: 0xe8, G: 0xea, B: 0xf6, A: 0xff}
Indigo100 = &color.RGBA{R: 0xc5, G: 0xca, B: 0xe9, A: 0xff}
Indigo200 = &color.RGBA{R: 0x9f, G: 0xa8, B: 0xda, A: 0xff}
Indigo300 = &color.RGBA{R: 0x79, G: 0x86, B: 0xcb, A: 0xff}
Indigo400 = &color.RGBA{R: 0x5c, G: 0x6b, B: 0xc0, A: 0xff}
Indigo500 = &color.RGBA{R: 0x3f, G: 0x51, B: 0xb5, A: 0xff}
Indigo600 = &color.RGBA{R: 0x39, G: 0x49, B: 0xab, A: 0xff}
Indigo700 = &color.RGBA{R: 0x30, G: 0x3f, B: 0x9f, A: 0xff}
Indigo800 = &color.RGBA{R: 0x28, G: 0x35, B: 0x93, A: 0xff}
Indigo900 = &color.RGBA{R: 0x1a, G: 0x23, B: 0x7e, A: 0xff}
IndigoA100 = &color.RGBA{R: 0x8c, G: 0x9e, B: 0xff, A: 0xff}
IndigoA200 = &color.RGBA{R: 0x53, G: 0x6d, B: 0xfe, A: 0xff}
IndigoA400 = &color.RGBA{R: 0x3d, G: 0x5a, B: 0xfe, A: 0xff}
IndigoA700 = &color.RGBA{R: 0x30, G: 0x4f, B: 0xfe, A: 0xff}
Blue50 = &color.RGBA{R: 0xe3, G: 0xf2, B: 0xfd, A: 0xff}
Blue100 = &color.RGBA{R: 0xbb, G: 0xde, B: 0xfb, A: 0xff}
Blue200 = &color.RGBA{R: 0x90, G: 0xca, B: 0xf9, A: 0xff}
Blue300 = &color.RGBA{R: 0x64, G: 0xb5, B: 0xf6, A: 0xff}
Blue400 = &color.RGBA{R: 0x42, G: 0xa5, B: 0xf5, A: 0xff}
Blue500 = &color.RGBA{R: 0x21, G: 0x96, B: 0xf3, A: 0xff}
Blue600 = &color.RGBA{R: 0x1e, G: 0x88, B: 0xe5, A: 0xff}
Blue700 = &color.RGBA{R: 0x19, G: 0x76, B: 0xd2, A: 0xff}
Blue800 = &color.RGBA{R: 0x15, G: 0x65, B: 0xc0, A: 0xff}
Blue900 = &color.RGBA{R: 0x0d, G: 0x47, B: 0xa1, A: 0xff}
BlueA100 = &color.RGBA{R: 0x82, G: 0xb1, B: 0xff, A: 0xff}
BlueA200 = &color.RGBA{R: 0x44, G: 0x8a, B: 0xff, A: 0xff}
BlueA400 = &color.RGBA{R: 0x29, G: 0x79, B: 0xff, A: 0xff}
BlueA700 = &color.RGBA{R: 0x29, G: 0x62, B: 0xff, A: 0xff}
LightBlue50 = &color.RGBA{R: 0xe1, G: 0xf5, B: 0xfe, A: 0xff}
LightBlue100 = &color.RGBA{R: 0xb3, G: 0xe5, B: 0xfc, A: 0xff}
LightBlue200 = &color.RGBA{R: 0x81, G: 0xd4, B: 0xfa, A: 0xff}
LightBlue300 = &color.RGBA{R: 0x4f, G: 0xc3, B: 0xf7, A: 0xff}
LightBlue400 = &color.RGBA{R: 0x29, G: 0xb6, B: 0xf6, A: 0xff}
LightBlue500 = &color.RGBA{R: 0x03, G: 0xa9, B: 0xf4, A: 0xff}
LightBlue600 = &color.RGBA{R: 0x03, G: 0x9b, B: 0xe5, A: 0xff}
LightBlue700 = &color.RGBA{R: 0x02, G: 0x88, B: 0xd1, A: 0xff}
LightBlue800 = &color.RGBA{R: 0x02, G: 0x77, B: 0xbd, A: 0xff}
LightBlue900 = &color.RGBA{R: 0x01, G: 0x57, B: 0x9b, A: 0xff}
LightBlueA100 = &color.RGBA{R: 0x80, G: 0xd8, B: 0xff, A: 0xff}
LightBlueA200 = &color.RGBA{R: 0x40, G: 0xc4, B: 0xff, A: 0xff}
LightBlueA400 = &color.RGBA{R: 0x00, G: 0xb0, B: 0xff, A: 0xff}
LightBlueA700 = &color.RGBA{R: 0x00, G: 0x91, B: 0xea, A: 0xff}
Cyan50 = &color.RGBA{R: 0xe0, G: 0xf7, B: 0xfa, A: 0xff}
Cyan100 = &color.RGBA{R: 0xb2, G: 0xeb, B: 0xf2, A: 0xff}
Cyan200 = &color.RGBA{R: 0x80, G: 0xde, B: 0xea, A: 0xff}
Cyan300 = &color.RGBA{R: 0x4d, G: 0xd0, B: 0xe1, A: 0xff}
Cyan400 = &color.RGBA{R: 0x26, G: 0xc6, B: 0xda, A: 0xff}
Cyan500 = &color.RGBA{R: 0x00, G: 0xbc, B: 0xd4, A: 0xff}
Cyan600 = &color.RGBA{R: 0x00, G: 0xac, B: 0xc1, A: 0xff}
Cyan700 = &color.RGBA{R: 0x00, G: 0x97, B: 0xa7, A: 0xff}
Cyan800 = &color.RGBA{R: 0x00, G: 0x83, B: 0x8f, A: 0xff}
Cyan900 = &color.RGBA{R: 0x00, G: 0x60, B: 0x64, A: 0xff}
CyanA100 = &color.RGBA{R: 0x84, G: 0xff, B: 0xff, A: 0xff}
CyanA200 = &color.RGBA{R: 0x18, G: 0xff, B: 0xff, A: 0xff}
CyanA400 = &color.RGBA{R: 0x00, G: 0xe5, B: 0xff, A: 0xff}
CyanA700 = &color.RGBA{R: 0x00, G: 0xb8, B: 0xd4, A: 0xff}
Teal50 = &color.RGBA{R: 0xe0, G: 0xf2, B: 0xf1, A: 0xff}
Teal100 = &color.RGBA{R: 0xb2, G: 0xdf, B: 0xdb, A: 0xff}
Teal200 = &color.RGBA{R: 0x80, G: 0xcb, B: 0xc4, A: 0xff}
Teal300 = &color.RGBA{R: 0x4d, G: 0xb6, B: 0xac, A: 0xff}
Teal400 = &color.RGBA{R: 0x26, G: 0xa6, B: 0x9a, A: 0xff}
Teal500 = &color.RGBA{R: 0x00, G: 0x96, B: 0x88, A: 0xff}
Teal600 = &color.RGBA{R: 0x00, G: 0x89, B: 0x7b, A: 0xff}
Teal700 = &color.RGBA{R: 0x00, G: 0x79, B: 0x6b, A: 0xff}
Teal800 = &color.RGBA{R: 0x00, G: 0x69, B: 0x5c, A: 0xff}
Teal900 = &color.RGBA{R: 0x00, G: 0x4d, B: 0x40, A: 0xff}
TealA100 = &color.RGBA{R: 0xa7, G: 0xff, B: 0xeb, A: 0xff}
TealA200 = &color.RGBA{R: 0x64, G: 0xff, B: 0xda, A: 0xff}
TealA400 = &color.RGBA{R: 0x1d, G: 0xe9, B: 0xb6, A: 0xff}
TealA700 = &color.RGBA{R: 0x00, G: 0xbf, B: 0xa5, A: 0xff}
Green50 = &color.RGBA{R: 0xe8, G: 0xf5, B: 0xe9, A: 0xff}
Green100 = &color.RGBA{R: 0xc8, G: 0xe6, B: 0xc9, A: 0xff}
Green200 = &color.RGBA{R: 0xa5, G: 0xd6, B: 0xa7, A: 0xff}
Green300 = &color.RGBA{R: 0x81, G: 0xc7, B: 0x84, A: 0xff}
Green400 = &color.RGBA{R: 0x66, G: 0xbb, B: 0x6a, A: 0xff}
Green500 = &color.RGBA{R: 0x4c, G: 0xaf, B: 0x50, A: 0xff}
Green600 = &color.RGBA{R: 0x43, G: 0xa0, B: 0x47, A: 0xff}
Green700 = &color.RGBA{R: 0x38, G: 0x8e, B: 0x3c, A: 0xff}
Green800 = &color.RGBA{R: 0x2e, G: 0x7d, B: 0x32, A: 0xff}
Green900 = &color.RGBA{R: 0x1b, G: 0x5e, B: 0x20, A: 0xff}
GreenA100 = &color.RGBA{R: 0xb9, G: 0xf6, B: 0xca, A: 0xff}
GreenA200 = &color.RGBA{R: 0x69, G: 0xf0, B: 0xae, A: 0xff}
GreenA400 = &color.RGBA{R: 0x00, G: 0xe6, B: 0x76, A: 0xff}
GreenA700 = &color.RGBA{R: 0x00, G: 0xc8, B: 0x53, A: 0xff}
LightGreen50 = &color.RGBA{R: 0xf1, G: 0xf8, B: 0xe9, A: 0xff}
LightGreen100 = &color.RGBA{R: 0xdc, G: 0xed, B: 0xc8, A: 0xff}
LightGreen200 = &color.RGBA{R: 0xc5, G: 0xe1, B: 0xa5, A: 0xff}
LightGreen300 = &color.RGBA{R: 0xae, G: 0xd5, B: 0x81, A: 0xff}
LightGreen400 = &color.RGBA{R: 0x9c, G: 0xcc, B: 0x65, A: 0xff}
LightGreen500 = &color.RGBA{R: 0x8b, G: 0xc3, B: 0x4a, A: 0xff}
LightGreen600 = &color.RGBA{R: 0x7c, G: 0xb3, B: 0x42, A: 0xff}
LightGreen700 = &color.RGBA{R: 0x68, G: 0x9f, B: 0x38, A: 0xff}
LightGreen800 = &color.RGBA{R: 0x55, G: 0x8b, B: 0x2f, A: 0xff}
LightGreen900 = &color.RGBA{R: 0x33, G: 0x69, B: 0x1e, A: 0xff}
LightGreenA100 = &color.RGBA{R: 0xcc, G: 0xff, B: 0x90, A: 0xff}
LightGreenA200 = &color.RGBA{R: 0xb2, G: 0xff, B: 0x59, A: 0xff}
LightGreenA400 = &color.RGBA{R: 0x76, G: 0xff, B: 0x03, A: 0xff}
LightGreenA700 = &color.RGBA{R: 0x64, G: 0xdd, B: 0x17, A: 0xff}
Lime50 = &color.RGBA{R: 0xf9, G: 0xfb, B: 0xe7, A: 0xff}
Lime100 = &color.RGBA{R: 0xf0, G: 0xf4, B: 0xc3, A: 0xff}
Lime200 = &color.RGBA{R: 0xe6, G: 0xee, B: 0x9c, A: 0xff}
Lime300 = &color.RGBA{R: 0xdc, G: 0xe7, B: 0x75, A: 0xff}
Lime400 = &color.RGBA{R: 0xd4, G: 0xe1, B: 0x57, A: 0xff}
Lime500 = &color.RGBA{R: 0xcd, G: 0xdc, B: 0x39, A: 0xff}
Lime600 = &color.RGBA{R: 0xc0, G: 0xca, B: 0x33, A: 0xff}
Lime700 = &color.RGBA{R: 0xaf, G: 0xb4, B: 0x2b, A: 0xff}
Lime800 = &color.RGBA{R: 0x9e, G: 0x9d, B: 0x24, A: 0xff}
Lime900 = &color.RGBA{R: 0x82, G: 0x77, B: 0x17, A: 0xff}
LimeA100 = &color.RGBA{R: 0xf4, G: 0xff, B: 0x81, A: 0xff}
LimeA200 = &color.RGBA{R: 0xee, G: 0xff, B: 0x41, A: 0xff}
LimeA400 = &color.RGBA{R: 0xc6, G: 0xff, B: 0x00, A: 0xff}
LimeA700 = &color.RGBA{R: 0xae, G: 0xea, B: 0x00, A: 0xff}
Yellow50 = &color.RGBA{R: 0xff, G: 0xfd, B: 0xe7, A: 0xff}
Yellow100 = &color.RGBA{R: 0xff, G: 0xf9, B: 0xc4, A: 0xff}
Yellow200 = &color.RGBA{R: 0xff, G: 0xf5, B: 0x9d, A: 0xff}
Yellow300 = &color.RGBA{R: 0xff, G: 0xf1, B: 0x76, A: 0xff}
Yellow400 = &color.RGBA{R: 0xff, G: 0xee, B: 0x58, A: 0xff}
Yellow500 = &color.RGBA{R: 0xff, G: 0xeb, B: 0x3b, A: 0xff}
Yellow600 = &color.RGBA{R: 0xfd, G: 0xd8, B: 0x35, A: 0xff}
Yellow700 = &color.RGBA{R: 0xfb, G: 0xc0, B: 0x2d, A: 0xff}
Yellow800 = &color.RGBA{R: 0xf9, G: 0xa8, B: 0x25, A: 0xff}
Yellow900 = &color.RGBA{R: 0xf5, G: 0x7f, B: 0x17, A: 0xff}
YellowA100 = &color.RGBA{R: 0xff, G: 0xff, B: 0x8d, A: 0xff}
YellowA200 = &color.RGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}
YellowA400 = &color.RGBA{R: 0xff, G: 0xea, B: 0x00, A: 0xff}
YellowA700 = &color.RGBA{R: 0xff, G: 0xd6, B: 0x00, A: 0xff}
Amber50 = &color.RGBA{R: 0xff, G: 0xf8, B: 0xe1, A: 0xff}
Amber100 = &color.RGBA{R: 0xff, G: 0xec, B: 0xb3, A: 0xff}
Amber200 = &color.RGBA{R: 0xff, G: 0xe0, B: 0x82, A: 0xff}
Amber300 = &color.RGBA{R: 0xff, G: 0xd5, B: 0x4f, A: 0xff}
Amber400 = &color.RGBA{R: 0xff, G: 0xca, B: 0x28, A: 0xff}
Amber500 = &color.RGBA{R: 0xff, G: 0xc1, B: 0x07, A: 0xff}
Amber600 = &color.RGBA{R: 0xff, G: 0xb3, B: 0x00, A: 0xff}
Amber700 = &color.RGBA{R: 0xff, G: 0xa0, B: 0x00, A: 0xff}
Amber800 = &color.RGBA{R: 0xff, G: 0x8f, B: 0x00, A: 0xff}
Amber900 = &color.RGBA{R: 0xff, G: 0x6f, B: 0x00, A: 0xff}
AmberA100 = &color.RGBA{R: 0xff, G: 0xe5, B: 0x7f, A: 0xff}
AmberA200 = &color.RGBA{R: 0xff, G: 0xd7, B: 0x40, A: 0xff}
AmberA400 = &color.RGBA{R: 0xff, G: 0xc4, B: 0x00, A: 0xff}
AmberA700 = &color.RGBA{R: 0xff, G: 0xab, B: 0x00, A: 0xff}
Orange50 = &color.RGBA{R: 0xff, G: 0xf3, B: 0xe0, A: 0xff}
Orange100 = &color.RGBA{R: 0xff, G: 0xe0, B: 0xb2, A: 0xff}
Orange200 = &color.RGBA{R: 0xff, G: 0xcc, B: 0x80, A: 0xff}
Orange300 = &color.RGBA{R: 0xff, G: 0xb7, B: 0x4d, A: 0xff}
Orange400 = &color.RGBA{R: 0xff, G: 0xa7, B: 0x26, A: 0xff}
Orange500 = &color.RGBA{R: 0xff, G: 0x98, B: 0x00, A: 0xff}
Orange600 = &color.RGBA{R: 0xfb, G: 0x8c, B: 0x00, A: 0xff}
Orange700 = &color.RGBA{R: 0xf5, G: 0x7c, B: 0x00, A: 0xff}
Orange800 = &color.RGBA{R: 0xef, G: 0x6c, B: 0x00, A: 0xff}
Orange900 = &color.RGBA{R: 0xe6, G: 0x51, B: 0x00, A: 0xff}
OrangeA100 = &color.RGBA{R: 0xff, G: 0xd1, B: 0x80, A: 0xff}
OrangeA200 = &color.RGBA{R: 0xff, G: 0xab, B: 0x40, A: 0xff}
OrangeA400 = &color.RGBA{R: 0xff, G: 0x91, B: 0x00, A: 0xff}
OrangeA700 = &color.RGBA{R: 0xff, G: 0x6d, B: 0x00, A: 0xff}
DeepOrange50 = &color.RGBA{R: 0xfb, G: 0xe9, B: 0xe7, A: 0xff}
DeepOrange100 = &color.RGBA{R: 0xff, G: 0xcc, B: 0xbc, A: 0xff}
DeepOrange200 = &color.RGBA{R: 0xff, G: 0xab, B: 0x91, A: 0xff}
DeepOrange300 = &color.RGBA{R: 0xff, G: 0x8a, B: 0x65, A: 0xff}
DeepOrange400 = &color.RGBA{R: 0xff, G: 0x70, B: 0x43, A: 0xff}
DeepOrange500 = &color.RGBA{R: 0xff, G: 0x57, B: 0x22, A: 0xff}
DeepOrange600 = &color.RGBA{R: 0xf4, G: 0x51, B: 0x1e, A: 0xff}
DeepOrange700 = &color.RGBA{R: 0xe6, G: 0x4a, B: 0x19, A: 0xff}
DeepOrange800 = &color.RGBA{R: 0xd8, G: 0x43, B: 0x15, A: 0xff}
DeepOrange900 = &color.RGBA{R: 0xbf, G: 0x36, B: 0x0c, A: 0xff}
DeepOrangeA100 = &color.RGBA{R: 0xff, G: 0x9e, B: 0x80, A: 0xff}
DeepOrangeA200 = &color.RGBA{R: 0xff, G: 0x6e, B: 0x40, A: 0xff}
DeepOrangeA400 = &color.RGBA{R: 0xff, G: 0x3d, B: 0x00, A: 0xff}
DeepOrangeA700 = &color.RGBA{R: 0xdd, G: 0x2c, B: 0x00, A: 0xff}
Brown50 = &color.RGBA{R: 0xef, G: 0xeb, B: 0xe9, A: 0xff}
Brown100 = &color.RGBA{R: 0xd7, G: 0xcc, B: 0xc8, A: 0xff}
Brown200 = &color.RGBA{R: 0xbc, G: 0xaa, B: 0xa4, A: 0xff}
Brown300 = &color.RGBA{R: 0xa1, G: 0x88, B: 0x7f, A: 0xff}
Brown400 = &color.RGBA{R: 0x8d, G: 0x6e, B: 0x63, A: 0xff}
Brown500 = &color.RGBA{R: 0x79, G: 0x55, B: 0x48, A: 0xff}
Brown600 = &color.RGBA{R: 0x6d, G: 0x4c, B: 0x41, A: 0xff}
Brown700 = &color.RGBA{R: 0x5d, G: 0x40, B: 0x37, A: 0xff}
Brown800 = &color.RGBA{R: 0x4e, G: 0x34, B: 0x2e, A: 0xff}
Brown900 = &color.RGBA{R: 0x3e, G: 0x27, B: 0x23, A: 0xff}
Grey50 = &color.RGBA{R: 0xfa, G: 0xfa, B: 0xfa, A: 0xff}
Grey100 = &color.RGBA{R: 0xf5, G: 0xf5, B: 0xf5, A: 0xff}
Grey200 = &color.RGBA{R: 0xee, G: 0xee, B: 0xee, A: 0xff}
Grey300 = &color.RGBA{R: 0xe0, G: 0xe0, B: 0xe0, A: 0xff}
Grey400 = &color.RGBA{R: 0xbd, G: 0xbd, B: 0xbd, A: 0xff}
Grey500 = &color.RGBA{R: 0x9e, G: 0x9e, B: 0x9e, A: 0xff}
Grey600 = &color.RGBA{R: 0x75, G: 0x75, B: 0x75, A: 0xff}
Grey700 = &color.RGBA{R: 0x61, G: 0x61, B: 0x61, A: 0xff}
Grey800 = &color.RGBA{R: 0x42, G: 0x42, B: 0x42, A: 0xff}
Grey900 = &color.RGBA{R: 0x21, G: 0x21, B: 0x21, A: 0xff}
BlueGrey50 = &color.RGBA{R: 0xec, G: 0xef, B: 0xf1, A: 0xff}
BlueGrey100 = &color.RGBA{R: 0xcf, G: 0xd8, B: 0xdc, A: 0xff}
BlueGrey200 = &color.RGBA{R: 0xb0, G: 0xbe, B: 0xc5, A: 0xff}
BlueGrey300 = &color.RGBA{R: 0x90, G: 0xa4, B: 0xae, A: 0xff}
BlueGrey400 = &color.RGBA{R: 0x78, G: 0x90, B: 0x9c, A: 0xff}
BlueGrey500 = &color.RGBA{R: 0x60, G: 0x7d, B: 0x8b, A: 0xff}
BlueGrey600 = &color.RGBA{R: 0x54, G: 0x6e, B: 0x7a, A: 0xff}
BlueGrey700 = &color.RGBA{R: 0x45, G: 0x5a, B: 0x64, A: 0xff}
BlueGrey800 = &color.RGBA{R: 0x37, G: 0x47, B: 0x4f, A: 0xff}
BlueGrey900 = &color.RGBA{R: 0x26, G: 0x32, B: 0x38, A: 0xff}
Black = &color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
White = &color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
)
var Colors500 = []*color.RGBA{Red500, Pink500, Purple500, DeepPurple500, Indigo500, Blue500, LightBlue500, Cyan500, Teal500, Green500, LightGreen500, Lime500, Yellow500, Amber500, Orange500, DeepOrange500}

106
ui/container.go Normal file
View File

@ -0,0 +1,106 @@
package ui
import (
"time"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type Container interface {
Control
Children() []Control
Arrange(Context, geom.RectangleF)
}
func NewContainer(parent Container, children ...Control) *ContainerBase {
return &ContainerBase{ControlBase{}, false, parent, children}
}
type ContainerBase struct {
ControlBase
created bool
parent Container
children []Control
}
func (c *ContainerBase) Children() []Control {
return c.children
}
func (c *ContainerBase) Arrange(ctx Context, rect geom.RectangleF) {
c.ControlBase.SetRect(rect)
for _, child := range c.children {
Arrange(ctx, child, rect)
}
}
func (c *ContainerBase) Append(child Control) {
if c.created {
panic("error append, must use append create when control is already created")
}
c.children = append(c.children, child)
}
func (c *ContainerBase) AppendCreate(ctx Context, child Control) error {
if !c.created {
panic("error append create, must use append when control is not yet created")
}
c.children = append(c.children, child)
return child.Created(ctx, c)
}
func (c *ContainerBase) RemoveFn(ctx Context, pred func(Control) bool) {
var i = 0
for i < len(c.children) {
if pred(c.children[i]) {
c.children[i].Destroyed(ctx)
c.children = c.children[:i+copy(c.children[i:], c.children[i+1:])]
} else {
i++
}
}
}
func (c *ContainerBase) Created(ctx Context, p Container) error {
if c.created {
panic("already created")
}
c.parent = p
for _, child := range c.children {
err := child.Created(ctx, p)
if nil != err {
return err
}
}
c.created = true
return nil
}
func (c *ContainerBase) Destroyed(ctx Context) {
if !c.created {
panic("not yet created or already destroyed")
}
for _, child := range c.children {
child.Destroyed(ctx)
}
}
func (c *ContainerBase) Update(ctx Context, dt time.Duration) {
for _, child := range c.children {
child.Update(ctx, dt)
}
}
func (c *ContainerBase) Handle(ctx Context, ev allegro5.Event) {
for _, child := range c.children {
child.Handle(ctx, ev)
}
}
func (c *ContainerBase) Render(ctx Context) {
for _, child := range c.children {
child.Render(ctx)
}
c.ControlBase.Render(ctx)
}

189
ui/contentscrollbar.go Normal file
View File

@ -0,0 +1,189 @@
package ui
import (
"math"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
var _ Control = &ContentScrollbar{}
type ContentScrollbarValueChangedFn func(float64)
type ContentScrollbar struct {
ControlBase
Length float64
Value float64
Orientation Orientation
OnChanged ContentScrollbarValueChangedFn
handle *contentScrollbarHandle
}
type contentScrollbarHandle struct {
ControlBase
}
func (h *contentScrollbarHandle) Render(ctx Context) {
var c = ctx.Palette().Primary()
if h.IsOver {
if h.IsPressed {
} else {
}
}
var min = h.Bounds.Min.To32()
var max = h.Bounds.Max.To32()
allegro5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, c)
}
func (s *ContentScrollbar) Created(ctx Context, p Container) error {
s.ControlBase.Created(ctx, p)
s.handle = &contentScrollbarHandle{}
s.handle.Created(ctx, nil)
return nil
}
func (s *ContentScrollbar) Destroyed(ctx Context) {
s.handle.Destroyed(ctx)
}
func (s *ContentScrollbar) barViewRange() (float64, float64) {
var min, max float64
switch s.Orientation {
case OrientationHorizontal:
min = s.Bounds.Min.X
max = s.Bounds.Max.X
default:
min = s.Bounds.Max.Y
max = s.Bounds.Min.Y
}
var length = (max - min)
var bar = length
if s.Length > length {
bar = length * length / s.Length
}
if bar < 20 {
if length < 40 {
bar = .5 * length
} else {
bar = 20
}
}
return min + .5*bar, max - .5*bar
}
func (s *ContentScrollbar) barViewCenter() float64 {
switch s.Orientation {
case OrientationHorizontal:
return (s.Bounds.Min.Y + s.Bounds.Max.Y) * .5
default:
return (s.Bounds.Min.X + s.Bounds.Max.X) * .5
}
}
func (s *ContentScrollbar) toValue(x, y float64) float64 {
var n = y
if OrientationHorizontal == s.Orientation {
n = x
}
var min, max = s.barViewRange()
if min == max {
return 0
}
var delta = s.Length - (max - min)
var off = (n - min) / (max - min)
var v = off * (s.Length - (max - min))
if v < 0 {
v = 0
} else if v > delta {
v = delta
}
return v
}
func (s *ContentScrollbar) change(v float64) {
if v != s.Value {
s.Value = v
var onChanged = s.OnChanged
if nil != onChanged {
onChanged(v)
}
}
}
func (s *ContentScrollbar) snapTo(x, y int) {
var val = s.toValue(float64(x), float64(y))
s.change(val)
}
func (s *ContentScrollbar) increment(d int) {
// var val = s.Value + d
// if val < s.Minimum {
// val = s.Minimum
// } else if val > s.Maximum {
// val = s.Maximum
// }
// s.change(val)
}
func (s *ContentScrollbar) Handle(ctx Context, ev allegro5.Event) {
s.ControlBase.Handle(ctx, ev)
s.handle.Handle(ctx, ev)
switch e := ev.(type) {
case *allegro5.MouseMoveEvent:
if s.handle.IsPressed {
s.snapTo(e.X, e.Y)
}
if 0 != e.DeltaZ && s.IsOver {
var d = e.DeltaZ
if allegro5.IsAnyKeyDown(allegro5.KeyLShift, allegro5.KeyRShift) {
d *= 10
}
s.increment(d)
}
case *allegro5.MouseButtonDownEvent:
if !s.handle.IsPressed && s.IsOver {
s.snapTo(e.X, e.Y)
}
}
}
func (s *ContentScrollbar) DesiredSize(Context) geom.PointF {
switch s.Orientation {
case OrientationHorizontal:
return geom.PtF(math.NaN(), ScrollbarWidth)
}
return geom.PtF(ScrollbarWidth, math.NaN())
}
func (s *ContentScrollbar) SetRect(rect geom.RectangleF) {
switch s.Orientation {
case OrientationHorizontal:
if rect.Dy() > ScrollbarWidth {
rect.Min.Y = rect.Max.Y - ScrollbarWidth
}
default:
if rect.Dx() > ScrollbarWidth {
rect.Min.X = rect.Max.X - ScrollbarWidth
}
}
s.ControlBase.SetRect(rect)
// var min, max = s.barViewRange()
// var off = float64(s.Value-s.Minimum) / float64(s.Maximum-s.Minimum)
// var centerH = min + (max-min)*off
// var r = 0.5 * float64(s.handles[0].Width())
// var center = s.barViewCenter()
// switch s.Orientation {
// case OrientationHorizontal:
// s.handle.SetRect(geom.RectF(centerH-r, center-r, centerH+r, center+r))
// default:
// s.handle.SetRect(geom.RectF(center-r, centerH-r, center+r, centerH+r))
// }
}
func (s *ContentScrollbar) Render(ctx Context) {
s.handle.Render(ctx)
s.ControlBase.Render(ctx)
}

72
ui/context.go Normal file
View File

@ -0,0 +1,72 @@
package ui
import "opslag.de/schobers/galleg/allegro5"
type Context interface {
Display() *allegro5.Display
Fonts() Fonts
Palette() Palette
Debug() Debug
}
type Debug interface {
IsEnabled() bool
Rainbow() allegro5.Color
}
var _ Context = &context{}
type context struct {
disp *allegro5.Display
fts Fonts
pal Palette
dbg *debug
}
type debug struct {
enbl bool
rainb []allegro5.Color
col int
}
func rainbow() []allegro5.Color {
var colors = make([]allegro5.Color, len(Colors500))
for i, c := range Colors500 {
colors[i] = NewColorAlpha(c, 0xbf)
}
return colors
}
func newContext(disp *allegro5.Display, f Fonts) *context {
return &context{disp, f, DefaultPalette(), &debug{rainb: rainbow()}}
}
func (c *context) Display() *allegro5.Display {
return c.disp
}
func (c *context) Fonts() Fonts {
return c.fts
}
func (c *context) Palette() Palette {
return c.pal
}
func (c *context) Debug() Debug {
return c.dbg
}
func (d *debug) IsEnabled() bool {
return d.enbl
}
func (d *debug) resetRainbow() {
d.col = 0
}
func (d *debug) Rainbow() allegro5.Color {
var col = d.col
d.col = (col + 1) % len(d.rainb)
return d.rainb[col]
}

89
ui/control.go Normal file
View File

@ -0,0 +1,89 @@
package ui
import (
"time"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type Control interface {
Created(Context, Container) error
Destroyed(Context)
Update(Context, time.Duration)
Handle(Context, allegro5.Event)
DesiredSize(Context) geom.PointF
SetRect(geom.RectangleF)
Render(Context)
}
type MouseClickFn func(Control)
var _ Control = &ControlBase{}
type ControlBase struct {
Parent Container
Bounds geom.RectangleF
Disabled bool
IsOver bool
IsPressed bool
OnClick MouseClickFn
MinSize geom.PointF
Background *allegro5.Color
}
func (c *ControlBase) Created(_ Context, p Container) error {
c.Parent = p
return nil
}
func (c *ControlBase) Destroyed(Context) {}
func (c *ControlBase) Update(Context, time.Duration) {}
func (c *ControlBase) Handle(ctx Context, ev allegro5.Event) {
switch e := ev.(type) {
case *allegro5.MouseMoveEvent:
c.IsOver = c.IsInRect(float64(e.X), float64(e.Y))
case *allegro5.MouseButtonDownEvent:
if c.IsOver {
c.IsPressed = true
}
case *allegro5.MouseButtonUpEvent:
if c.IsPressed && c.IsOver {
var onClick = c.OnClick
if nil != onClick {
onClick(c)
}
}
c.IsPressed = false
}
}
func (c *ControlBase) DesiredSize(Context) geom.PointF {
return c.MinSize
}
func (c *ControlBase) SetRect(rect geom.RectangleF) {
c.Bounds = rect
}
func (c *ControlBase) Render(ctx Context) {
var min = c.Bounds.Min.To32()
var max = c.Bounds.Max.To32()
if nil != c.Background {
allegro5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, *c.Background)
}
if ctx.Debug().IsEnabled() {
allegro5.DrawRectangle(min.X, min.Y, max.X, max.Y, ctx.Debug().Rainbow(), 5)
}
}
func (c *ControlBase) Rect() geom.RectangleF {
return c.Bounds
}
func (c *ControlBase) IsInRect(x, y float64) bool {
return geom.PtF(x, y).In(c.Bounds)
}

5
ui/dimensions.go Normal file
View File

@ -0,0 +1,5 @@
package ui
const topMargin = 4
const leftMargin = 8
const checkboxSize = 12

146
ui/dockpanel.go Normal file
View File

@ -0,0 +1,146 @@
package ui
import (
"math"
"opslag.de/schobers/geom"
)
type DockPanel interface {
Container
Append(o Dock, children ...Control) error
AppendCreate(ctx Context, o Dock, children ...Control) error
}
type Dock int
const (
DockLeft Dock = iota
DockTop
DockRight
DockBottom
)
type dockPanel struct {
ContainerBase
docks []Dock
}
func NewDockPanel(parent Container) DockPanel {
return &dockPanel{ContainerBase: ContainerBase{parent: parent}}
}
func (p *dockPanel) Append(d Dock, children ...Control) error {
for _, child := range children {
p.ContainerBase.Append(child)
p.docks = append(p.docks, d)
}
return nil
}
func (p *dockPanel) AppendCreate(ctx Context, d Dock, children ...Control) error {
for _, child := range children {
err := p.ContainerBase.AppendCreate(ctx, child)
if nil != err {
return err
}
p.docks = append(p.docks, d)
}
return nil
}
func (p *dockPanel) DesiredSize(ctx Context) geom.PointF {
var width, height float64 = 0, 0
for i, child := range p.children {
var d = p.docks[i]
var desired = child.DesiredSize(ctx)
switch {
case d == DockLeft || d == DockRight:
if !math.IsNaN(width) {
if math.IsNaN(desired.X) {
width = desired.X
} else {
width += desired.X
}
}
if !math.IsNaN(desired.Y) {
height = math.Max(height, desired.Y)
}
case d == DockTop || d == DockBottom:
if !math.IsNaN(height) {
if math.IsNaN(desired.Y) {
height = desired.Y
} else {
height += desired.Y
}
}
if !math.IsNaN(desired.X) {
width = math.Max(width, desired.X)
}
}
}
return geom.PtF(width, height)
}
func (p *dockPanel) arrangeChildren(ctx Context) []geom.RectangleF {
var n = len(p.children)
var rects = make([]geom.RectangleF, n)
var rem = p.Bounds
var last = n - 1
for i, child := range p.children {
if last == i {
rects[i] = rem
} else {
var d = p.docks[i]
var desired = child.DesiredSize(ctx)
var width, height, top, left float64
switch {
case d == DockLeft || d == DockRight:
width = rem.Dx()
if !math.IsNaN(desired.X) {
width = math.Min(desired.X, width)
}
if d == DockLeft {
left = rem.Min.X
} else {
left = rem.Max.X - width
}
top = rem.Min.Y
height = rem.Dy()
case d == DockTop || d == DockBottom:
height = rem.Dy()
if !math.IsNaN(desired.Y) {
height = math.Min(desired.Y, height)
}
if d == DockTop {
top = rem.Min.Y
} else {
top = rem.Max.Y - height
}
left = rem.Min.X
width = rem.Dx()
}
rects[i] = geom.RectF(left, top, left+width, top+height)
switch d {
case DockLeft:
rem = geom.RectF(rem.Min.X+width, rem.Min.Y, rem.Max.X, rem.Max.Y)
case DockTop:
rem = geom.RectF(rem.Min.X, rem.Min.Y+height, rem.Max.X, rem.Max.Y)
case DockRight:
rem = geom.RectF(rem.Min.X, rem.Min.Y, rem.Max.X-width, rem.Max.Y)
case DockBottom:
rem = geom.RectF(rem.Min.X, rem.Min.Y, rem.Max.X, rem.Max.Y-height)
}
}
}
return rects
}
func (p *dockPanel) Arrange(ctx Context, rect geom.RectangleF) {
p.ContainerBase.SetRect(rect)
var rects = p.arrangeChildren(ctx)
for i, child := range p.children {
Arrange(ctx, child, rects[i])
}
}

65
ui/fonts.go Normal file
View File

@ -0,0 +1,65 @@
package ui
import (
"github.com/spf13/afero"
"opslag.de/schobers/fs/vfs"
"opslag.de/schobers/galleg/allegro5"
)
type Fonts interface {
Register(name, path string, size int) error
Get(name string) *allegro5.Font
Destroy()
}
type fonts struct {
fts map[string]*allegro5.Font
dir vfs.CopyDir
}
func newFonts(dir vfs.CopyDir) *fonts {
return &fonts{make(map[string]*allegro5.Font), dir}
}
func NewFonts(fs afero.Fs) (Fonts, error) {
var dir, err = vfs.NewCopyDir(fs)
if nil != err {
return nil, err
}
return newFonts(dir), nil
}
func (fts *fonts) load(name, path string, size int) error {
f, err := allegro5.LoadTTFFont(path, size)
if nil != err {
return err
}
fts.fts[name] = f
return nil
}
func (fts *fonts) loadFromBox(name, path string, size int) error {
path, err := fts.dir.Retrieve(path)
if nil != err {
return err
}
return fts.load(name, path, size)
}
func (fts *fonts) Register(name, path string, size int) error {
if nil != fts.dir {
return fts.loadFromBox(name, path, size)
}
return fts.load(name, path, size)
}
func (fts *fonts) Get(name string) *allegro5.Font {
return fts.fts[name]
}
func (fts *fonts) Destroy() {
for _, f := range fts.fts {
f.Destroy()
}
fts.dir.Destroy()
}

19
ui/label.go Normal file
View File

@ -0,0 +1,19 @@
package ui
import "opslag.de/schobers/galleg/allegro5"
type Label struct {
ControlBase
Text string
}
func (l *Label) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = l.Bounds.Min.To32()
var fnt = fonts.Get("default")
fnt.Draw(min.X+leftMargin, min.Y+topMargin, ctx.Palette().Black(), allegro5.AlignLeft, l.Text)
l.ControlBase.Render(ctx)
}

121
ui/loop.go Normal file
View File

@ -0,0 +1,121 @@
package ui
import (
"time"
"opslag.de/schobers/galleg/allegro5"
)
func shouldClose(ev allegro5.Event) bool {
switch e := ev.(type) {
case *allegro5.KeyCharEvent:
switch e.KeyCode {
case allegro5.KeyEscape:
return true
case allegro5.KeyF4:
if e.Modifiers&allegro5.KeyModAlt == allegro5.KeyModAlt {
return true
}
}
case *allegro5.DisplayCloseEvent:
return true
}
return false
}
func shouldToggleDebug(ev allegro5.Event) bool {
switch e := ev.(type) {
case *allegro5.KeyCharEvent:
switch e.KeyCode {
case allegro5.KeyD:
return e.Modifiers&allegro5.KeyModAlt == allegro5.KeyModAlt
}
}
return false
}
func switchState(ctx Context, curr State, s State) error {
var err error
err = curr.Leave(ctx)
if nil != err {
return err
}
return s.Enter(ctx)
}
func Init() error {
return allegro5.Init(allegro5.InitAll)
}
func Run(w, h int, title string, s State, f Fonts, opts *allegro5.NewDisplayOptions) error {
if nil == opts {
opts = &allegro5.NewDisplayOptions{}
}
disp, err := allegro5.NewDisplay(w, h, *opts)
if nil != err {
return err
}
disp.SetWindowTitle(title)
defer disp.Destroy()
evq, err := allegro5.NewEventQueue()
if nil != err {
return err
}
evq.RegisterDisplay(disp)
evq.RegisterKeyboard()
evq.RegisterMouse()
defer evq.Destroy()
ctx := newContext(disp, f)
var state = s
err = state.Enter(ctx)
if nil != err {
return err
}
var t = time.Now()
main:
for nil != state {
for ev := evq.Get(); ev != nil; ev = evq.Get() {
if shouldClose(ev) {
break main
}
if shouldToggleDebug(ev) {
ctx.dbg.enbl = !ctx.dbg.enbl
}
err := state.Handle(ctx, ev)
if nil != err {
return err
}
}
var now = time.Now()
var dt = now.Sub(t)
var s, err = state.Update(ctx, dt)
if nil != err {
return err
}
t = now
if s != nil {
err = switchState(ctx, state, s)
if nil != err {
return err
}
state = s
}
ctx.dbg.resetRainbow()
err = state.Render(ctx)
if nil != err {
return err
}
disp.Flip()
time.Sleep(10 * time.Millisecond)
}
err = state.Leave(ctx)
if nil != err {
return err
}
return nil
}

19
ui/orientation.go Normal file
View File

@ -0,0 +1,19 @@
package ui
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationHorizontal
)
func (o Orientation) String() string {
switch o {
case OrientationVertical:
return "vertical"
case OrientationHorizontal:
return "horizontal"
default:
return "unknown"
}
}

51
ui/palette.go Normal file
View File

@ -0,0 +1,51 @@
package ui
import (
"image/color"
"opslag.de/schobers/galleg/allegro5"
)
type Palette interface {
Primary() allegro5.Color
White() allegro5.Color
Black() allegro5.Color
Disabled() allegro5.Color
}
type palette struct {
primary allegro5.Color
white allegro5.Color
black allegro5.Color
disabled allegro5.Color
}
func (p *palette) Primary() allegro5.Color {
return p.primary
}
func (p *palette) White() allegro5.Color {
return p.white
}
func (p *palette) Black() allegro5.Color {
return p.black
}
func (p *palette) Disabled() allegro5.Color {
return p.disabled
}
func NewColor(c *color.RGBA) allegro5.Color {
return allegro5.NewColorAlpha(c.R, c.G, c.B, c.A)
}
func NewColorAlpha(c *color.RGBA, a uint8) allegro5.Color {
return allegro5.NewColorAlpha(c.R, c.G, c.B, a)
}
func DefaultPalette() Palette {
return &palette{
primary: NewColor(Blue500),
white: allegro5.NewColor(0xff, 0xff, 0xff),
black: allegro5.NewColor(0, 0, 0),
disabled: allegro5.NewColorAlpha(0x1f, 0x1f, 0x1f, 0x1f),
}
}

26
ui/queue.go Normal file
View File

@ -0,0 +1,26 @@
package ui
type Queue struct {
q chan func(Context)
}
func NewQueue() *Queue {
return &Queue{make(chan func(Context), 4)}
}
func (q *Queue) Do(act func(Context)) {
go func() { // Non-blocking queue
q.q <- act
}()
}
func (q *Queue) Process(ctx Context) {
for {
select {
case act := <-q.q:
act(ctx)
default:
return
}
}
}

213
ui/scrollbar.go Normal file
View File

@ -0,0 +1,213 @@
package ui
import (
"math"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
var _ Control = &Scrollbar{}
const ScrollbarWidth = 26
const ScrollbarHandleThickness = 4
const ScrollbarHandlePadding = 1
type ScrollbarValueChangedFn func(int)
type Scrollbar struct {
ControlBase
Minimum int
Maximum int
Value int
Orientation Orientation
OnChanged ScrollbarValueChangedFn
handle *ControlBase
handles []*allegro5.Bitmap
}
func (s *Scrollbar) Created(ctx Context, p Container) error {
s.ControlBase.Created(ctx, p)
s.handles = []*allegro5.Bitmap{
createCirle((ScrollbarWidth/2)-ScrollbarHandleThickness-2*ScrollbarHandlePadding, ScrollbarHandleThickness, 0, math.Pi*2, Blue500),
createCirle((ScrollbarWidth/2)-ScrollbarHandleThickness-2*ScrollbarHandlePadding, ScrollbarHandleThickness, 0, math.Pi*2, Blue300),
createCirle((ScrollbarWidth/2)-ScrollbarHandleThickness-2*ScrollbarHandlePadding, ScrollbarHandleThickness, 0, math.Pi*2, Blue700),
}
s.handle = &ControlBase{}
s.handle.Created(ctx, nil)
return nil
}
func (s *Scrollbar) Destroyed(ctx Context) {
if nil != s.handles {
for _, h := range s.handles {
h.Destroy()
}
}
s.handle.Destroyed(ctx)
}
func (s *Scrollbar) barViewRange() (float64, float64) {
var small bool
var min, max float64
switch s.Orientation {
case OrientationHorizontal:
min = s.Bounds.Min.X + ScrollbarWidth*.5
max = s.Bounds.Max.X - ScrollbarWidth*.5
small = min > max
default:
min = s.Bounds.Max.Y - ScrollbarWidth*.5
max = s.Bounds.Min.Y + ScrollbarWidth*.5
small = max > min
}
if small {
var center = (min + max) * .5
min, max = center, center
}
return min, max
}
func (s *Scrollbar) barViewCenter() float64 {
switch s.Orientation {
case OrientationHorizontal:
return (s.Bounds.Min.Y + s.Bounds.Max.Y) * .5
default:
return (s.Bounds.Min.X + s.Bounds.Max.X) * .5
}
}
func (s *Scrollbar) toValue(x, y float64) int {
var n = y
if OrientationHorizontal == s.Orientation {
n = x
}
var min, max = s.barViewRange()
if min == max {
return s.Minimum
}
var off = (n - min) / (max - min)
var v = s.Minimum + int(off*float64(s.Maximum-s.Minimum)+.5)
if v < s.Minimum {
v = s.Minimum
} else if v > s.Maximum {
v = s.Maximum
}
return v
}
func (s *Scrollbar) change(v int) {
if v != s.Value {
s.Value = v
var onChanged = s.OnChanged
if nil != onChanged {
onChanged(v)
}
}
}
func (s *Scrollbar) snapTo(x, y int) {
var val = s.toValue(float64(x), float64(y))
s.change(val)
}
func (s *Scrollbar) increment(d int) {
var val = s.Value + d
if val < s.Minimum {
val = s.Minimum
} else if val > s.Maximum {
val = s.Maximum
}
s.change(val)
}
func (s *Scrollbar) Handle(ctx Context, ev allegro5.Event) {
s.ControlBase.Handle(ctx, ev)
s.handle.Handle(ctx, ev)
switch e := ev.(type) {
case *allegro5.MouseMoveEvent:
if s.handle.IsPressed {
s.snapTo(e.X, e.Y)
}
if 0 != e.DeltaZ && s.IsOver {
var d = e.DeltaZ
if allegro5.IsAnyKeyDown(allegro5.KeyLShift, allegro5.KeyRShift) {
d *= 10
}
s.increment(d)
}
case *allegro5.MouseButtonDownEvent:
if !s.handle.IsPressed && s.IsOver {
s.snapTo(e.X, e.Y)
}
}
}
func (s *Scrollbar) DesiredSize(Context) geom.PointF {
switch s.Orientation {
case OrientationHorizontal:
return geom.PtF(math.NaN(), ScrollbarWidth)
}
return geom.PtF(ScrollbarWidth, math.NaN())
}
func (s *Scrollbar) SetRect(rect geom.RectangleF) {
switch s.Orientation {
case OrientationHorizontal:
if rect.Dy() > ScrollbarWidth {
rect.Min.Y = rect.Max.Y - ScrollbarWidth
}
default:
if rect.Dx() > ScrollbarWidth {
rect.Min.X = rect.Max.X - ScrollbarWidth
}
}
s.ControlBase.SetRect(rect)
var min, max = s.barViewRange()
var off = float64(s.Value-s.Minimum) / float64(s.Maximum-s.Minimum)
var centerH = min + (max-min)*off
var r = 0.5 * float64(s.handles[0].Width())
var center = s.barViewCenter()
switch s.Orientation {
case OrientationHorizontal:
s.handle.SetRect(geom.RectF(centerH-r, center-r, centerH+r, center+r))
default:
s.handle.SetRect(geom.RectF(center-r, centerH-r, center+r, centerH+r))
}
}
func (s *Scrollbar) Render(ctx Context) {
var center = float32(s.barViewCenter())
var min64, max64 = s.barViewRange()
var min, max = float32(min64), float32(max64)
var minH = s.handle.Bounds.Min.To32()
var stateH = 0
if s.handle.IsOver {
stateH = 1
if s.handle.IsPressed {
stateH = 2
}
}
s.handles[stateH].Draw(minH.X, minH.Y)
var maxH = s.handle.Bounds.Max.To32()
switch s.Orientation {
case OrientationHorizontal:
if min < minH.X-1 { // Top line
allegro5.DrawLine(min, center, minH.X-1, center, ctx.Palette().Black(), 1)
}
if max > maxH.X+1 { // Bottom line
allegro5.DrawLine(maxH.X+1, center, max, center, ctx.Palette().Black(), 1)
}
default:
if max < minH.Y-1 { // Top line
allegro5.DrawLine(center, max, center, minH.Y-1, ctx.Palette().Black(), 1)
}
if min > maxH.Y+1 { // Bottom line
allegro5.DrawLine(center, maxH.Y+1, center, min, ctx.Palette().Black(), 1)
}
}
s.ControlBase.Render(ctx)
}

29
ui/shadow.go Normal file
View File

@ -0,0 +1,29 @@
package ui
import (
"opslag.de/schobers/galleg/allegro5"
)
var dropShadowPalette []allegro5.Color
func initDropShadowPalette() {
if nil != dropShadowPalette {
return
}
dropShadowPalette = []allegro5.Color{
allegro5.NewColorAlpha(0, 0, 0, 0x0f),
allegro5.NewColorAlpha(0, 0, 0, 0x07),
allegro5.NewColorAlpha(0, 0, 0, 0x03),
}
}
func DropShadow(x1, y1, x2, y2 float32) {
initDropShadowPalette()
allegro5.DrawLine(x1, y2+1, x2+1, y2+1, dropShadowPalette[0], 1)
allegro5.DrawLine(x2+1, y1, x2+1, y2, dropShadowPalette[0], 1)
allegro5.DrawLine(x1, y2+2, x2+2, y2+2, dropShadowPalette[1], 1)
allegro5.DrawLine(x2+2, y1, x2+2, y2+1, dropShadowPalette[1], 1)
allegro5.DrawLine(x1+1, y2+3, x2+2, y2+3, dropShadowPalette[2], 1)
allegro5.DrawLine(x2+3, y1+1, x2+3, y2+2, dropShadowPalette[2], 1)
}

97
ui/spinner.go Normal file
View File

@ -0,0 +1,97 @@
package ui
import (
"image"
"image/color"
"math"
"time"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/galleg/allegro5"
)
var _ Control = &Spinner{}
type Spinner struct {
ControlBase
Text string
spin float32
circs []*allegro5.Bitmap
}
func createCirle(r, w int, startAngle, a float64, c color.Color) *allegro5.Bitmap {
var width = 2*r + w
dest := image.NewRGBA(image.Rect(0, 0, width, width))
gc := draw2dimg.NewGraphicContext(dest)
gc.SetFillColor(color.Transparent)
gc.Clear()
gc.SetFillColor(c)
gc.SetStrokeColor(c)
gc.SetLineWidth(float64(w))
var rad = float64(r)
var cnt = float64(width) * .5
var dx1, dy1 = cnt + math.Cos(startAngle)*rad, cnt + math.Sin(startAngle)*rad
gc.MoveTo(dx1, dy1)
gc.ArcTo(cnt, cnt, rad, rad, startAngle, a)
gc.Stroke()
bmp, err := allegro5.NewBitmapFromImage(dest, false)
if nil != err {
return nil
}
return bmp
}
func (s *Spinner) Created(ctx Context, p Container) error {
s.ControlBase.Created(ctx, p)
const numCircs = 32
s.circs = make([]*allegro5.Bitmap, numCircs)
var am = math.Pi * 2 / float64(numCircs)
for i := range s.circs {
s.circs[i] = createCirle(8, 3, float64(i)*am, math.Pi, Blue500)
}
return nil
}
func (s *Spinner) Destroyed(ctx Context) {
for _, circ := range s.circs {
circ.Destroy()
}
}
func (s *Spinner) Update(ctx Context, dt time.Duration) {
var spin = float64(s.spin)
spin += dt.Seconds()
for spin > 1. {
spin -= 1.
}
s.spin = float32(spin)
}
func (s *Spinner) Render(ctx Context) {
var disp = ctx.Display()
var fonts = ctx.Fonts()
var width = float32(disp.Width())
var height = float32(disp.Height())
var fnt = fonts.Get("default")
var textW = fnt.TextWidth(s.Text)
allegro5.DrawFilledRectangle(0, 0, width, height, ctx.Palette().Disabled())
const marginH, marginV float32 = 64, 16
const textH float32 = 12
var rectW, rectH float32 = textW + 2*marginH, 3*marginV + textH + 32
var rectX, rectY = (width - rectW) * .5, (height - rectH) * .5
allegro5.DrawFilledRectangle(rectX, rectY, rectX+rectW, rectY+rectH, ctx.Palette().White())
DropShadow(rectX, rectY, rectX+rectW, rectY+rectH)
fnt.Draw(rectX+marginH, rectY+marginV, ctx.Palette().Black(), allegro5.AlignLeft, s.Text)
const numCircs = 32
var i = int(math.Floor(float64(s.spin) * numCircs))
s.circs[i].DrawOptions(width*.5, rectY+2*marginV+2*textH, allegro5.DrawOptions{Center: true})
s.ControlBase.Render(ctx)
}

70
ui/state.go Normal file
View File

@ -0,0 +1,70 @@
package ui
import (
"log"
"time"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type State interface {
Enter(Context) error
Leave(Context) error
Update(Context, time.Duration) (State, error)
Handle(Context, allegro5.Event) error
Render(Context) error
}
type StateBase struct {
Control Control
}
func (s *StateBase) Enter(ctx Context) error {
if nil != s.Control {
err := s.Control.Created(ctx, nil)
if nil != err {
log.Println("error creating control")
s.Control = nil
}
}
return nil
}
func (s *StateBase) Leave(ctx Context) error {
if nil != s.Control {
s.Control.Destroyed(ctx)
}
return nil
}
func (s *StateBase) Update(ctx Context, dt time.Duration) (State, error) {
if nil != s.Control {
s.Control.Update(ctx, dt)
}
return nil, nil
}
func (s *StateBase) Handle(ctx Context, ev allegro5.Event) error {
if nil != s.Control {
s.Control.Handle(ctx, ev)
}
return nil
}
func Arrange(ctx Context, c Control, rect geom.RectangleF) {
if cont, ok := c.(Container); ok {
cont.Arrange(ctx, rect)
} else {
c.SetRect(rect)
}
}
func (s *StateBase) Render(ctx Context) error {
if nil != s.Control {
var disp = ctx.Display()
Arrange(ctx, s.Control, geom.RectF(0, 0, float64(disp.Width()), float64(disp.Height())))
s.Control.Render(ctx)
}
return nil
}

36
ui/statusbar.go Normal file
View File

@ -0,0 +1,36 @@
package ui
import (
"math"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
var _ Control = &StatusBar{}
const statusBarHeight = 24
type StatusBar struct {
ControlBase
Text string
RightText string
}
func (b *StatusBar) DesiredSize(Context) geom.PointF {
return geom.PtF(math.NaN(), statusBarHeight)
}
func (b *StatusBar) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = b.Bounds.Min.To32()
var max = b.Bounds.Max.To32()
allegro5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, ctx.Palette().Primary())
var fnt = fonts.Get("default")
fnt.Draw(min.X+leftMargin, min.Y+topMargin, ctx.Palette().White(), allegro5.AlignLeft, b.Text)
fnt.Draw(max.X-leftMargin, min.Y+topMargin, ctx.Palette().White(), allegro5.AlignRight, b.RightText)
b.ControlBase.Render(ctx)
}