From 8157d8b3d8a5d8febdf5da3c2e24d4eda33aa5ec Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Fri, 3 Aug 2018 08:46:10 +0200 Subject: [PATCH] Some basic UI controls. --- ui/checkbox.go | 133 +++++++++++++++++++ ui/colors.go | 283 +++++++++++++++++++++++++++++++++++++++++ ui/container.go | 106 +++++++++++++++ ui/contentscrollbar.go | 189 +++++++++++++++++++++++++++ ui/context.go | 72 +++++++++++ ui/control.go | 89 +++++++++++++ ui/dimensions.go | 5 + ui/dockpanel.go | 146 +++++++++++++++++++++ ui/fonts.go | 65 ++++++++++ ui/label.go | 19 +++ ui/loop.go | 121 ++++++++++++++++++ ui/orientation.go | 19 +++ ui/palette.go | 51 ++++++++ ui/queue.go | 26 ++++ ui/scrollbar.go | 213 +++++++++++++++++++++++++++++++ ui/shadow.go | 29 +++++ ui/spinner.go | 97 ++++++++++++++ ui/state.go | 70 ++++++++++ ui/statusbar.go | 36 ++++++ 19 files changed, 1769 insertions(+) create mode 100644 ui/checkbox.go create mode 100644 ui/colors.go create mode 100644 ui/container.go create mode 100644 ui/contentscrollbar.go create mode 100644 ui/context.go create mode 100644 ui/control.go create mode 100644 ui/dimensions.go create mode 100644 ui/dockpanel.go create mode 100644 ui/fonts.go create mode 100644 ui/label.go create mode 100644 ui/loop.go create mode 100644 ui/orientation.go create mode 100644 ui/palette.go create mode 100644 ui/queue.go create mode 100644 ui/scrollbar.go create mode 100644 ui/shadow.go create mode 100644 ui/spinner.go create mode 100644 ui/state.go create mode 100644 ui/statusbar.go diff --git a/ui/checkbox.go b/ui/checkbox.go new file mode 100644 index 0000000..c92acf8 --- /dev/null +++ b/ui/checkbox.go @@ -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) +} diff --git a/ui/colors.go b/ui/colors.go new file mode 100644 index 0000000..9c12cb7 --- /dev/null +++ b/ui/colors.go @@ -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} diff --git a/ui/container.go b/ui/container.go new file mode 100644 index 0000000..156d241 --- /dev/null +++ b/ui/container.go @@ -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) +} diff --git a/ui/contentscrollbar.go b/ui/contentscrollbar.go new file mode 100644 index 0000000..f2e415b --- /dev/null +++ b/ui/contentscrollbar.go @@ -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) +} diff --git a/ui/context.go b/ui/context.go new file mode 100644 index 0000000..095c917 --- /dev/null +++ b/ui/context.go @@ -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] +} diff --git a/ui/control.go b/ui/control.go new file mode 100644 index 0000000..2d547b3 --- /dev/null +++ b/ui/control.go @@ -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) +} diff --git a/ui/dimensions.go b/ui/dimensions.go new file mode 100644 index 0000000..d492ad6 --- /dev/null +++ b/ui/dimensions.go @@ -0,0 +1,5 @@ +package ui + +const topMargin = 4 +const leftMargin = 8 +const checkboxSize = 12 diff --git a/ui/dockpanel.go b/ui/dockpanel.go new file mode 100644 index 0000000..1f8d163 --- /dev/null +++ b/ui/dockpanel.go @@ -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]) + } +} diff --git a/ui/fonts.go b/ui/fonts.go new file mode 100644 index 0000000..38a9790 --- /dev/null +++ b/ui/fonts.go @@ -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() +} diff --git a/ui/label.go b/ui/label.go new file mode 100644 index 0000000..3852fc8 --- /dev/null +++ b/ui/label.go @@ -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) +} diff --git a/ui/loop.go b/ui/loop.go new file mode 100644 index 0000000..298800a --- /dev/null +++ b/ui/loop.go @@ -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 +} diff --git a/ui/orientation.go b/ui/orientation.go new file mode 100644 index 0000000..21d5126 --- /dev/null +++ b/ui/orientation.go @@ -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" + } +} diff --git a/ui/palette.go b/ui/palette.go new file mode 100644 index 0000000..e0bd471 --- /dev/null +++ b/ui/palette.go @@ -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), + } +} diff --git a/ui/queue.go b/ui/queue.go new file mode 100644 index 0000000..725db8a --- /dev/null +++ b/ui/queue.go @@ -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 + } + } +} diff --git a/ui/scrollbar.go b/ui/scrollbar.go new file mode 100644 index 0000000..f0abb70 --- /dev/null +++ b/ui/scrollbar.go @@ -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) +} diff --git a/ui/shadow.go b/ui/shadow.go new file mode 100644 index 0000000..56955dc --- /dev/null +++ b/ui/shadow.go @@ -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) +} diff --git a/ui/spinner.go b/ui/spinner.go new file mode 100644 index 0000000..b4fa8b5 --- /dev/null +++ b/ui/spinner.go @@ -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) +} diff --git a/ui/state.go b/ui/state.go new file mode 100644 index 0000000..9706f10 --- /dev/null +++ b/ui/state.go @@ -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 +} diff --git a/ui/statusbar.go b/ui/statusbar.go new file mode 100644 index 0000000..90e2224 --- /dev/null +++ b/ui/statusbar.go @@ -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) +}