Added UI elements.
Reversed order of operands when comparing with nil/0 .
This commit is contained in:
parent
89a3d49992
commit
19daffd110
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,3 +1,9 @@
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
|
||||
# Go
|
||||
debug
|
||||
debug.test
|
||||
debug.test
|
||||
|
||||
# Project
|
||||
ui/examples/99_playground
|
||||
|
@ -56,7 +56,7 @@ func newBitmap(width, height int, mut func(m FlagMutation), flags []NewBitmapFla
|
||||
var newBmpFlags = CaptureNewBitmapFlags()
|
||||
defer newBmpFlags.Revert()
|
||||
newBmpFlags.Mutate(func(m FlagMutation) {
|
||||
if nil != mut {
|
||||
if mut != nil {
|
||||
mut(m)
|
||||
}
|
||||
for _, f := range flags {
|
||||
@ -64,7 +64,7 @@ func newBitmap(width, height int, mut func(m FlagMutation), flags []NewBitmapFla
|
||||
}
|
||||
})
|
||||
b := C.al_create_bitmap(C.int(width), C.int(height))
|
||||
if nil == b {
|
||||
if b == nil {
|
||||
return nil, errors.New("error creating bitmap")
|
||||
}
|
||||
return &Bitmap{b, width, height, nil}, nil
|
||||
@ -103,12 +103,12 @@ func NewBitmapFromImage(im image.Image, video bool) (*Bitmap, error) {
|
||||
var bnd = im.Bounds()
|
||||
width, height := bnd.Dx(), bnd.Dy()
|
||||
var b = C.al_create_bitmap(C.int(width), C.int(height))
|
||||
if nil == b {
|
||||
if b == nil {
|
||||
return nil, errors.New("error creating memory bitmap")
|
||||
}
|
||||
row := make([]uint8, width*4)
|
||||
rgn := C.al_lock_bitmap(b, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_WRITEONLY)
|
||||
if nil == rgn {
|
||||
if rgn == nil {
|
||||
C.al_destroy_bitmap(b)
|
||||
return nil, errors.New("unable to lock bitmap")
|
||||
}
|
||||
@ -142,7 +142,7 @@ func LoadBitmap(path string) (*Bitmap, error) {
|
||||
p := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(p))
|
||||
b := C.al_load_bitmap(p)
|
||||
if nil == b {
|
||||
if b == nil {
|
||||
return nil, errors.New("error loading bitmap")
|
||||
}
|
||||
width := int(C.al_get_bitmap_width(b))
|
||||
@ -177,7 +177,7 @@ func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) {
|
||||
}
|
||||
|
||||
if scale {
|
||||
if nil == options.Tint { // scaled
|
||||
if options.Tint == nil { // scaled
|
||||
if rotated { // scaled & rotated
|
||||
C.al_draw_scaled_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal()), C.float(options.Scale.Vertical()), C.float(options.Rotation.Angle), 0)
|
||||
} else { // scaled
|
||||
@ -191,7 +191,7 @@ func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if nil == options.Tint {
|
||||
if options.Tint == nil {
|
||||
if rotated { // rotated
|
||||
C.al_draw_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Rotation.Angle), 0)
|
||||
} else {
|
||||
@ -210,7 +210,7 @@ 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 {
|
||||
if sub == nil {
|
||||
return nil
|
||||
}
|
||||
var bmp = &Bitmap{sub, w, h, nil}
|
||||
@ -238,7 +238,7 @@ func (b *Bitmap) SetAsTarget() {
|
||||
// Destroy destroys the bitmap
|
||||
func (b *Bitmap) Destroy() {
|
||||
var bmp = b.bitmap
|
||||
if nil == bmp {
|
||||
if bmp == nil {
|
||||
return
|
||||
}
|
||||
b.bitmap = nil
|
||||
|
@ -41,7 +41,7 @@ func NewDisplay(width, height int, options NewDisplayOptions) (*Display, error)
|
||||
}
|
||||
C.al_set_new_display_flags(flags)
|
||||
d := C.al_create_display(C.int(width), C.int(height))
|
||||
if nil == d {
|
||||
if d == nil {
|
||||
return nil, errors.New("error creating display")
|
||||
}
|
||||
return &Display{d}, nil
|
||||
@ -92,11 +92,20 @@ func (d *Display) SetWindowTitle(title string) {
|
||||
C.al_set_window_title(d.display, t)
|
||||
}
|
||||
|
||||
func (d *Display) Target() *Bitmap {
|
||||
return &Bitmap{C.al_get_backbuffer(d.display), d.Width(), d.Height(), nil}
|
||||
}
|
||||
|
||||
// Destroy destroys the display
|
||||
func (d *Display) Destroy() {
|
||||
C.al_destroy_display(d.display)
|
||||
}
|
||||
|
||||
func CurrentTarget() *Bitmap {
|
||||
var bmp = C.al_get_target_bitmap()
|
||||
return &Bitmap{bmp, int(C.al_get_bitmap_width(bmp)), int(C.al_get_bitmap_height(bmp)), nil}
|
||||
}
|
||||
|
||||
func SetNewWindowTitle(title string) {
|
||||
t := C.CString(title)
|
||||
defer C.free(unsafe.Pointer(t))
|
||||
|
@ -119,7 +119,7 @@ type MouseMoveEvent struct {
|
||||
|
||||
func NewEventQueue() (*EventQueue, error) {
|
||||
q := C.al_create_event_queue()
|
||||
if nil == q {
|
||||
if q == nil {
|
||||
return nil, errors.New("unable to create event queue")
|
||||
}
|
||||
return &EventQueue{q}, nil
|
||||
|
@ -30,7 +30,7 @@ func LoadTTFFont(path string, size int) (*Font, error) {
|
||||
defer C.free(unsafe.Pointer(p))
|
||||
|
||||
f := C.al_load_ttf_font(p, C.int(size), 0)
|
||||
if nil == f {
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("unable to load ttf font '%s'", path)
|
||||
}
|
||||
return &Font{f, 0, 0, 0}, nil
|
||||
@ -58,7 +58,7 @@ func (f *Font) Draw(left, top float32, color Color, align HorizontalAlignment, t
|
||||
|
||||
// Ascent returns the ascent of the font
|
||||
func (f *Font) Ascent() float32 {
|
||||
if 0 == f.asc {
|
||||
if f.asc == 0 {
|
||||
f.asc = float32(C.al_get_font_ascent(f.font))
|
||||
}
|
||||
return f.asc
|
||||
@ -66,7 +66,7 @@ func (f *Font) Ascent() float32 {
|
||||
|
||||
// Descent returns the descent of the font.
|
||||
func (f *Font) Descent() float32 {
|
||||
if 0 == f.desc {
|
||||
if f.desc == 0 {
|
||||
f.desc = float32(C.al_get_font_descent(f.font))
|
||||
}
|
||||
return f.desc
|
||||
@ -74,7 +74,7 @@ func (f *Font) Descent() float32 {
|
||||
|
||||
// Height returns the height of the font
|
||||
func (f *Font) Height() float32 {
|
||||
if 0 == f.hght {
|
||||
if f.hght == 0 {
|
||||
f.hght = f.Ascent() + f.Descent()
|
||||
}
|
||||
return f.hght
|
||||
|
9
ui/alignment.go
Normal file
9
ui/alignment.go
Normal file
@ -0,0 +1,9 @@
|
||||
package ui
|
||||
|
||||
type HorizontalAlignment int
|
||||
|
||||
const (
|
||||
AlignLeft HorizontalAlignment = iota
|
||||
AlignCenter
|
||||
AlignRight
|
||||
)
|
46
ui/allg5ui/font.go
Normal file
46
ui/allg5ui/font.go
Normal file
@ -0,0 +1,46 @@
|
||||
package allg5ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
"opslag.de/schobers/zntg/allg5"
|
||||
)
|
||||
|
||||
type FontDefinition struct {
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
func NewFontDefinition(name string, size int) FontDefinition {
|
||||
return FontDefinition{Name: name, Size: size}
|
||||
}
|
||||
|
||||
type font struct {
|
||||
f *allg5.Font
|
||||
}
|
||||
|
||||
func newFont(f *allg5.Font) *font {
|
||||
return &font{f}
|
||||
}
|
||||
|
||||
func (f *font) Destroy() {
|
||||
f.f.Destroy()
|
||||
}
|
||||
|
||||
func (f *font) Height() float32 {
|
||||
if f == nil {
|
||||
return 0
|
||||
}
|
||||
return f.f.Height()
|
||||
}
|
||||
|
||||
func (f *font) WidthOf(t string) float32 {
|
||||
return f.Measure(t).Dx()
|
||||
}
|
||||
|
||||
func (f *font) Measure(t string) geom.RectangleF32 {
|
||||
if f == nil {
|
||||
return geom.RectangleF32{}
|
||||
}
|
||||
var x, y, w, h = f.f.TextDimensions(t)
|
||||
return geom.RectF32(x, y, x+w, y+h)
|
||||
}
|
24
ui/allg5ui/image.go
Normal file
24
ui/allg5ui/image.go
Normal file
@ -0,0 +1,24 @@
|
||||
package allg5ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/zntg/allg5"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
var _ ui.Image = &uiImage{}
|
||||
|
||||
type uiImage struct {
|
||||
bmp *allg5.Bitmap
|
||||
}
|
||||
|
||||
func (i *uiImage) Height() float32 {
|
||||
return float32(i.bmp.Height())
|
||||
}
|
||||
|
||||
func (i *uiImage) Width() float32 {
|
||||
return float32(i.bmp.Width())
|
||||
}
|
||||
|
||||
func (i *uiImage) Destroy() {
|
||||
i.bmp.Destroy()
|
||||
}
|
209
ui/allg5ui/renderer.go
Normal file
209
ui/allg5ui/renderer.go
Normal file
@ -0,0 +1,209 @@
|
||||
package allg5ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
|
||||
"opslag.de/schobers/zntg/allg5"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
var _ ui.Renderer = &Renderer{}
|
||||
|
||||
func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
|
||||
var err = allg5.Init(allg5.InitAll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
disp, err := allg5.NewDisplay(w, h, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eq, err := allg5.NewEventQueue()
|
||||
if err != nil {
|
||||
disp.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eq.RegisterKeyboard()
|
||||
eq.RegisterMouse()
|
||||
eq.RegisterDisplay(disp)
|
||||
|
||||
return &Renderer{disp, eq, map[string]*font{}}, nil
|
||||
}
|
||||
|
||||
// Renderer implements ui.Renderer using Allegro 5.
|
||||
type Renderer struct {
|
||||
disp *allg5.Display
|
||||
eq *allg5.EventQueue
|
||||
ft map[string]*font
|
||||
}
|
||||
|
||||
// Renderer implementation (events)
|
||||
|
||||
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
|
||||
r.disp.Flip()
|
||||
|
||||
var ev = eventWait(r.eq, wait)
|
||||
for nil != ev {
|
||||
switch e := ev.(type) {
|
||||
case *allg5.DisplayCloseEvent:
|
||||
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
|
||||
case *allg5.MouseButtonDownEvent:
|
||||
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
|
||||
case *allg5.MouseButtonUpEvent:
|
||||
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
|
||||
case *allg5.MouseMoveEvent:
|
||||
t.Handle(&ui.MouseMoveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
|
||||
}
|
||||
ev = r.eq.Get()
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer implementation (lifetime)
|
||||
|
||||
func (r *Renderer) Destroy() error {
|
||||
r.eq.Destroy()
|
||||
r.disp.Destroy()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renderer implementation (drawing)
|
||||
|
||||
func (r *Renderer) Clear(c color.Color) {
|
||||
allg5.ClearToColor(newColor(c))
|
||||
}
|
||||
|
||||
func (r *Renderer) CreateImage(im image.Image) (ui.Image, error) {
|
||||
bmp, err := allg5.NewBitmapFromImage(im, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uiImage{bmp}, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) CreateImagePath(path string) (ui.Image, error) {
|
||||
bmp, err := allg5.LoadBitmap(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uiImage{bmp}, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) CreateImageSize(w, h float32) (ui.Image, error) {
|
||||
bmp, err := allg5.NewVideoBitmap(int(w), int(h))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &uiImage{bmp}, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) DefaultTarget() ui.Image {
|
||||
return &uiImage{r.disp.Target()}
|
||||
}
|
||||
|
||||
func (r *Renderer) DrawImage(p geom.PointF32, im ui.Image) {
|
||||
bmp := r.mustGetBitmap(im)
|
||||
bmp.Draw(p.X, p.Y)
|
||||
}
|
||||
|
||||
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
|
||||
allg5.DrawFilledRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c))
|
||||
}
|
||||
|
||||
func (r *Renderer) Font(name string) ui.Font {
|
||||
return r.ft[name]
|
||||
}
|
||||
|
||||
func (r *Renderer) mustGetBitmap(im ui.Image) *allg5.Bitmap {
|
||||
m, ok := im.(*uiImage)
|
||||
if !ok {
|
||||
panic("image must be created on same renderer")
|
||||
}
|
||||
return m.bmp
|
||||
}
|
||||
|
||||
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
|
||||
allg5.DrawRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c), thickness)
|
||||
}
|
||||
|
||||
func (r *Renderer) RegisterFont(path, name string, size int) error {
|
||||
var f, err = allg5.LoadTTFFont(path, int(size))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var prev = r.ft[name]
|
||||
if prev != nil {
|
||||
prev.Destroy()
|
||||
}
|
||||
r.ft[name] = newFont(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) RegisterFonts(path string, fonts ...FontDefinition) error {
|
||||
for _, f := range fonts {
|
||||
err := r.RegisterFont(path, f.Name, f.Size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) RenderTo(im ui.Image) {
|
||||
bmp := r.mustGetBitmap(im)
|
||||
bmp.SetAsTarget()
|
||||
}
|
||||
|
||||
func (r *Renderer) RenderToDisplay() {
|
||||
r.disp.SetAsTarget()
|
||||
}
|
||||
|
||||
func (r *Renderer) Size() geom.PointF32 {
|
||||
return geom.PtF32(float32(r.disp.Width()), float32(r.disp.Height()))
|
||||
}
|
||||
|
||||
func (r *Renderer) SetWindowTitle(t string) {
|
||||
r.disp.SetWindowTitle(t)
|
||||
}
|
||||
|
||||
func (r *Renderer) Target() ui.Image {
|
||||
return &uiImage{allg5.CurrentTarget()}
|
||||
}
|
||||
|
||||
func (r *Renderer) Text(p geom.PointF32, font string, c color.Color, t string) {
|
||||
var f = r.ft[font]
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
f.f.Draw(p.X, p.Y, newColor(c), allg5.AlignLeft, t)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
func eventWait(eq *allg5.EventQueue, wait bool) allg5.Event {
|
||||
if wait {
|
||||
return eq.GetWait()
|
||||
}
|
||||
return eq.Get()
|
||||
}
|
||||
|
||||
func eventBase(e allg5.Event) ui.EventBase {
|
||||
return ui.EventBase{StampInSeconds: e.Stamp()}
|
||||
}
|
||||
|
||||
func mouseEvent(e allg5.MouseEvent) ui.MouseEvent {
|
||||
return ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}
|
||||
}
|
||||
|
||||
func newColor(c color.Color) allg5.Color {
|
||||
if c == nil {
|
||||
return newColor(color.Black)
|
||||
}
|
||||
var r, g, b, a = c.RGBA()
|
||||
var r8, g8, b8, a8 = byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)
|
||||
return allg5.NewColorAlpha(r8, g8, b8, a8)
|
||||
}
|
42
ui/button.go
Normal file
42
ui/button.go
Normal file
@ -0,0 +1,42 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Button struct {
|
||||
ControlBase
|
||||
|
||||
Text string
|
||||
}
|
||||
|
||||
func BuildButton(text string, factory func(b *Button)) *Button {
|
||||
var b = &Button{Text: text}
|
||||
factory(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Button) DesiredSize(ctx Context) geom.PointF32 {
|
||||
var fontName = b.fontName(ctx)
|
||||
var font = ctx.Renderer().Font(fontName)
|
||||
var width = font.WidthOf(b.Text)
|
||||
var height = font.Height()
|
||||
var pad = ctx.Style().Dimensions.TextPadding
|
||||
return geom.PtF32(width+pad*2, height+pad*2)
|
||||
}
|
||||
|
||||
func (b *Button) Render(ctx Context) {
|
||||
var fore = b.FontColor
|
||||
var style = ctx.Style()
|
||||
if fore == nil {
|
||||
fore = style.Palette.TextOnPrimary
|
||||
}
|
||||
var fill = style.Palette.Primary
|
||||
if b.over {
|
||||
fill = style.Palette.PrimaryHighlight
|
||||
}
|
||||
var pad = style.Dimensions.TextPadding
|
||||
var font = b.fontName(ctx)
|
||||
ctx.Renderer().FillRectangle(b.bounds, fill)
|
||||
ctx.Renderer().Text(b.bounds.Min.Add(geom.PtF32(pad, pad)), font, fore, b.Text)
|
||||
}
|
41
ui/containerbase.go
Normal file
41
ui/containerbase.go
Normal file
@ -0,0 +1,41 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type ContainerBase struct {
|
||||
ControlBase
|
||||
Children []Control
|
||||
}
|
||||
|
||||
func BuildContainerBase(controls ...Control) ContainerBase {
|
||||
return ContainerBase{
|
||||
Children: controls,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ContainerBase) AddChild(child ...Control) {
|
||||
c.Children = append(c.Children, child...)
|
||||
}
|
||||
|
||||
func (c *ContainerBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
for _, child := range c.Children {
|
||||
child.Arrange(ctx, bounds, offset)
|
||||
}
|
||||
c.ControlBase.Arrange(ctx, bounds, offset)
|
||||
}
|
||||
|
||||
func (c *ContainerBase) Handle(ctx Context, e Event) {
|
||||
for _, child := range c.Children {
|
||||
child.Handle(ctx, e)
|
||||
}
|
||||
c.ControlBase.Handle(ctx, e)
|
||||
}
|
||||
|
||||
func (c *ContainerBase) Render(ctx Context) {
|
||||
c.RenderBackground(ctx)
|
||||
for _, child := range c.Children {
|
||||
child.Render(ctx)
|
||||
}
|
||||
}
|
40
ui/context.go
Normal file
40
ui/context.go
Normal file
@ -0,0 +1,40 @@
|
||||
package ui
|
||||
|
||||
type Context interface {
|
||||
HasQuit() bool
|
||||
Images() *Images
|
||||
Quit()
|
||||
Renderer() Renderer
|
||||
Style() *Style
|
||||
}
|
||||
|
||||
var _ Context = &context{}
|
||||
var _ EventTarget = &context{}
|
||||
|
||||
type context struct {
|
||||
quit bool
|
||||
r Renderer
|
||||
view Control
|
||||
ims *Images
|
||||
style *Style
|
||||
}
|
||||
|
||||
func (c *context) HasQuit() bool { return c.quit }
|
||||
|
||||
func (c *context) Images() *Images { return c.ims }
|
||||
|
||||
func (c *context) Quit() { c.quit = true }
|
||||
|
||||
func (c *context) Renderer() Renderer { return c.r }
|
||||
|
||||
func (c *context) Style() *Style { return c.style }
|
||||
|
||||
// Handle implement EventTarget
|
||||
func (c *context) Handle(e Event) {
|
||||
switch e.(type) {
|
||||
case *DisplayCloseEvent:
|
||||
c.Quit()
|
||||
return
|
||||
}
|
||||
c.view.Handle(c, e)
|
||||
}
|
24
ui/control.go
Normal file
24
ui/control.go
Normal file
@ -0,0 +1,24 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Control interface {
|
||||
Arrange(Context, geom.RectangleF32, geom.PointF32)
|
||||
DesiredSize(Context) geom.PointF32
|
||||
Handle(Context, Event)
|
||||
Render(Context)
|
||||
|
||||
Bounds() geom.RectangleF32
|
||||
OnClick(ClickFn)
|
||||
OnDragStart(DragStartFn)
|
||||
OnDragMove(DragMoveFn)
|
||||
OnDragEnd(DragEndFn)
|
||||
}
|
||||
|
||||
type RootControl interface {
|
||||
Control
|
||||
|
||||
Init(Context)
|
||||
}
|
131
ui/controlbase.go
Normal file
131
ui/controlbase.go
Normal file
@ -0,0 +1,131 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type ClickFn func(ctx Context, c Control, pos geom.PointF32, btn MouseButton)
|
||||
type DragEndFn func(ctx Context, c Control, start, end geom.PointF32)
|
||||
type DragMoveFn func(ctx Context, c Control, start, move geom.PointF32)
|
||||
type DragStartFn func(ctx Context, c Control, start geom.PointF32)
|
||||
type MouseEnterFn func(ctx Context, c Control)
|
||||
type MouseLeaveFn func(ctx Context, c Control)
|
||||
|
||||
var _ Control = &ControlBase{}
|
||||
|
||||
type ControlBase struct {
|
||||
bounds geom.RectangleF32
|
||||
offset geom.PointF32
|
||||
dragStart *geom.PointF32
|
||||
over bool
|
||||
pressed bool
|
||||
|
||||
onClick ClickFn
|
||||
onDragEnd DragEndFn
|
||||
onDragMove DragMoveFn
|
||||
onDragStart DragStartFn
|
||||
|
||||
Background color.Color
|
||||
FontName string
|
||||
FontColor color.Color
|
||||
}
|
||||
|
||||
func (c *ControlBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
c.bounds = bounds
|
||||
c.offset = offset
|
||||
}
|
||||
|
||||
func (c *ControlBase) Bounds() geom.RectangleF32 {
|
||||
return c.bounds
|
||||
}
|
||||
|
||||
func (c *ControlBase) DesiredSize(Context) geom.PointF32 {
|
||||
return geom.ZeroPtF32
|
||||
}
|
||||
|
||||
func (c *ControlBase) Handle(ctx Context, e Event) {
|
||||
var pos = func(e MouseEvent) geom.PointF32 { return e.Pos().Sub(c.offset) }
|
||||
var over = func(e MouseEvent) bool {
|
||||
c.over = pos(e).In(c.bounds)
|
||||
return c.over
|
||||
}
|
||||
switch e := e.(type) {
|
||||
case *MouseMoveEvent:
|
||||
over(e.MouseEvent)
|
||||
if c.pressed {
|
||||
if c.dragStart == nil {
|
||||
var start = pos(e.MouseEvent)
|
||||
c.dragStart = &start
|
||||
if c.onDragStart != nil {
|
||||
c.onDragStart(ctx, c, start)
|
||||
}
|
||||
} else {
|
||||
var start = *c.dragStart
|
||||
var move = pos(e.MouseEvent)
|
||||
if c.onDragMove != nil {
|
||||
c.onDragMove(ctx, c, start, move)
|
||||
}
|
||||
}
|
||||
}
|
||||
case *MouseButtonDownEvent:
|
||||
if over(e.MouseEvent) && 1 == e.Button {
|
||||
c.pressed = true
|
||||
}
|
||||
case *MouseButtonUpEvent:
|
||||
if 1 == e.Button {
|
||||
if c.dragStart != nil {
|
||||
var start = *c.dragStart
|
||||
var end = pos(e.MouseEvent)
|
||||
c.dragStart = nil
|
||||
if c.onDragEnd != nil {
|
||||
c.onDragEnd(ctx, c, start, end)
|
||||
}
|
||||
}
|
||||
if c.pressed {
|
||||
if c.onClick != nil {
|
||||
c.onClick(ctx, c, pos(e.MouseEvent), e.Button)
|
||||
}
|
||||
}
|
||||
c.pressed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ControlBase) IsOver() bool { return c.over }
|
||||
|
||||
func (c *ControlBase) IsPressed() bool { return c.pressed }
|
||||
|
||||
func (c *ControlBase) OnClick(fn ClickFn) {
|
||||
c.onClick = fn
|
||||
}
|
||||
|
||||
func (c *ControlBase) OnDragStart(fn DragStartFn) {
|
||||
c.onDragStart = fn
|
||||
}
|
||||
|
||||
func (c *ControlBase) OnDragMove(fn DragMoveFn) {
|
||||
c.onDragMove = fn
|
||||
}
|
||||
|
||||
func (c *ControlBase) OnDragEnd(fn DragEndFn) {
|
||||
c.onDragEnd = fn
|
||||
}
|
||||
|
||||
func (c *ControlBase) RenderBackground(ctx Context) {
|
||||
if c.Background != nil {
|
||||
ctx.Renderer().FillRectangle(c.bounds, c.Background)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ControlBase) Render(Context) {
|
||||
}
|
||||
|
||||
func (c *ControlBase) fontName(ctx Context) string {
|
||||
var name = c.FontName
|
||||
if len(name) == 0 {
|
||||
name = ctx.Style().Fonts.Default
|
||||
}
|
||||
return name
|
||||
}
|
50
ui/event.go
Normal file
50
ui/event.go
Normal file
@ -0,0 +1,50 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/geom"
|
||||
|
||||
type DisplayCloseEvent struct {
|
||||
EventBase
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Stamp() float64
|
||||
}
|
||||
|
||||
type EventBase struct {
|
||||
StampInSeconds float64
|
||||
}
|
||||
|
||||
func (e *EventBase) Stamp() float64 {
|
||||
return e.StampInSeconds
|
||||
}
|
||||
|
||||
type MouseButton int
|
||||
|
||||
const (
|
||||
MouseButtonLeft MouseButton = 1
|
||||
MouseButtonRight MouseButton = 2
|
||||
MouseButtonMiddle MouseButton = 3
|
||||
)
|
||||
|
||||
type MouseButtonDownEvent struct {
|
||||
MouseEvent
|
||||
Button MouseButton
|
||||
}
|
||||
|
||||
type MouseButtonUpEvent struct {
|
||||
MouseEvent
|
||||
Button MouseButton
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
EventBase
|
||||
X, Y float32
|
||||
}
|
||||
|
||||
func (e *MouseEvent) Pos() geom.PointF32 {
|
||||
return geom.PtF32(e.X, e.Y)
|
||||
}
|
||||
|
||||
type MouseMoveEvent struct {
|
||||
MouseEvent
|
||||
}
|
5
ui/eventtarget.go
Normal file
5
ui/eventtarget.go
Normal file
@ -0,0 +1,5 @@
|
||||
package ui
|
||||
|
||||
type EventTarget interface {
|
||||
Handle(Event)
|
||||
}
|
48
ui/examples/01_basic/basic.go
Normal file
48
ui/examples/01_basic/basic.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
|
||||
"opslag.de/schobers/zntg/allg5"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
"opslag.de/schobers/zntg/ui/allg5ui"
|
||||
)
|
||||
|
||||
func run() error {
|
||||
var render, err = allg5ui.NewRenderer(800, 600, allg5.NewDisplayOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer render.Destroy()
|
||||
|
||||
err = render.RegisterFont("../resources/font/OpenSans-Regular.ttf", "default", 14)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var view = &ui.StackPanel{ContainerBase: ui.ContainerBase{
|
||||
ControlBase: ui.ControlBase{Background: color.White},
|
||||
Children: []ui.Control{
|
||||
&ui.Label{Text: "Hello, world!"},
|
||||
ui.BuildButton("Quit", func(b *ui.Button) {
|
||||
b.OnClick(func(ctx ui.Context, _ ui.Control, _ geom.PointF32, _ ui.MouseButton) {
|
||||
ctx.Quit()
|
||||
})
|
||||
}),
|
||||
ui.Stretch(&ui.Label{Text: "Content"}),
|
||||
&ui.Label{Text: "Status"},
|
||||
},
|
||||
}}
|
||||
|
||||
return ui.RunWait(render, ui.DefaultStyle(), view, true)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err = run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
202
ui/examples/resources/font/LICENSE.txt
Normal file
202
ui/examples/resources/font/LICENSE.txt
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
BIN
ui/examples/resources/font/OpenSans-Regular.ttf
Normal file
BIN
ui/examples/resources/font/OpenSans-Regular.ttf
Normal file
Binary file not shown.
42
ui/fixed.go
Normal file
42
ui/fixed.go
Normal file
@ -0,0 +1,42 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
var _ Control = &stretch{}
|
||||
|
||||
type fixed struct {
|
||||
Proxy
|
||||
|
||||
size geom.PointF32
|
||||
}
|
||||
|
||||
func (f *fixed) DesiredSize(ctx Context) geom.PointF32 {
|
||||
w, h := f.size.X, f.size.Y
|
||||
if geom.IsNaN32(w) || geom.IsNaN32(h) {
|
||||
child := f.Content.DesiredSize(ctx)
|
||||
if geom.IsNaN32(w) {
|
||||
w = child.X
|
||||
}
|
||||
if geom.IsNaN32(h) {
|
||||
h = child.Y
|
||||
}
|
||||
}
|
||||
return geom.PtF32(w, h)
|
||||
}
|
||||
|
||||
// Fixed wraps the supplied control to fill exactly the space specified.
|
||||
func Fixed(c Control, w, h float32) Control {
|
||||
return &fixed{Proxy{Content: c}, geom.PtF32(w, h)}
|
||||
}
|
||||
|
||||
// FixedHeight wraps the supplied control to fill exactly the height specified. Width is taken from wrapped control.
|
||||
func FixedHeight(c Control, h float32) Control {
|
||||
return Fixed(c, geom.NaN32(), h)
|
||||
}
|
||||
|
||||
// FixedWidth wraps the supplied control to fill exactly the width specified. Height is taken from wrapped control.
|
||||
func FixedWidth(c Control, w float32) Control {
|
||||
return Fixed(c, w, geom.NaN32())
|
||||
}
|
11
ui/font.go
Normal file
11
ui/font.go
Normal file
@ -0,0 +1,11 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Font interface {
|
||||
Height() float32
|
||||
Measure(t string) geom.RectangleF32
|
||||
WidthOf(t string) float32
|
||||
}
|
7
ui/image.go
Normal file
7
ui/image.go
Normal file
@ -0,0 +1,7 @@
|
||||
package ui
|
||||
|
||||
type Image interface {
|
||||
Height() float32
|
||||
Width() float32
|
||||
Destroy()
|
||||
}
|
48
ui/images.go
Normal file
48
ui/images.go
Normal file
@ -0,0 +1,48 @@
|
||||
package ui
|
||||
|
||||
type ImageFactoryFn func(Context) (Image, error)
|
||||
|
||||
type Images struct {
|
||||
Factories map[string]ImageFactoryFn
|
||||
Images map[string]Image
|
||||
}
|
||||
|
||||
func NewImages() *Images {
|
||||
return &Images{map[string]ImageFactoryFn{}, map[string]Image{}}
|
||||
}
|
||||
|
||||
func (i *Images) AddFactory(name string, fn ImageFactoryFn) {
|
||||
i.Factories[name] = fn
|
||||
}
|
||||
|
||||
func (i *Images) AddImage(name string, im Image) {
|
||||
curr := i.Images[name]
|
||||
if curr != nil {
|
||||
curr.Destroy()
|
||||
}
|
||||
i.Images[name] = im
|
||||
}
|
||||
|
||||
func (i *Images) Destroy() {
|
||||
for _, im := range i.Images {
|
||||
im.Destroy()
|
||||
}
|
||||
i.Images = nil
|
||||
}
|
||||
|
||||
func (i *Images) Image(ctx Context, name string) Image {
|
||||
im, ok := i.Images[name]
|
||||
if ok {
|
||||
return im
|
||||
}
|
||||
fact, ok := i.Factories[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
im, err := fact(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
i.Images[name] = im
|
||||
return im
|
||||
}
|
37
ui/label.go
Normal file
37
ui/label.go
Normal file
@ -0,0 +1,37 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
ControlBase
|
||||
|
||||
Text string
|
||||
}
|
||||
|
||||
func BuildLabel(text string, factory func(*Label)) *Label {
|
||||
var l = &Label{Text: text}
|
||||
factory(l)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Label) DesiredSize(ctx Context) geom.PointF32 {
|
||||
var fontName = l.fontName(ctx)
|
||||
var font = ctx.Renderer().Font(fontName)
|
||||
var width = font.WidthOf(l.Text)
|
||||
var height = font.Height()
|
||||
var pad = ctx.Style().Dimensions.TextPadding
|
||||
return geom.PtF32(width+pad*2, height+pad*2)
|
||||
}
|
||||
|
||||
func (l *Label) Render(ctx Context) {
|
||||
l.RenderBackground(ctx)
|
||||
var c = l.FontColor
|
||||
if c == nil {
|
||||
c = ctx.Style().Palette.Text
|
||||
}
|
||||
var f = l.fontName(ctx)
|
||||
var pad = ctx.Style().Dimensions.TextPadding
|
||||
ctx.Renderer().Text(l.bounds.Min.Add(geom.PtF32(pad, pad)), f, c, l.Text)
|
||||
}
|
76
ui/orientation.go
Normal file
76
ui/orientation.go
Normal file
@ -0,0 +1,76 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/geom"
|
||||
|
||||
type Orientation int
|
||||
|
||||
const (
|
||||
OrientationVertical Orientation = iota
|
||||
OrientationHorizontal
|
||||
)
|
||||
|
||||
func (o Orientation) FlipPt(p geom.PointF32) geom.PointF32 {
|
||||
if o == OrientationHorizontal {
|
||||
return geom.PtF32(p.Y, p.X)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (o Orientation) FlipRect(r geom.RectangleF32) geom.RectangleF32 {
|
||||
if o == OrientationHorizontal {
|
||||
return geom.RectF32(r.Min.Y, r.Min.X, r.Max.Y, r.Max.X)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (o Orientation) LengthParallel(pt geom.PointF32) float32 {
|
||||
if o == OrientationVertical {
|
||||
return pt.Y
|
||||
}
|
||||
return pt.X
|
||||
}
|
||||
|
||||
func (o Orientation) LengthPerpendicular(pt geom.PointF32) float32 {
|
||||
if o == OrientationVertical {
|
||||
return pt.X
|
||||
}
|
||||
return pt.Y
|
||||
}
|
||||
|
||||
func (o Orientation) Pt(parallel, perpendicular float32) geom.PointF32 {
|
||||
if o == OrientationVertical {
|
||||
return geom.PtF32(perpendicular, parallel)
|
||||
}
|
||||
return geom.PtF32(parallel, perpendicular)
|
||||
}
|
||||
|
||||
func (o Orientation) Rect(min geom.PointF32, parallel, perpendicular float32) geom.RectangleF32 {
|
||||
if o == OrientationVertical {
|
||||
return geom.RectF32(min.X, min.Y, min.X+perpendicular, min.Y+parallel)
|
||||
}
|
||||
return geom.RectF32(min.X, min.Y, min.X+parallel, min.Y+perpendicular)
|
||||
}
|
||||
|
||||
func (o Orientation) SizeParallel(r geom.RectangleF32) float32 {
|
||||
if o == OrientationVertical {
|
||||
return r.Dy()
|
||||
}
|
||||
return r.Dx()
|
||||
}
|
||||
|
||||
func (o Orientation) SizePerpendicular(r geom.RectangleF32) float32 {
|
||||
if o == OrientationVertical {
|
||||
return r.Dx()
|
||||
}
|
||||
return r.Dy()
|
||||
}
|
||||
|
||||
func (o Orientation) String() string {
|
||||
switch o {
|
||||
case OrientationVertical:
|
||||
return "vertical"
|
||||
case OrientationHorizontal:
|
||||
return "horizontal"
|
||||
}
|
||||
return "undefined"
|
||||
}
|
128
ui/overflow.go
Normal file
128
ui/overflow.go
Normal file
@ -0,0 +1,128 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type overflow struct {
|
||||
Proxy
|
||||
|
||||
Background color.Color
|
||||
|
||||
barWidth float32
|
||||
desired geom.PointF32
|
||||
bounds geom.RectangleF32
|
||||
buffer Image
|
||||
|
||||
hor *Scrollbar
|
||||
ver *Scrollbar
|
||||
}
|
||||
|
||||
func Overflow(content Control) Control {
|
||||
return OverflowBackground(content, nil)
|
||||
}
|
||||
|
||||
func OverflowBackground(content Control, back color.Color) Control {
|
||||
var o = &overflow{Proxy: Proxy{Content: content}, Background: back}
|
||||
o.hor = BuildScrollbar(OrientationHorizontal, func(*Scrollbar) {})
|
||||
o.ver = BuildScrollbar(OrientationVertical, func(*Scrollbar) {})
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
|
||||
var scroll = func(need, actual float32) bool {
|
||||
return !geom.IsNaN32(need) && need > actual
|
||||
}
|
||||
var size = o.desired
|
||||
hor = scroll(size.X, bounds.Dx())
|
||||
ver = scroll(size.Y, bounds.Dy())
|
||||
if ver && !hor {
|
||||
hor = scroll(size.X+o.barWidth, bounds.Dx())
|
||||
}
|
||||
if hor && !ver {
|
||||
ver = scroll(size.Y+o.barWidth, bounds.Dy())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *overflow) doOnVisibleBars(fn func(bar *Scrollbar)) {
|
||||
hor, ver := o.shouldScroll(o.bounds)
|
||||
if hor {
|
||||
fn(o.hor)
|
||||
}
|
||||
if ver {
|
||||
fn(o.ver)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
o.barWidth = ctx.Style().Dimensions.ScrollbarWidth
|
||||
o.desired = o.Content.DesiredSize(ctx)
|
||||
o.bounds = bounds
|
||||
|
||||
var hor, ver = o.shouldScroll(bounds)
|
||||
var contentX, contentY float32 = 0, 0
|
||||
var contentW, contentH = bounds.Dx(), bounds.Dy()
|
||||
if hor {
|
||||
contentX -= o.hor.Offset
|
||||
contentH = geom.Max32(0, contentH-o.barWidth)
|
||||
}
|
||||
if ver {
|
||||
contentY -= o.ver.Offset
|
||||
contentW = geom.Max32(0, contentW-o.barWidth)
|
||||
}
|
||||
o.Content.Arrange(ctx, geom.RectF32(contentX, contentY, contentW, contentH), offset.Add(bounds.Min))
|
||||
if hor {
|
||||
o.hor.Content = o.desired.X
|
||||
o.hor.Arrange(ctx, geom.RectF32(bounds.Min.X, bounds.Min.Y+contentH, bounds.Min.X+contentW, bounds.Max.Y), offset)
|
||||
}
|
||||
if ver {
|
||||
o.ver.Content = o.desired.Y
|
||||
o.ver.Arrange(ctx, geom.RectF32(bounds.Min.X+contentW, bounds.Min.Y, bounds.Max.X, bounds.Max.Y), offset)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *overflow) Handle(ctx Context, e Event) {
|
||||
hor, ver := o.shouldScroll(o.bounds)
|
||||
if hor {
|
||||
o.hor.Handle(ctx, e)
|
||||
}
|
||||
if ver {
|
||||
o.ver.Handle(ctx, e)
|
||||
}
|
||||
o.Content.Handle(ctx, e)
|
||||
}
|
||||
|
||||
func (o *overflow) Render(ctx Context) {
|
||||
var renderer = ctx.Renderer()
|
||||
|
||||
if o.Background != nil {
|
||||
ctx.Renderer().FillRectangle(o.bounds, o.Background)
|
||||
}
|
||||
|
||||
var content = o.Content.Bounds()
|
||||
if o.buffer == nil || o.buffer.Width() != content.Dx() || o.buffer.Height() != content.Dy() {
|
||||
if o.buffer != nil {
|
||||
o.buffer.Destroy()
|
||||
o.buffer = nil
|
||||
}
|
||||
buffer, err := renderer.CreateImageSize(content.Dx(), content.Dy())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
o.buffer = buffer
|
||||
}
|
||||
|
||||
target := renderer.Target()
|
||||
renderer.RenderTo(o.buffer)
|
||||
renderer.Clear(color.Transparent)
|
||||
o.Content.Render(ctx)
|
||||
renderer.RenderTo(target)
|
||||
renderer.DrawImage(o.bounds.Min, o.buffer)
|
||||
|
||||
o.doOnVisibleBars(func(bar *Scrollbar) {
|
||||
bar.Render(ctx)
|
||||
})
|
||||
}
|
45
ui/proxy.go
Normal file
45
ui/proxy.go
Normal file
@ -0,0 +1,45 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/geom"
|
||||
|
||||
var _ Control = &Proxy{}
|
||||
|
||||
type Proxy struct {
|
||||
Content Control
|
||||
}
|
||||
|
||||
func (p *Proxy) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
p.Content.Arrange(ctx, bounds, offset)
|
||||
}
|
||||
|
||||
func (p *Proxy) DesiredSize(ctx Context) geom.PointF32 {
|
||||
return p.Content.DesiredSize(ctx)
|
||||
}
|
||||
|
||||
func (p *Proxy) Handle(ctx Context, e Event) {
|
||||
p.Content.Handle(ctx, e)
|
||||
}
|
||||
|
||||
func (p *Proxy) Render(ctx Context) {
|
||||
p.Content.Render(ctx)
|
||||
}
|
||||
|
||||
func (p *Proxy) Bounds() geom.RectangleF32 {
|
||||
return p.Content.Bounds()
|
||||
}
|
||||
|
||||
func (p *Proxy) OnClick(fn ClickFn) {
|
||||
p.Content.OnClick(fn)
|
||||
}
|
||||
|
||||
func (p *Proxy) OnDragStart(fn DragStartFn) {
|
||||
p.Content.OnDragStart(fn)
|
||||
}
|
||||
|
||||
func (p *Proxy) OnDragMove(fn DragMoveFn) {
|
||||
p.Content.OnDragMove(fn)
|
||||
}
|
||||
|
||||
func (p *Proxy) OnDragEnd(fn DragEndFn) {
|
||||
p.Content.OnDragEnd(fn)
|
||||
}
|
32
ui/renderer.go
Normal file
32
ui/renderer.go
Normal file
@ -0,0 +1,32 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Renderer interface {
|
||||
// Events
|
||||
PushEvents(t EventTarget, wait bool)
|
||||
|
||||
// Lifetime
|
||||
Destroy() error
|
||||
|
||||
// Drawing
|
||||
Clear(c color.Color)
|
||||
CreateImage(m image.Image) (Image, error)
|
||||
CreateImagePath(path string) (Image, error)
|
||||
CreateImageSize(w, h float32) (Image, error)
|
||||
DefaultTarget() Image
|
||||
DrawImage(p geom.PointF32, im Image)
|
||||
FillRectangle(r geom.RectangleF32, c color.Color)
|
||||
Font(font string) Font
|
||||
Rectangle(r geom.RectangleF32, c color.Color, thickness float32)
|
||||
RenderTo(Image)
|
||||
RenderToDisplay()
|
||||
Size() geom.PointF32
|
||||
Target() Image
|
||||
Text(p geom.PointF32, font string, color color.Color, text string)
|
||||
}
|
80
ui/scrollbar.go
Normal file
80
ui/scrollbar.go
Normal file
@ -0,0 +1,80 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Scrollbar struct {
|
||||
ControlBase
|
||||
|
||||
Orientation Orientation
|
||||
|
||||
Content float32
|
||||
Offset float32
|
||||
|
||||
handle ScrollbarHandle
|
||||
startDragOffset float32
|
||||
}
|
||||
|
||||
func BuildScrollbar(o Orientation, fn func(s *Scrollbar)) *Scrollbar {
|
||||
var s = &Scrollbar{Orientation: o, Content: 0, Offset: 0}
|
||||
s.handle.OnDragStart(func(_ Context, _ Control, _ geom.PointF32) {
|
||||
s.startDragOffset = s.Offset
|
||||
})
|
||||
s.handle.OnDragMove(func(_ Context, _ Control, start, move geom.PointF32) {
|
||||
var length = s.Orientation.SizeParallel(s.bounds)
|
||||
var handleMaxOffset = length - s.Orientation.SizeParallel(s.handle.bounds)
|
||||
var hidden = s.Content - length
|
||||
var offset = (s.Orientation.LengthParallel(move) - s.Orientation.LengthParallel(start)) / handleMaxOffset
|
||||
s.Offset = geom.Max32(0, geom.Min32(s.startDragOffset+offset*hidden, hidden))
|
||||
})
|
||||
fn(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scrollbar) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
s.ControlBase.Arrange(ctx, bounds, offset)
|
||||
s.updateBar(ctx)
|
||||
}
|
||||
|
||||
func (s *Scrollbar) DesiredSize(ctx Context) geom.PointF32 {
|
||||
return s.Orientation.Pt(geom.NaN32(), ctx.Style().Dimensions.ScrollbarWidth)
|
||||
}
|
||||
|
||||
func (s *Scrollbar) Handle(ctx Context, e Event) {
|
||||
s.handle.Handle(ctx, e)
|
||||
s.ControlBase.Handle(ctx, e)
|
||||
}
|
||||
|
||||
func (s *Scrollbar) Render(ctx Context) {
|
||||
s.handle.Render(ctx)
|
||||
}
|
||||
|
||||
func (s *Scrollbar) updateBar(ctx Context) {
|
||||
var length = s.Orientation.SizeParallel(s.bounds)
|
||||
var width = ctx.Style().Dimensions.ScrollbarWidth
|
||||
var handleLength = length
|
||||
var handleOffset = s.Orientation.LengthParallel(s.bounds.Min)
|
||||
if s.Content > length {
|
||||
handleLength = geom.Max32(2*width, length/s.Content)
|
||||
var hidden = s.Content - length
|
||||
var offset = geom.Min32(1, s.Offset/hidden)
|
||||
handleOffset = handleOffset + offset*(length-handleLength)
|
||||
}
|
||||
var min = s.Orientation.Pt(handleOffset, s.Orientation.LengthPerpendicular(s.bounds.Max)-width)
|
||||
s.handle.Arrange(ctx, s.Orientation.Rect(min, handleLength, width), s.offset)
|
||||
}
|
||||
|
||||
type ScrollbarHandle struct {
|
||||
ControlBase
|
||||
}
|
||||
|
||||
func (h *ScrollbarHandle) Render(ctx Context) {
|
||||
h.RenderBackground(ctx)
|
||||
p := ctx.Style().Palette
|
||||
fill := p.Primary
|
||||
if h.over {
|
||||
fill = p.PrimaryHighlight
|
||||
}
|
||||
ctx.Renderer().FillRectangle(h.bounds.Inset(1), fill)
|
||||
}
|
79
ui/stackpanel.go
Normal file
79
ui/stackpanel.go
Normal file
@ -0,0 +1,79 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/geom"
|
||||
|
||||
type StackPanel struct {
|
||||
ContainerBase
|
||||
Orientation Orientation
|
||||
}
|
||||
|
||||
func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32) {
|
||||
bounds = p.Orientation.FlipRect(bounds)
|
||||
var length float32
|
||||
var stretch int
|
||||
var desired = make([]geom.PointF32, len(p.Children))
|
||||
for i, child := range p.Children {
|
||||
var size = p.Orientation.FlipPt(child.DesiredSize(ctx))
|
||||
if geom.IsNaN32(size.Y) {
|
||||
stretch++
|
||||
} else {
|
||||
length += size.Y
|
||||
}
|
||||
desired[i] = size
|
||||
}
|
||||
var remainder = bounds.Dy() - length
|
||||
var childOffset float32
|
||||
for i, size := range desired {
|
||||
var height = size.Y
|
||||
if geom.IsNaN32(size.Y) {
|
||||
if remainder < 0 {
|
||||
height = 0
|
||||
} else {
|
||||
height = remainder / float32(stretch)
|
||||
}
|
||||
} else if childOffset+height > bounds.Dy() {
|
||||
height = bounds.Dy() - childOffset
|
||||
}
|
||||
var child = geom.RectF32(bounds.Min.X, bounds.Min.Y+childOffset, bounds.Max.X, bounds.Min.Y+childOffset+height)
|
||||
p.Children[i].Arrange(ctx, p.Orientation.FlipRect(child), offset)
|
||||
childOffset += height
|
||||
}
|
||||
p.ControlBase.Arrange(ctx, p.Orientation.FlipRect(bounds), offset)
|
||||
}
|
||||
|
||||
func (p *StackPanel) DesiredSize(ctx Context) geom.PointF32 {
|
||||
var length float32
|
||||
var width float32
|
||||
for _, child := range p.Children {
|
||||
var size = child.DesiredSize(ctx)
|
||||
var l, w = p.Orientation.LengthParallel(size), p.Orientation.LengthPerpendicular(size)
|
||||
if geom.IsNaN32(l) {
|
||||
length = l
|
||||
} else {
|
||||
if !geom.IsNaN32(length) {
|
||||
length += l
|
||||
}
|
||||
}
|
||||
|
||||
if geom.IsNaN32(w) {
|
||||
width = w
|
||||
} else {
|
||||
if !geom.IsNaN32(width) {
|
||||
width = geom.Max32(width, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return p.Orientation.Pt(length, width)
|
||||
}
|
||||
|
||||
func (p *StackPanel) Render(ctx Context) {
|
||||
p.RenderBackground(ctx)
|
||||
var bounds = p.Bounds()
|
||||
for _, child := range p.Children {
|
||||
var childB = child.Bounds()
|
||||
if childB.Min.X >= bounds.Max.X || childB.Min.Y >= bounds.Max.Y || childB.Max.X < bounds.Min.X || childB.Max.Y < bounds.Min.Y {
|
||||
continue
|
||||
}
|
||||
child.Render(ctx)
|
||||
}
|
||||
}
|
40
ui/stretch.go
Normal file
40
ui/stretch.go
Normal file
@ -0,0 +1,40 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
var _ Control = &stretch{}
|
||||
|
||||
type stretch struct {
|
||||
Proxy
|
||||
|
||||
w, h bool
|
||||
}
|
||||
|
||||
func (s *stretch) DesiredSize(ctx Context) geom.PointF32 {
|
||||
var size = geom.PtF32(geom.NaN32(), geom.NaN32())
|
||||
if !s.w || !s.h {
|
||||
child := s.Content.DesiredSize(ctx)
|
||||
if !s.w {
|
||||
size.X = child.X
|
||||
}
|
||||
if !s.h {
|
||||
size.Y = child.Y
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func newStretch(c Control, w, h bool) *stretch {
|
||||
return &stretch{Proxy{Content: c}, w, h}
|
||||
}
|
||||
|
||||
// Stretch wraps the supplied control to stretch in both directions.
|
||||
func Stretch(c Control) Control { return newStretch(c, true, true) }
|
||||
|
||||
// StretchHeight wraps the supplied control to stretch vertically. Width is taken from wrapped control.
|
||||
func StretchHeight(c Control) Control { return newStretch(c, false, true) }
|
||||
|
||||
// StretchWidth wraps the supplied control to stretch horizontally. Height is taken from wrapped control.
|
||||
func StretchWidth(c Control) Control { return newStretch(c, true, false) }
|
82
ui/style.go
Normal file
82
ui/style.go
Normal file
@ -0,0 +1,82 @@
|
||||
package ui
|
||||
|
||||
import "image/color"
|
||||
|
||||
var defaultDimensions *Dimensions
|
||||
var defaultFonts *Fonts
|
||||
var defaultPalette *Palette
|
||||
var defaultStyle *Style
|
||||
|
||||
type Dimensions struct {
|
||||
ScrollbarWidth float32
|
||||
TextPadding float32
|
||||
}
|
||||
|
||||
type Fonts struct {
|
||||
Default string
|
||||
}
|
||||
|
||||
type Palette struct {
|
||||
Background color.Color
|
||||
Primary color.Color
|
||||
PrimaryHighlight color.Color
|
||||
ShadedBackground color.Color
|
||||
Text color.Color
|
||||
TextDisabled color.Color
|
||||
TextOnPrimary color.Color
|
||||
}
|
||||
|
||||
type Style struct {
|
||||
Dimensions *Dimensions
|
||||
Fonts *Fonts
|
||||
Palette *Palette
|
||||
}
|
||||
|
||||
func DefaultDimensions() *Dimensions {
|
||||
if defaultDimensions == nil {
|
||||
defaultDimensions = &Dimensions{
|
||||
ScrollbarWidth: 16.,
|
||||
TextPadding: 8.,
|
||||
}
|
||||
}
|
||||
return defaultDimensions
|
||||
}
|
||||
|
||||
func DefaultFonts() *Fonts {
|
||||
if defaultFonts == nil {
|
||||
defaultFonts = &Fonts{
|
||||
Default: "default",
|
||||
}
|
||||
}
|
||||
return defaultFonts
|
||||
}
|
||||
|
||||
func DefaultPalette() *Palette {
|
||||
if defaultPalette == nil {
|
||||
defaultPalette = &Palette{
|
||||
Background: color.White,
|
||||
Primary: RGBA(0x3F, 0x51, 0xB5, 0xFF),
|
||||
PrimaryHighlight: RGBA(0x5C, 0x6B, 0xC0, 0xFF),
|
||||
ShadedBackground: RGBA(0xFA, 0xFA, 0xFA, 0xFF),
|
||||
Text: color.Black,
|
||||
TextDisabled: RGBA(0xBD, 0xBD, 0xBD, 0xFF),
|
||||
TextOnPrimary: color.White,
|
||||
}
|
||||
}
|
||||
return defaultPalette
|
||||
}
|
||||
|
||||
func DefaultStyle() *Style {
|
||||
if defaultStyle == nil {
|
||||
defaultStyle = &Style{
|
||||
Dimensions: DefaultDimensions(),
|
||||
Fonts: DefaultFonts(),
|
||||
Palette: DefaultPalette(),
|
||||
}
|
||||
}
|
||||
return defaultStyle
|
||||
}
|
||||
|
||||
func RGBA(r, g, b, a byte) *color.RGBA {
|
||||
return &color.RGBA{R: r, G: g, B: b, A: a}
|
||||
}
|
30
ui/ui.go
Normal file
30
ui/ui.go
Normal file
@ -0,0 +1,30 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
// Run runs the application loop.
|
||||
func Run(r Renderer, s *Style, view Control) error {
|
||||
return RunWait(r, s, view, false)
|
||||
}
|
||||
|
||||
// RunWait runs the application loop and conditionally waits on events before rendering.
|
||||
func RunWait(r Renderer, s *Style, view Control, wait bool) error {
|
||||
ctx := &context{r: r, style: s, view: view, ims: NewImages()}
|
||||
root, ok := view.(RootControl)
|
||||
if ok {
|
||||
root.Init(ctx)
|
||||
}
|
||||
for !ctx.quit {
|
||||
var size = r.Size()
|
||||
var bounds = geom.RectF32(0, 0, size.X, size.Y)
|
||||
view.Arrange(ctx, bounds, geom.ZeroPtF32)
|
||||
view.Render(ctx)
|
||||
if ctx.quit {
|
||||
return nil
|
||||
}
|
||||
r.PushEvents(ctx, wait)
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user