Compare commits

...

9 Commits
master ... ui

Author SHA1 Message Date
23ce18b51b Use float32 in ui package. 2018-09-24 09:40:20 +02:00
ad697d5508 Renamed repository to opslag.de/schobers/zntg.
Renamed allegro5 package to allg5
2018-09-22 16:31:09 +02:00
bb803e4cc3 Added -static flag to LDFLAGS for static build. 2018-09-09 21:26:27 +02:00
9d4b097352 Added Rect method to Control interface.
Fixed ContentScrollbar.
Added horizontal alignment to label.
Removed some dimensional constants.
Added several controls:
- Margin;
- Button;
- Columns;
- Scroll;
- Wrapper (reusable layout control) that wraps around existing control.
2018-09-09 21:07:53 +02:00
eb0b660ab6 Added Highlight color to Palette. 2018-09-09 18:49:18 +02:00
c2009c5a8b Added SetIcon to Display. 2018-09-09 18:24:33 +02:00
239c24533d Some changes to UI elements.
- Added colors to palette and renamed white/black to lightest/darkest.
- Extracted drawing code.
- Restyled checkbox, spinner and scrollbar.
2018-08-07 10:13:05 +02:00
115f9877c1 Various additions:
- Added sub-bitmaps.
- Color implements color.Color interface.
- Added methods to retrieve font dimensions and its rendered text.
- Added DrawPolyline method.
2018-08-07 10:13:04 +02:00
8157d8b3d8 Some basic UI controls. 2018-08-03 08:46:33 +02:00
44 changed files with 2357 additions and 42 deletions

View File

@ -1,6 +0,0 @@
// +build windows,static
package allegro5
// #cgo LDFLAGS: -lallegro_monolith-static -ljpeg -ldumb -lFLAC -lfreetype -lvorbisfile -lvorbis -logg -lphysfs -lpng16 -lzlib -luuid -lkernel32 -lwinmm -lpsapi -lopengl32 -lglu32 -luser32 -lcomdlg32 -lgdi32 -lshell32 -lole32 -ladvapi32 -lws2_32 -lshlwapi -lstdc++
import "C"

View File

@ -1,16 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
import "C"
type Color struct {
color C.ALLEGRO_COLOR
}
func NewColor(r, g, b byte) Color {
return Color{C.al_map_rgb(C.uchar(r), C.uchar(g), C.uchar(b))}
}
func NewColorAlpha(r, g, b, a byte) Color {
return Color{C.al_map_rgba(C.uchar(r), C.uchar(g), C.uchar(b), C.uchar(a))}
}

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
// #include <stdlib.h>
@ -16,6 +16,7 @@ type Bitmap struct {
bitmap *C.ALLEGRO_BITMAP
width int
height int
subs []*Bitmap
}
type DrawOptions struct {
@ -66,7 +67,7 @@ func newBitmap(width, height int, mut func(m FlagMutation), flags []NewBitmapFla
if nil == b {
return nil, errors.New("error creating bitmap")
}
return &Bitmap{b, width, height}, nil
return &Bitmap{b, width, height, nil}, nil
}
// NewBitmap creates a new bitmap of given width and height and optional flags
@ -133,7 +134,7 @@ func NewBitmapFromImage(im image.Image, video bool) (*Bitmap, error) {
})
C.al_convert_bitmap(b)
}
return &Bitmap{b, width, height}, nil
return &Bitmap{b, width, height, nil}, nil
}
// LoadBitmap tries to load the image at the specified path as a bitmap
@ -146,7 +147,7 @@ func LoadBitmap(path string) (*Bitmap, error) {
}
width := int(C.al_get_bitmap_width(b))
height := int(C.al_get_bitmap_height(b))
return &Bitmap{b, width, height}, nil
return &Bitmap{b, width, height, nil}, nil
}
// Draw draws the bitmap at the given location
@ -206,6 +207,22 @@ func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) {
}
}
// Sub creates a sub-bitmap of the original bitmap
func (b *Bitmap) Sub(x, y, w, h int) *Bitmap {
var sub = C.al_create_sub_bitmap(b.bitmap, C.int(x), C.int(y), C.int(w), C.int(h))
if nil == sub {
return nil
}
var bmp = &Bitmap{sub, w, h, nil}
b.subs = append(b.subs, bmp)
return bmp
}
// Subs returns the slice of sub-bitmaps
func (b *Bitmap) Subs() []*Bitmap {
return b.subs
}
func (b *Bitmap) Width() int {
return b.width
}
@ -220,5 +237,13 @@ func (b *Bitmap) SetAsTarget() {
// Destroy destroys the bitmap
func (b *Bitmap) Destroy() {
C.al_destroy_bitmap(b.bitmap)
var bmp = b.bitmap
if nil == bmp {
return
}
b.bitmap = nil
for _, sub := range b.subs {
sub.Destroy()
}
C.al_destroy_bitmap(bmp)
}

View File

@ -1,6 +1,6 @@
// +build !windows
package allegro5
package allg5
// #cgo pkg-config: allegro-5 allegro_font-5 allegro_image-5 allegro_primitives-5 allegro_ttf-5
import "C"

View File

@ -1,6 +1,6 @@
// +build windows,!static
package allegro5
package allg5
// #cgo LDFLAGS: -lallegro -lallegro_font -lallegro_image -lallegro_primitives -lallegro_ttf
import "C"

View File

@ -0,0 +1,6 @@
// +build windows,static
package allg5
// #cgo LDFLAGS: -lallegro_monolith-static -static -ljpeg -ldumb -lFLAC -lfreetype -lvorbisfile -lvorbis -logg -lphysfs -lpng16 -lzlib -luuid -lkernel32 -lwinmm -lpsapi -lopengl32 -lglu32 -luser32 -lcomdlg32 -lgdi32 -lshell32 -lole32 -ladvapi32 -lws2_32 -lshlwapi -lstdc++
import "C"

31
allg5/color.go Normal file
View File

@ -0,0 +1,31 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
import "image/color"
var _ color.Color = &Color{}
type Color struct {
color C.ALLEGRO_COLOR
}
func NewColor(r, g, b byte) Color {
return Color{C.al_map_rgb(C.uchar(r), C.uchar(g), C.uchar(b))}
}
func NewColorAlpha(r, g, b, a byte) Color {
return Color{C.al_map_rgba(C.uchar(r), C.uchar(g), C.uchar(b), C.uchar(a))}
}
// RGBA implements the color.Color interface.
func (c Color) RGBA() (r, g, b, a uint32) {
var cr, cg, cb, ca C.uchar
C.al_unmap_rgba(c.color, &cr, &cg, &cb, &ca)
a = uint32(ca)
r = uint32(cr) * a
g = uint32(cg) * a
b = uint32(cb) * a
a *= a
return
}

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"
@ -74,6 +74,10 @@ func (d *Display) SetAsTarget() {
C.al_set_target_backbuffer(d.display)
}
func (d *Display) SetIcon(i *Bitmap) {
C.al_set_display_icon(d.display, i.bitmap)
}
func (d *Display) SetMousePosition(x, y int) {
C.al_set_mouse_xy(d.display, C.int(x), C.int(y))
}

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <stdlib.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_font.h>
@ -12,6 +12,9 @@ import (
type Font struct {
font *C.ALLEGRO_FONT
hght float32
asc float32
desc float32
}
type HorizontalAlignment int
@ -30,7 +33,7 @@ func LoadTTFFont(path string, size int) (*Font, error) {
if nil == f {
return nil, fmt.Errorf("unable to load ttf font '%s'", path)
}
return &Font{f}, nil
return &Font{f, 0, 0, 0}, nil
}
func (f *Font) drawFlags(a HorizontalAlignment) C.int {
@ -53,6 +56,45 @@ func (f *Font) Draw(left, top float32, color Color, align HorizontalAlignment, t
C.al_draw_text(f.font, color.color, C.float(left), C.float(top), flags, t)
}
// Ascent returns the ascent of the font
func (f *Font) Ascent() float32 {
if 0 == f.asc {
f.asc = float32(C.al_get_font_ascent(f.font))
}
return f.asc
}
// Descent returns the descent of the font.
func (f *Font) Descent() float32 {
if 0 == f.desc {
f.desc = float32(C.al_get_font_descent(f.font))
}
return f.desc
}
// Height returns the height of the font
func (f *Font) Height() float32 {
if 0 == f.hght {
f.hght = f.Ascent() + f.Descent()
}
return f.hght
}
// TextDimensions returns the bounding box of the rendered text.
func (f *Font) TextDimensions(text string) (x, y, w, h float32) {
t := C.CString(text)
defer C.free(unsafe.Pointer(t))
var bbx, bby, bbw, bbh C.int
C.al_get_text_dimensions(f.font, t, &bbx, &bby, &bbw, &bbh)
x = float32(bbx)
y = float32(bby)
w = float32(bbw)
h = float32(bbh)
return
}
// TextWidth returns the width of the rendered text.
func (f *Font) TextWidth(text string) float32 {
t := C.CString(text)
defer C.free(unsafe.Pointer(t))

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
import "C"

View File

@ -1,6 +1,6 @@
// +build windows
package allegro5
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_windows.h>

View File

@ -1,8 +1,9 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_primitives.h>
import "C"
import "unsafe"
func DrawFilledRectangle(x1, y1, x2, y2 float32, c Color) {
C.al_draw_filled_rectangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color)
@ -16,6 +17,10 @@ func DrawLine(x1, y1, x2, y2 float32, c Color, thickness float32) {
C.al_draw_line(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color, C.float(thickness))
}
func DrawPolyline(vertices []float32, c Color, thickness float32) {
C.al_draw_polyline((*C.float)(unsafe.Pointer(&vertices[0])), 8, C.int(len(vertices)>>1), C.ALLEGRO_LINE_JOIN_ROUND, C.ALLEGRO_LINE_CAP_ROUND, c.color, C.float(thickness), 1)
}
func DrawRectangle(x1, y1, x2, y2 float32, c Color, thickness float32) {
C.al_draw_rectangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color, C.float(thickness))
}

View File

@ -1,4 +1,4 @@
package allegro5
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_font.h>

54
ui/button.go Normal file
View File

@ -0,0 +1,54 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type Button struct {
ControlBase
Text string
HorizontalAlignment allg5.HorizontalAlignment
}
func NewButton(text string, click MouseClickFn) *Button {
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text}
}
func NewButtonAlign(text string, click MouseClickFn, align allg5.HorizontalAlignment) *Button {
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text, HorizontalAlignment: align}
}
func (b *Button) DesiredSize(ctx Context) geom.PointF32 {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var w = fnt.TextWidth(b.Text)
var fntH = fnt.Height()
return geom.PtF32(w+fntH, 2*fntH)
}
func (b *Button) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = b.Bounds.Min
var max = b.Bounds.Max
var fnt = fonts.Get("default")
var fntH = fnt.Height()
var back = ctx.Palette().Primary()
if b.IsOver && !b.IsPressed {
back = ctx.Palette().PrimaryHighlight()
}
allg5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, back)
switch b.HorizontalAlignment {
case allg5.AlignLeft:
fnt.Draw(min.X+.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allg5.AlignLeft, b.Text)
case allg5.AlignCenter:
fnt.Draw(.5*(min.X+max.X), min.Y+.5*fntH, ctx.Palette().Lightest(), allg5.AlignCenter, b.Text)
case allg5.AlignRight:
fnt.Draw(min.X-.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allg5.AlignRight, b.Text)
}
b.ControlBase.Render(ctx)
}

112
ui/checkbox.go Normal file
View File

@ -0,0 +1,112 @@
package ui
import (
"fmt"
"image/color"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dkit"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type CheckboxValueChangedFn func(bool)
func drawCheckedBitmap(fill, stroke color.Color) *allg5.Bitmap {
return drawBitmap(checkboxSize, checkboxSize, func(gc *draw2dimg.GraphicContext) {
var size float64 = checkboxSize
var margin float64 = checkboxMargin
gc.SetFillColor(fill)
draw2dkit.RoundedRectangle(gc, margin, margin, size-margin, size-margin, margin, margin)
gc.Fill()
gc.SetStrokeColor(stroke)
gc.SetLineWidth(1.5)
gc.MoveTo(7, 12)
gc.LineTo(10.5, 15.5)
gc.LineTo(17, 9)
gc.Stroke()
})
}
func drawUncheckedBitmap(fill, stroke color.Color) *allg5.Bitmap {
return drawBitmap(checkboxSize, checkboxSize, func(gc *draw2dimg.GraphicContext) {
var size float64 = checkboxSize
var margin float64 = checkboxMargin
gc.SetLineWidth(2)
gc.SetStrokeColor(stroke)
draw2dkit.RoundedRectangle(gc, margin+1, margin+1, size-margin-1, size-margin-1, margin-1, margin-1)
gc.Stroke()
})
}
type Checkbox struct {
ControlBase
Value bool
Text string
OnChanged CheckboxValueChangedFn
checked *allg5.Bitmap
unchecked *allg5.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.checked = drawCheckedBitmap(plt.Primary(), plt.Lightest())
c.unchecked = drawUncheckedBitmap(plt.Lightest(), plt.Darkest())
if nil == c.checked || nil == c.unchecked {
return fmt.Errorf("error creating checkboxes")
}
return nil
}
func (c *Checkbox) Handle(ctx Context, ev allg5.Event) {
var pressed = c.IsPressed
c.ControlBase.Handle(ctx, ev)
switch ev.(type) {
case *allg5.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.PointF32 {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var w = fnt.TextWidth(c.Text)
return geom.PtF32(w+checkboxSize, checkboxSize)
}
func (c *Checkbox) box() *allg5.Bitmap {
if c.Value {
return c.checked
}
return c.unchecked
}
func (c *Checkbox) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = c.Bounds.Min
var fnt = fonts.Get("default")
fnt.Draw(min.X+checkboxSize, min.Y-.67*fnt.Ascent()+.5*checkboxSize, ctx.Palette().Darkest(), allg5.AlignLeft, c.Text)
if c.Disabled {
var disabled = ctx.Palette().Disabled()
c.box().DrawOptions(min.X, min.Y, allg5.DrawOptions{Tint: &disabled})
} else {
c.box().Draw(min.X, min.Y)
}
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}

112
ui/columns.go Normal file
View File

@ -0,0 +1,112 @@
package ui
import (
"opslag.de/schobers/geom"
)
type columns struct {
ContainerBase
cols []*column
col int
}
type column struct {
p DockPanel
}
func (c *column) Append(ctrl Control) {
c.p.Append(DockTop, ctrl)
}
func (c *column) AppendCreate(ctx Context, ctrl Control) {
c.p.AppendCreate(ctx, DockTop, ctrl)
}
type Columns interface {
Container
Columns() []Column
Append(Control)
AppendCreate(Context, Control)
}
type Column interface {
Append(Control)
AppendCreate(Context, Control)
}
func NewColumns(cols int, ctrls ...Control) Columns {
var c = &columns{}
for i := 0; i < cols; i++ {
c.addColumn()
}
for _, ctrl := range ctrls {
c.Append(ctrl)
}
return c
}
func (c *columns) addColumn() {
var p = NewDockPanel(c)
c.ContainerBase.Append(p)
c.cols = append(c.cols, &column{p})
}
func (c *columns) next() {
c.col = (c.col + 1) % len(c.cols)
}
func (c *columns) Columns() []Column {
var cols = make([]Column, len(c.cols))
for i, col := range c.cols {
cols[i] = col
}
return cols
}
func (c *columns) Append(ctrl Control) {
c.cols[c.col].Append(ctrl)
c.next()
}
func (c *columns) AppendCreate(ctx Context, ctrl Control) {
c.cols[c.col].AppendCreate(ctx, ctrl)
c.next()
}
func (c *columns) DesiredSize(ctx Context) geom.PointF32 {
var w float32
var h float32
for _, col := range c.cols {
var sz = col.p.DesiredSize(ctx)
if !geom.IsNaN32(w) {
if geom.IsNaN32(sz.X) {
w = sz.X
} else {
w += sz.X
}
}
if !geom.IsNaN32(h) {
if geom.IsNaN32(sz.Y) {
h = sz.Y
} else {
h = geom.Max32(h, sz.Y)
}
}
}
return geom.PtF32(w, h)
}
func (c *columns) Arrange(ctx Context, rect geom.RectangleF32) {
c.ContainerBase.SetRect(rect)
var w, h = rect.Dx(), rect.Dy()
var cols = float32(len(c.cols))
for i, col := range c.cols {
var ii = float32(i)
var colH = col.p.DesiredSize(ctx).Y
if colH > h {
colH = h
}
var colR = geom.RectF32(rect.Min.X+ii*w/cols, rect.Min.Y, rect.Min.X+(ii+1)*w/cols, rect.Min.Y+colH)
Arrange(ctx, col.p, colR)
}
}

106
ui/container.go Normal file
View File

@ -0,0 +1,106 @@
package ui
import (
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type Container interface {
Control
Children() []Control
Arrange(Context, geom.RectangleF32)
}
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.RectangleF32) {
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 allg5.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)
}

201
ui/contentscrollbar.go Normal file
View File

@ -0,0 +1,201 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
var _ Control = &ContentScrollbar{}
type ContentScrollbarValueChangedFn func(float32)
type ContentScrollbar struct {
ControlBase
Length float32
Value float32
Orientation Orientation
OnChanged ContentScrollbarValueChangedFn
handle *contentScrollbarHandle
barLength float32
scrollDistance float32
}
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
var max = h.Bounds.Max
allg5.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) length() (float32, float32) {
var min, max float32
switch s.Orientation {
case OrientationHorizontal:
min = s.Bounds.Min.X
max = s.Bounds.Max.X
default:
min = s.Bounds.Min.Y
max = s.Bounds.Max.Y
}
return max - min, min
}
func (s *ContentScrollbar) updateBarLength() {
var length, _ = s.length()
var bar = length
if s.Length > length {
bar = length * length / s.Length
}
if bar < 20 {
if length < 40 {
bar = .5 * length
} else {
bar = 20
}
}
s.barLength = bar
var d = s.Length - length
if d < 0 {
d = 0
}
s.scrollDistance = d
}
func (s *ContentScrollbar) barViewCenter() float32 {
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 float32) float32 {
var pos = y
if OrientationHorizontal == s.Orientation {
pos = x
}
var length, min = s.length()
if length == 0 {
return 0
}
var offset = (pos - .5*s.barLength - min) / (length - s.barLength)
if offset < 0 {
return 0
} else if offset > 1 {
return s.scrollDistance
}
return s.scrollDistance * offset
}
func (s *ContentScrollbar) change(v float32) {
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(float32(x), float32(y))
s.change(val)
}
func (s *ContentScrollbar) increment(d int) {
if s.Orientation == OrientationVertical {
d *= -1
}
var val = s.Value + float32(d)*ScrollbarWidth
if val < 0 {
val = 0
} else if val > s.scrollDistance {
val = s.scrollDistance
}
s.change(val)
}
func (s *ContentScrollbar) Handle(ctx Context, ev allg5.Event) {
s.ControlBase.Handle(ctx, ev)
s.handle.Handle(ctx, ev)
switch e := ev.(type) {
case *allg5.MouseMoveEvent:
if s.handle.IsPressed {
s.snapTo(e.X, e.Y)
}
if 0 != e.DeltaZ && s.IsOver {
var d = e.DeltaZ
if allg5.IsAnyKeyDown(allg5.KeyLShift, allg5.KeyRShift) {
d *= 10
}
s.increment(d)
}
case *allg5.MouseButtonDownEvent:
if !s.handle.IsPressed && s.IsOver {
s.snapTo(e.X, e.Y)
}
}
}
func (s *ContentScrollbar) DesiredSize(Context) geom.PointF32 {
switch s.Orientation {
case OrientationHorizontal:
return geom.PtF32(geom.NaN32(), ScrollbarWidth)
}
return geom.PtF32(ScrollbarWidth, geom.NaN32())
}
func (s *ContentScrollbar) SetRect(rect geom.RectangleF32) {
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)
s.updateBarLength()
var offset float32
if 0 < s.scrollDistance {
offset = s.Value / s.scrollDistance
}
var length, min = s.length()
var begin = min + (length-s.barLength)*offset
var end = begin + s.barLength
switch s.Orientation {
case OrientationHorizontal:
s.handle.SetRect(geom.RectF32(begin, rect.Min.Y, end, rect.Max.Y))
default:
s.handle.SetRect(geom.RectF32(rect.Min.X, begin, rect.Max.X, end))
}
}
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/zntg/allg5"
type Context interface {
Display() *allg5.Display
Fonts() Fonts
Palette() Palette
Debug() Debug
}
type Debug interface {
IsEnabled() bool
Rainbow() allg5.Color
}
var _ Context = &context{}
type context struct {
disp *allg5.Display
fts Fonts
pal Palette
dbg *debug
}
type debug struct {
enbl bool
rainb []allg5.Color
col int
}
func rainbow() []allg5.Color {
var colors = make([]allg5.Color, len(Colors500))
for i, c := range Colors500 {
colors[i] = NewColorAlpha(c, 0x7f)
}
return colors
}
func newContext(disp *allg5.Display, f Fonts) *context {
return &context{disp, f, DefaultPalette(), &debug{rainb: rainbow()}}
}
func (c *context) Display() *allg5.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() allg5.Color {
var col = d.col
d.col = (col + 1) % len(d.rainb)
return d.rainb[col]
}

90
ui/control.go Normal file
View File

@ -0,0 +1,90 @@
package ui
import (
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type Control interface {
Created(Context, Container) error
Destroyed(Context)
Update(Context, time.Duration)
Handle(Context, allg5.Event)
DesiredSize(Context) geom.PointF32
Rect() geom.RectangleF32
SetRect(geom.RectangleF32)
Render(Context)
}
type MouseClickFn func(Control)
var _ Control = &ControlBase{}
type ControlBase struct {
Parent Container
Bounds geom.RectangleF32
Disabled bool
IsOver bool
IsPressed bool
OnClick MouseClickFn
MinSize geom.PointF32
Background *allg5.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 allg5.Event) {
switch e := ev.(type) {
case *allg5.MouseMoveEvent:
c.IsOver = c.IsInRect(float32(e.X), float32(e.Y))
case *allg5.MouseButtonDownEvent:
if c.IsOver {
c.IsPressed = true
}
case *allg5.MouseButtonUpEvent:
if c.IsPressed && c.IsOver {
var onClick = c.OnClick
if nil != onClick {
onClick(c)
}
}
c.IsPressed = false
}
}
func (c *ControlBase) DesiredSize(Context) geom.PointF32 {
return c.MinSize
}
func (c *ControlBase) SetRect(rect geom.RectangleF32) {
c.Bounds = rect
}
func (c *ControlBase) Render(ctx Context) {
var min = c.Bounds.Min
var max = c.Bounds.Max
if nil != c.Background {
allg5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, *c.Background)
}
if ctx.Debug().IsEnabled() {
allg5.DrawRectangle(min.X, min.Y, max.X, max.Y, ctx.Debug().Rainbow(), 5)
}
}
func (c *ControlBase) Rect() geom.RectangleF32 {
return c.Bounds
}
func (c *ControlBase) IsInRect(x, y float32) bool {
return geom.PtF32(x, y).In(c.Bounds)
}

5
ui/dimensions.go Normal file
View File

@ -0,0 +1,5 @@
package ui
const leftMargin = 8
const checkboxMargin = 4
const checkboxSize = 24

152
ui/dockpanel.go Normal file
View File

@ -0,0 +1,152 @@
package ui
import (
"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 NewDockPanelContent(parent Container, d Dock, controls ...Control) DockPanel {
var p = NewDockPanel(parent)
for _, c := range controls {
p.Append(d, c)
}
return p
}
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.PointF32 {
var width, height float32 = 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 !geom.IsNaN32(width) {
if geom.IsNaN32(desired.X) {
width = desired.X
} else {
width += desired.X
}
}
if !geom.IsNaN32(desired.Y) {
height = geom.Max32(height, desired.Y)
}
case d == DockTop || d == DockBottom:
if !geom.IsNaN32(height) {
if geom.IsNaN32(desired.Y) {
height = desired.Y
} else {
height += desired.Y
}
}
if !geom.IsNaN32(desired.X) {
width = geom.Max32(width, desired.X)
}
}
}
return geom.PtF32(width, height)
}
func (p *dockPanel) arrangeChildren(ctx Context) []geom.RectangleF32 {
var n = len(p.children)
var rects = make([]geom.RectangleF32, 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 float32
switch {
case d == DockLeft || d == DockRight:
width = rem.Dx()
if !geom.IsNaN32(desired.X) {
width = geom.Min32(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 !geom.IsNaN32(desired.Y) {
height = geom.Min32(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.RectF32(left, top, left+width, top+height)
switch d {
case DockLeft:
rem = geom.RectF32(rem.Min.X+width, rem.Min.Y, rem.Max.X, rem.Max.Y)
case DockTop:
rem = geom.RectF32(rem.Min.X, rem.Min.Y+height, rem.Max.X, rem.Max.Y)
case DockRight:
rem = geom.RectF32(rem.Min.X, rem.Min.Y, rem.Max.X-width, rem.Max.Y)
case DockBottom:
rem = geom.RectF32(rem.Min.X, rem.Min.Y, rem.Max.X, rem.Max.Y-height)
}
}
}
return rects
}
func (p *dockPanel) Arrange(ctx Context, rect geom.RectangleF32) {
p.ContainerBase.SetRect(rect)
var rects = p.arrangeChildren(ctx)
for i, child := range p.children {
Arrange(ctx, child, rects[i])
}
}

38
ui/draw.go Normal file
View File

@ -0,0 +1,38 @@
package ui
import (
"image"
"image/color"
"math"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/zntg/allg5"
)
func drawBitmap(w, h int, draw func(*draw2dimg.GraphicContext)) *allg5.Bitmap {
dest := image.NewRGBA(image.Rect(0, 0, w, h))
gc := draw2dimg.NewGraphicContext(dest)
gc.SetFillColor(color.Transparent)
gc.Clear()
draw(gc)
bmp, err := allg5.NewBitmapFromImage(dest, false)
if nil != err {
return nil
}
return bmp
}
func drawCircle(r, w int, startAngle, a float64, c color.Color) *allg5.Bitmap {
var width = 2*r + w
return drawBitmap(width, width, func(gc *draw2dimg.GraphicContext) {
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()
})
}

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/zntg/allg5"
)
type Fonts interface {
Register(name, path string, size int) error
Get(name string) *allg5.Font
Destroy()
}
type fonts struct {
fts map[string]*allg5.Font
dir vfs.CopyDir
}
func newFonts(dir vfs.CopyDir) *fonts {
return &fonts{make(map[string]*allg5.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 := allg5.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) *allg5.Font {
return fts.fts[name]
}
func (fts *fonts) Destroy() {
for _, f := range fts.fts {
f.Destroy()
}
fts.dir.Destroy()
}

30
ui/label.go Normal file
View File

@ -0,0 +1,30 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type Label struct {
ControlBase
Text string
HorizontalAlignment allg5.HorizontalAlignment
}
func (l *Label) DesiredSize(ctx Context) geom.PointF32 {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var _, _, w, h = fnt.TextDimensions(l.Text)
return geom.PtF32(w, h)
}
func (l *Label) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = l.Bounds.Min
var fnt = fonts.Get("default")
fnt.Draw(min.X, min.Y+fnt.Height()-fnt.Ascent(), ctx.Palette().Darkest(), l.HorizontalAlignment, 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/zntg/allg5"
)
func shouldClose(ev allg5.Event) bool {
switch e := ev.(type) {
case *allg5.KeyCharEvent:
switch e.KeyCode {
case allg5.KeyEscape:
return true
case allg5.KeyF4:
if e.Modifiers&allg5.KeyModAlt == allg5.KeyModAlt {
return true
}
}
case *allg5.DisplayCloseEvent:
return true
}
return false
}
func shouldToggleDebug(ev allg5.Event) bool {
switch e := ev.(type) {
case *allg5.KeyCharEvent:
switch e.KeyCode {
case allg5.KeyD:
return e.Modifiers&allg5.KeyModAlt == allg5.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 allg5.Init(allg5.InitAll)
}
func Run(w, h int, title string, s State, f Fonts, opts *allg5.NewDisplayOptions) error {
if nil == opts {
opts = &allg5.NewDisplayOptions{}
}
disp, err := allg5.NewDisplay(w, h, *opts)
if nil != err {
return err
}
disp.SetWindowTitle(title)
defer disp.Destroy()
evq, err := allg5.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
}

50
ui/margin.go Normal file
View File

@ -0,0 +1,50 @@
package ui
import "opslag.de/schobers/geom"
type margin struct {
Wrapper
Left, Top, Right, Bottom float32
}
func Margin(c Control, m float32) Control {
return &margin{Wrap(c), m, m, m, m}
}
func LeftMargin(c Control, m float32) Control {
return &margin{Wrap(c), m, 0, 0, 0}
}
func TopMargin(c Control, m float32) Control {
return &margin{Wrap(c), 0, m, 0, 0}
}
func HorizontalMargin(c Control, m float32) Control {
return &margin{Wrap(c), m, 0, m, 0}
}
func VerticalMargin(c Control, m float32) Control {
return &margin{Wrap(c), 0, m, 0, m}
}
func (m *margin) DesiredSize(ctx Context) geom.PointF32 {
var sz = m.Wrapper.DesiredSize(ctx)
return geom.PtF32(sz.X+m.Left+m.Right, sz.Y+m.Top+m.Bottom)
}
func (m *margin) Arrange(ctx Context, rect geom.RectangleF32) {
m.Wrapper.SetRect(rect)
rect.Min.X += m.Left
rect.Min.Y += m.Top
rect.Max.X -= m.Right
rect.Max.Y -= m.Bottom
if rect.Min.X > rect.Max.X {
var x = .5 * (rect.Min.X + rect.Max.X)
rect.Min.X, rect.Max.X = x, x
}
if rect.Min.Y > rect.Max.Y {
var y = .5 * (rect.Min.Y + rect.Max.Y)
rect.Min.Y, rect.Max.Y = y, y
}
Arrange(ctx, m.Wrapped, rect)
}

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"
}
}

76
ui/palette.go Normal file
View File

@ -0,0 +1,76 @@
package ui
import (
"image/color"
"opslag.de/schobers/zntg/allg5"
)
type Palette interface {
Primary() allg5.Color
PrimaryHighlight() allg5.Color
PrimaryTransparent() allg5.Color
Lightest() allg5.Color
Darker() allg5.Color
Darkest() allg5.Color
Disabled() allg5.Color
}
type palette struct {
primary allg5.Color
primaryH allg5.Color
primaryT allg5.Color
lightest allg5.Color
darker allg5.Color
darkest allg5.Color
disabled allg5.Color
}
func (p *palette) Primary() allg5.Color {
return p.primary
}
func (p *palette) PrimaryHighlight() allg5.Color {
return p.primaryH
}
func (p *palette) PrimaryTransparent() allg5.Color {
return p.primaryT
}
func (p *palette) Lightest() allg5.Color {
return p.lightest
}
func (p *palette) Darker() allg5.Color {
return p.darker
}
func (p *palette) Darkest() allg5.Color {
return p.darkest
}
func (p *palette) Disabled() allg5.Color {
return p.disabled
}
func NewColor(c *color.RGBA) allg5.Color {
return allg5.NewColorAlpha(c.R, c.G, c.B, c.A)
}
func NewColorAlpha(c *color.RGBA, a uint8) allg5.Color {
return allg5.NewColorAlpha(c.R, c.G, c.B, a)
}
func DefaultPalette() Palette {
var primary = Blue500
return &palette{
primary: NewColor(primary),
primaryH: NewColor(Blue400),
primaryT: NewColorAlpha(primary, 96),
lightest: allg5.NewColor(0xff, 0xff, 0xff),
darker: allg5.NewColorAlpha(0, 0, 0, 188),
darkest: allg5.NewColorAlpha(0, 0, 0, 222),
disabled: allg5.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
}
}
}

77
ui/scroll.go Normal file
View File

@ -0,0 +1,77 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type scroll struct {
Wrapper
Content Control
Bar *ContentScrollbar
}
func Scroll(c Control, o Orientation) Control {
var dock = NewDockPanel(nil)
var bar = &ContentScrollbar{Orientation: o}
switch o {
case OrientationHorizontal:
dock.Append(DockBottom, TopMargin(bar, ScrollbarWidth))
dock.Append(DockBottom, c)
case OrientationVertical:
dock.Append(DockRight, LeftMargin(bar, ScrollbarWidth))
dock.Append(DockRight, c)
}
var s = &scroll{Wrap(dock), c, bar}
bar.OnChanged = func(v float32) {
}
return s
}
func (s *scroll) Handle(ctx Context, ev allg5.Event) {
s.Wrapper.Handle(ctx, ev)
switch e := ev.(type) {
case *allg5.MouseMoveEvent:
if 0 != e.DeltaZ && !s.Bar.IsOver && geom.PtF32(float32(e.X), float32(e.Y)).In(s.Bounds) {
var d = e.DeltaZ
if allg5.IsAnyKeyDown(allg5.KeyLShift, allg5.KeyRShift) {
d *= 10
}
s.Bar.increment(d)
}
}
}
func (s *scroll) Render(ctx Context) {
var bounds = s.Content.Rect()
var w, h = bounds.Dx(), bounds.Dy()
var bmp, err = allg5.NewVideoBitmap(int(w), int(h))
if nil != err {
return
}
defer bmp.Destroy()
bmp.SetAsTarget()
switch s.Bar.Orientation {
case OrientationHorizontal:
Arrange(ctx, s.Content, geom.RectF32(-s.Bar.Value, 0, w, h))
case OrientationVertical:
Arrange(ctx, s.Content, geom.RectF32(0, -s.Bar.Value, w, h))
}
s.Content.Render(ctx)
ctx.Display().SetAsTarget()
var min = s.Bounds.Min
bmp.Draw(min.X, min.Y)
s.Bar.Render(ctx)
}
func (s *scroll) Arrange(ctx Context, rect geom.RectangleF32) {
var sz = s.Content.DesiredSize(ctx)
switch s.Bar.Orientation {
case OrientationHorizontal:
s.Bar.Length = sz.X
case OrientationVertical:
s.Bar.Length = sz.Y
}
s.Wrapper.Arrange(ctx, rect)
}

243
ui/scrollbar.go Normal file
View File

@ -0,0 +1,243 @@
package ui
import (
"math"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
var _ Control = &Scrollbar{}
const ScrollbarWidth = 12
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
normal *allg5.Bitmap
hover *allg5.Bitmap
pressed *allg5.Bitmap
}
func (s *Scrollbar) Created(ctx Context, p Container) error {
s.ControlBase.Created(ctx, p)
var center = float64(ScrollbarWidth) * 1.5
var rad = float64(ScrollbarWidth) * .5
s.normal = drawBitmap(3*ScrollbarWidth, 3*ScrollbarWidth, func(gc *draw2dimg.GraphicContext) {
gc.SetFillColor(ctx.Palette().Primary())
gc.MoveTo(center, center)
gc.ArcTo(center, center, rad, rad, 0, 2*math.Pi)
gc.Fill()
})
s.hover = drawBitmap(3*ScrollbarWidth, 3*ScrollbarWidth, func(gc *draw2dimg.GraphicContext) {
gc.SetFillColor(ctx.Palette().PrimaryTransparent())
gc.MoveTo(center, center)
gc.ArcTo(center, center, center, center, 0, 2*math.Pi)
gc.Fill()
gc.SetFillColor(ctx.Palette().Primary())
gc.MoveTo(center, center)
gc.ArcTo(center, center, rad, rad, 0, 2*math.Pi)
gc.Fill()
})
s.pressed = drawBitmap(3*ScrollbarWidth, 3*ScrollbarWidth, func(gc *draw2dimg.GraphicContext) {
gc.SetFillColor(ctx.Palette().PrimaryTransparent())
for i := 0; i < 2; i++ {
gc.MoveTo(center, center)
gc.ArcTo(center, center, center, center, 0, 2*math.Pi)
gc.Fill()
}
gc.SetFillColor(ctx.Palette().Primary())
gc.MoveTo(center, center)
gc.ArcTo(center, center, rad, rad, 0, 2*math.Pi)
gc.Fill()
})
s.handle = &ControlBase{}
s.handle.Created(ctx, nil)
return nil
}
func (s *Scrollbar) Destroyed(ctx Context) {
var d = func(b *allg5.Bitmap) {
if nil != b {
b.Destroy()
}
}
d(s.normal)
d(s.hover)
d(s.pressed)
s.handle.Destroyed(ctx)
}
func (s *Scrollbar) barViewRange() (float32, float32) {
var small bool
var min, max float32
switch s.Orientation {
case OrientationHorizontal:
min = s.Bounds.Min.X + ScrollbarWidth
max = s.Bounds.Max.X - ScrollbarWidth
small = min > max
default:
min = s.Bounds.Max.Y - ScrollbarWidth
max = s.Bounds.Min.Y + ScrollbarWidth
small = max > min
}
if small {
var center = (min + max) * .5
min, max = center, center
}
return min, max
}
func (s *Scrollbar) barViewCenter() float32 {
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 float32) 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*float32(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(float32(x), float32(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 allg5.Event) {
s.ControlBase.Handle(ctx, ev)
s.handle.Handle(ctx, ev)
switch e := ev.(type) {
case *allg5.MouseMoveEvent:
if s.handle.IsPressed {
s.snapTo(e.X, e.Y)
}
if 0 != e.DeltaZ && s.IsOver {
var d = e.DeltaZ
if allg5.IsAnyKeyDown(allg5.KeyLShift, allg5.KeyRShift) {
d *= 10
}
s.increment(d)
}
case *allg5.MouseButtonDownEvent:
if !s.handle.IsPressed && s.IsOver {
s.snapTo(e.X, e.Y)
}
}
}
func (s *Scrollbar) DesiredSize(Context) geom.PointF32 {
var width = float32(2 * ScrollbarWidth)
switch s.Orientation {
case OrientationHorizontal:
return geom.PtF32(geom.NaN32(), width)
}
return geom.PtF32(width, geom.NaN32())
}
func (s *Scrollbar) SetRect(rect geom.RectangleF32) {
var width = float32(2 * ScrollbarWidth)
switch s.Orientation {
case OrientationHorizontal:
if rect.Dy() > width {
rect.Min.Y = rect.Max.Y - width
}
default:
if rect.Dx() > width {
rect.Min.X = rect.Max.X - width
}
}
s.ControlBase.SetRect(rect)
var min, max = s.barViewRange()
var off = float32(s.Value-s.Minimum) / float32(s.Maximum-s.Minimum)
var centerH = min + (max-min)*off
var r = 0.5 * float32(s.normal.Width())
var center = s.barViewCenter()
switch s.Orientation {
case OrientationHorizontal:
s.handle.SetRect(geom.RectF32(centerH-r, center-r, centerH+r, center+r))
default:
s.handle.SetRect(geom.RectF32(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 centerH = s.handle.Bounds.Center()
switch s.Orientation {
case OrientationHorizontal:
// Left line
allg5.DrawLine(min, center, centerH.X, center, ctx.Palette().Primary(), 2)
allg5.DrawLine(centerH.X, center, max, center, ctx.Palette().PrimaryTransparent(), 2)
default:
allg5.DrawLine(center, max, center, centerH.Y, ctx.Palette().PrimaryTransparent(), 2)
allg5.DrawLine(center, centerH.Y, center, min, ctx.Palette().Primary(), 2)
}
var minH = s.handle.Bounds.Min
var state = s.normal
if s.handle.IsOver {
if s.handle.IsPressed {
state = s.pressed
} else {
state = s.hover
}
}
state.Draw(minH.X, minH.Y)
s.ControlBase.Render(ctx)
}

29
ui/shadow.go Normal file
View File

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

99
ui/spinner.go Normal file
View File

@ -0,0 +1,99 @@
package ui
import (
"math"
"time"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/zntg/allg5"
)
var _ Control = &Spinner{}
type Spinner struct {
ControlBase
Text string
spin float32
circs *allg5.Bitmap
}
func (s *Spinner) Created(ctx Context, p Container) error {
s.ControlBase.Created(ctx, p)
const row = 6
const numCircs = row * row
const width = 48
const center = width * .5
const full = 2 * math.Pi
s.circs = drawBitmap(row*width, row*width, func(gc *draw2dimg.GraphicContext) {
var start, a float64 = 0, .2 * math.Pi
var inc = true
gc.SetLineWidth(4)
gc.SetStrokeColor(ctx.Palette().Primary())
for i := 0; i < numCircs; i++ {
var left = float64(i%row) * width
var top = float64(i/row) * width
gc.ArcTo(left+center, top+center, center-8, center-8, start, a)
gc.Stroke()
if inc {
start += full / numCircs
a += 1.90 * full / numCircs
if a >= full {
inc = false
}
} else {
start += 2.90 * full / numCircs
a -= 1.90 * full / numCircs
// if a <= .1*math.Pi {
// inc = true
// }
}
}
})
for i := 0; i < numCircs; i++ {
var left = (i % row) * width
var top = (i / row) * width
s.circs.Sub(left, top, width, width)
}
return nil
}
func (s *Spinner) Destroyed(ctx Context) {
s.circs.Destroy()
}
func (s *Spinner) Update(ctx Context, dt time.Duration) {
var spin = float64(s.spin)
spin += dt.Seconds() * .5
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)
allg5.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
allg5.DrawFilledRectangle(rectX, rectY, rectX+rectW, rectY+rectH, ctx.Palette().Lightest())
DropShadow(rectX, rectY, rectX+rectW, rectY+rectH)
fnt.Draw(rectX+marginH, rectY+marginV, ctx.Palette().Darkest(), allg5.AlignLeft, s.Text)
const numCircs = 36
var i = int(math.Floor(float64(s.spin) * numCircs))
s.circs.Subs()[i].DrawOptions(width*.5, rectY+2*marginV+2*textH, allg5.DrawOptions{Center: true})
s.ControlBase.Render(ctx)
}

71
ui/state.go Normal file
View File

@ -0,0 +1,71 @@
package ui
import (
"log"
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type State interface {
Enter(Context) error
Leave(Context) error
Update(Context, time.Duration) (State, error)
Handle(Context, allg5.Event) error
Render(Context) error
}
type StateBase struct {
Control Control
ChangeTo State
}
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 s.ChangeTo, nil
}
func (s *StateBase) Handle(ctx Context, ev allg5.Event) error {
if nil != s.Control {
s.Control.Handle(ctx, ev)
}
return nil
}
func Arrange(ctx Context, c Control, rect geom.RectangleF32) {
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.RectF32(0, 0, float32(disp.Width()), float32(disp.Height())))
s.Control.Render(ctx)
}
return nil
}

35
ui/statusbar.go Normal file
View File

@ -0,0 +1,35 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
var _ Control = &StatusBar{}
const statusBarHeight = 24
type StatusBar struct {
ControlBase
Text string
RightText string
}
func (b *StatusBar) DesiredSize(Context) geom.PointF32 {
return geom.PtF32(geom.NaN32(), statusBarHeight)
}
func (b *StatusBar) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = b.Bounds.Min
var max = b.Bounds.Max
allg5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, ctx.Palette().Primary())
var fnt = fonts.Get("default")
var y = min.Y + float32(.5*b.Bounds.Dy()) - .67*fnt.Ascent()
fnt.Draw(min.X+leftMargin, y, ctx.Palette().Lightest(), allg5.AlignLeft, b.Text)
fnt.Draw(max.X-leftMargin, y, ctx.Palette().Lightest(), allg5.AlignRight, b.RightText)
b.ControlBase.Render(ctx)
}

58
ui/wrapper.go Normal file
View File

@ -0,0 +1,58 @@
package ui
import (
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type Wrapper struct {
Wrapped Control
Bounds geom.RectangleF32
}
func Wrap(c Control) Wrapper {
return Wrapper{Wrapped: c}
}
func (w *Wrapper) Created(ctx Context, p Container) error {
return w.Wrapped.Created(ctx, p)
}
func (w *Wrapper) Destroyed(ctx Context) {
w.Wrapped.Destroyed(ctx)
}
func (w *Wrapper) Update(ctx Context, d time.Duration) {
w.Wrapped.Update(ctx, d)
}
func (w *Wrapper) Handle(ctx Context, ev allg5.Event) {
w.Wrapped.Handle(ctx, ev)
}
func (w *Wrapper) DesiredSize(ctx Context) geom.PointF32 {
return w.Wrapped.DesiredSize(ctx)
}
func (w *Wrapper) Rect() geom.RectangleF32 {
return w.Bounds
}
func (w *Wrapper) SetRect(rect geom.RectangleF32) {
w.Bounds = rect
}
func (w *Wrapper) Render(ctx Context) {
w.Wrapped.Render(ctx)
}
func (w *Wrapper) Children() []Control {
return []Control{w.Wrapped}
}
func (w *Wrapper) Arrange(ctx Context, rect geom.RectangleF32) {
w.Bounds = rect
Arrange(ctx, w.Wrapped, rect)
}