Compare commits
2 Commits
451f389ef6
...
b431e44110
Author | SHA1 | Date | |
---|---|---|---|
b431e44110 | |||
c194f443a7 |
117
alui/application.go
Normal file
117
alui/application.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/fs/vfs"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
disp *allg5.Display
|
||||||
|
eq *allg5.EventQueue
|
||||||
|
dirs map[string]vfs.CopyDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplication(size geom.Point, dispOptions allg5.NewDisplayOptions) (*Application, error) {
|
||||||
|
var init = allg5.InitAll
|
||||||
|
init.Audio = false
|
||||||
|
err := allg5.Init(init)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
app := &Application{}
|
||||||
|
|
||||||
|
disp, err := allg5.NewDisplay(size.X, size.Y, dispOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.disp = disp
|
||||||
|
|
||||||
|
eq, err := allg5.NewEventQueue()
|
||||||
|
if err != nil {
|
||||||
|
app.Destroy()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.eq = eq
|
||||||
|
|
||||||
|
eq.RegisterDisplay(disp)
|
||||||
|
eq.RegisterKeyboard()
|
||||||
|
eq.RegisterMouse()
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) Destroy() {
|
||||||
|
a.disp.Destroy()
|
||||||
|
a.eq.Destroy()
|
||||||
|
if a.dirs != nil {
|
||||||
|
for _, dir := range a.dirs {
|
||||||
|
dir.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) AddDir(path string, backup afero.Fs) (vfs.CopyDir, error) {
|
||||||
|
fs := vfs.NewOsFsFallback(path, backup)
|
||||||
|
copy, err := vfs.NewCopyDir(fs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if a.dirs == nil {
|
||||||
|
a.dirs = map[string]vfs.CopyDir{}
|
||||||
|
}
|
||||||
|
a.dirs[path] = copy
|
||||||
|
return copy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) UseFile(dir, name string, use func(string) error) error {
|
||||||
|
if a.dirs == nil {
|
||||||
|
return errors.New(`no directories available`)
|
||||||
|
}
|
||||||
|
resources, ok := a.dirs[dir]
|
||||||
|
if !ok {
|
||||||
|
return errors.New(`direct not found`)
|
||||||
|
}
|
||||||
|
path, err := resources.Retrieve(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return use(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) Run(main Control, init func(*UI) error) error {
|
||||||
|
disp := a.disp
|
||||||
|
eq := a.eq
|
||||||
|
|
||||||
|
ui := NewUI(disp, main)
|
||||||
|
defer ui.Destroy()
|
||||||
|
err := init(ui)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := ui.Context()
|
||||||
|
for {
|
||||||
|
allg5.ClearToColor(ctx.Palette.Icon)
|
||||||
|
ui.Render()
|
||||||
|
disp.Flip()
|
||||||
|
|
||||||
|
e := eq.Get()
|
||||||
|
for e != nil {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *allg5.DisplayCloseEvent:
|
||||||
|
return nil
|
||||||
|
case *allg5.KeyCharEvent:
|
||||||
|
if e.KeyCode == allg5.KeyF4 && e.Modifiers&allg5.KeyModAlt != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Handle(e)
|
||||||
|
e = eq.Get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
alui/button.go
Normal file
37
alui/button.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Control = &Button{}
|
||||||
|
|
||||||
|
type Button struct {
|
||||||
|
ControlBase
|
||||||
|
|
||||||
|
Text string
|
||||||
|
TextAlign allg5.HorizontalAlignment
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewButton(text string, onClick func()) *Button {
|
||||||
|
b := &Button{Text: text}
|
||||||
|
b.OnClick = onClick
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Button) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
font := ctx.Fonts.Get(b.Font)
|
||||||
|
w := font.TextWidth(b.Text)
|
||||||
|
return geom.PtF32(w+8, font.Height()+8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Button) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
fore := ctx.Palette.Primary
|
||||||
|
if b.Over {
|
||||||
|
fore = ctx.Palette.Dark
|
||||||
|
ctx.Cursor = allg5.MouseCursorLink
|
||||||
|
}
|
||||||
|
font := ctx.Fonts.Get(b.Font)
|
||||||
|
ctx.Fonts.DrawAlignFont(font, bounds.Min.X+4, bounds.Min.Y+4, bounds.Max.X-4, fore, b.TextAlign, b.Text)
|
||||||
|
}
|
23
alui/center.go
Normal file
23
alui/center.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type center struct {
|
||||||
|
Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func Center(control Control) Control {
|
||||||
|
return ¢er{Proxy: Proxy{Target: control}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *center) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
size := c.DesiredSize(ctx)
|
||||||
|
center := bounds.Center()
|
||||||
|
|
||||||
|
size.X = geom.Min32(size.X, bounds.Dx())
|
||||||
|
size.Y = geom.Min32(size.Y, bounds.Dy())
|
||||||
|
size = size.Mul(.5)
|
||||||
|
c.Proxy.Layout(ctx, geom.RectF32(center.X-size.X, center.Y-size.Y, center.X+size.X, center.Y+size.Y))
|
||||||
|
}
|
34
alui/column.go
Normal file
34
alui/column.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import "opslag.de/schobers/geom"
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Proxy
|
||||||
|
|
||||||
|
panel *StackPanel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewColumn() *Column {
|
||||||
|
c := &Column{}
|
||||||
|
c.Init()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) AddChild(child ...Control) {
|
||||||
|
c.panel.Children = append(c.panel.Children, child...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) Init() {
|
||||||
|
c.panel = &StackPanel{Orientation: OrientationVertical}
|
||||||
|
c.Proxy.Target = c.panel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) Layout(ctx *Context, bounds geom.RectangleF32) {}
|
||||||
|
|
||||||
|
func (c *Column) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
columnHeight := c.Proxy.DesiredSize(ctx).Y
|
||||||
|
width, center := bounds.Dx(), bounds.Center()
|
||||||
|
columnBounds := geom.RectF32(.25*width, center.Y-.5*columnHeight, .75*width, center.Y+.5*columnHeight)
|
||||||
|
c.Proxy.Layout(ctx, columnBounds)
|
||||||
|
c.Proxy.Render(ctx, columnBounds)
|
||||||
|
}
|
55
alui/container.go
Normal file
55
alui/container.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
ControlBase
|
||||||
|
|
||||||
|
Children []Control
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) AddChild(child ...Control) {
|
||||||
|
c.Children = append(c.Children, child...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
var size geom.PointF32
|
||||||
|
for _, child := range c.Children {
|
||||||
|
s := child.DesiredSize(ctx)
|
||||||
|
if geom.IsNaN32(s.X) || geom.IsNaN32(size.X) {
|
||||||
|
size.X = geom.NaN32()
|
||||||
|
} else {
|
||||||
|
size.X = geom.Max32(s.X, size.X)
|
||||||
|
}
|
||||||
|
if geom.IsNaN32(s.Y) || geom.IsNaN32(size.Y) {
|
||||||
|
size.Y = geom.NaN32()
|
||||||
|
} else {
|
||||||
|
size.Y = geom.Max32(s.Y, size.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Handle(e allg5.Event) {
|
||||||
|
c.ControlBase.Handle(e)
|
||||||
|
for _, child := range c.Children {
|
||||||
|
child.Handle(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
c.ControlBase.Layout(ctx, bounds)
|
||||||
|
for _, child := range c.Children {
|
||||||
|
child.Layout(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
c.ControlBase.Render(ctx, bounds)
|
||||||
|
for _, child := range c.Children {
|
||||||
|
child.Render(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
21
alui/context.go
Normal file
21
alui/context.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Display *allg5.Display
|
||||||
|
Fonts *Fonts
|
||||||
|
Cursor allg5.MouseCursor
|
||||||
|
Palette Palette
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) DisplayBounds() geom.RectangleF32 {
|
||||||
|
return geom.RectF32(0, 0, float32(c.Display.Width()), float32(c.Display.Height()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Font string
|
||||||
|
}
|
73
alui/control.go
Normal file
73
alui/control.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Control interface {
|
||||||
|
Bounds() geom.RectangleF32
|
||||||
|
|
||||||
|
Handle(allg5.Event)
|
||||||
|
|
||||||
|
DesiredSize(*Context) geom.PointF32
|
||||||
|
Layout(*Context, geom.RectangleF32)
|
||||||
|
Render(*Context, geom.RectangleF32)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bounds struct {
|
||||||
|
value geom.RectangleF32
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Control = &ControlBase{}
|
||||||
|
|
||||||
|
type ControlBase struct {
|
||||||
|
_Bounds geom.RectangleF32
|
||||||
|
Over bool
|
||||||
|
Font string
|
||||||
|
Foreground *allg5.Color
|
||||||
|
Background *allg5.Color
|
||||||
|
|
||||||
|
OnClick func()
|
||||||
|
OnEnter func()
|
||||||
|
OnLeave func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MouseEventToPos(e allg5.MouseEvent) geom.PointF32 { return geom.PtF32(float32(e.X), float32(e.Y)) }
|
||||||
|
|
||||||
|
func (b *ControlBase) DesiredSize(*Context) geom.PointF32 { return geom.ZeroPtF32 }
|
||||||
|
|
||||||
|
func (b *ControlBase) Handle(e allg5.Event) {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *allg5.MouseMoveEvent:
|
||||||
|
pos := MouseEventToPos(e.MouseEvent)
|
||||||
|
over := pos.In(b._Bounds)
|
||||||
|
if over != b.Over {
|
||||||
|
b.Over = over
|
||||||
|
if over {
|
||||||
|
if b.OnEnter != nil {
|
||||||
|
b.OnEnter()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if b.OnLeave != nil {
|
||||||
|
b.OnLeave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *allg5.MouseButtonDownEvent:
|
||||||
|
if !b.Over {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if e.Button == allg5.MouseButtonLeft {
|
||||||
|
if b.OnClick != nil {
|
||||||
|
b.OnClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ControlBase) Layout(_ *Context, bounds geom.RectangleF32) { b._Bounds = bounds }
|
||||||
|
|
||||||
|
func (b *ControlBase) Render(*Context, geom.RectangleF32) {}
|
||||||
|
|
||||||
|
func (b *ControlBase) Bounds() geom.RectangleF32 { return b._Bounds }
|
93
alui/fonts.go
Normal file
93
alui/fonts.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Fonts struct {
|
||||||
|
fonts map[string]*allg5.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
type FontDescription struct {
|
||||||
|
Path string
|
||||||
|
Size int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFonts() *Fonts {
|
||||||
|
return &Fonts{map[string]*allg5.Font{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) Destroy() {
|
||||||
|
for _, font := range f.fonts {
|
||||||
|
font.Destroy()
|
||||||
|
}
|
||||||
|
f.fonts = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) Draw(font string, left, top float32, color allg5.Color, text string) {
|
||||||
|
f.DrawFont(f.Get(font), left, top, color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) DrawAlign(font string, left, top, right float32, color allg5.Color, align allg5.HorizontalAlignment, text string) {
|
||||||
|
f.DrawAlignFont(f.Get(font), left, top, right, color, align, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) DrawAlignFont(font *allg5.Font, left, top, right float32, color allg5.Color, align allg5.HorizontalAlignment, text string) {
|
||||||
|
switch align {
|
||||||
|
case allg5.AlignCenter:
|
||||||
|
center, top := geom.Round32(.5*(left+right)), geom.Round32(top)
|
||||||
|
font.Draw(center, top, color, allg5.AlignCenter, text)
|
||||||
|
case allg5.AlignRight:
|
||||||
|
right, top = geom.Round32(right), geom.Round32(top)
|
||||||
|
font.Draw(right, top, color, allg5.AlignRight, text)
|
||||||
|
default:
|
||||||
|
left, top = geom.Round32(left), geom.Round32(top)
|
||||||
|
font.Draw(left, top, color, allg5.AlignLeft, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) DrawCenter(font string, center, top float32, color allg5.Color, text string) {
|
||||||
|
f.DrawCenterFont(f.Get(font), center, top, color, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) DrawCenterFont(font *allg5.Font, center, top float32, color allg5.Color, text string) {
|
||||||
|
f.DrawAlignFont(font, center, top, center, color, allg5.AlignCenter, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) DrawFont(font *allg5.Font, left, top float32, color allg5.Color, text string) {
|
||||||
|
left, top = geom.Round32(left), geom.Round32(top)
|
||||||
|
font.Draw(left, top, color, allg5.AlignLeft, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) Len() int { return len(f.fonts) }
|
||||||
|
|
||||||
|
func (f *Fonts) Load(path string, size int, name string) error {
|
||||||
|
font, err := allg5.LoadTTFFont(path, size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if old := f.fonts[name]; old != nil {
|
||||||
|
old.Destroy()
|
||||||
|
}
|
||||||
|
f.fonts[name] = font
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) LoadFonts(descriptions ...FontDescription) error {
|
||||||
|
for _, desc := range descriptions {
|
||||||
|
err := f.Load(desc.Path, desc.Size, desc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fonts) Get(name string) *allg5.Font {
|
||||||
|
if name == "" {
|
||||||
|
return f.Get("default")
|
||||||
|
}
|
||||||
|
return f.fonts[name]
|
||||||
|
}
|
34
alui/label.go
Normal file
34
alui/label.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Control = &Label{}
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
ControlBase
|
||||||
|
|
||||||
|
Text string
|
||||||
|
TextAlign allg5.HorizontalAlignment
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
font := ctx.Fonts.Get(l.Font)
|
||||||
|
_, _, w, h := font.TextDimensions(l.Text)
|
||||||
|
return geom.PtF32(w+8, h+8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
back := l.Background
|
||||||
|
fore := l.Foreground
|
||||||
|
if fore == nil {
|
||||||
|
fore = &ctx.Palette.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
if back != nil {
|
||||||
|
allg5.DrawFilledRectangle(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, *back)
|
||||||
|
}
|
||||||
|
ctx.Fonts.DrawAlign(l.Font, bounds.Min.X+4, bounds.Min.Y+4, bounds.Max.X-4, *fore, l.TextAlign, l.Text)
|
||||||
|
}
|
53
alui/margins.go
Normal file
53
alui/margins.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Control = &Margins{}
|
||||||
|
|
||||||
|
type Margins struct {
|
||||||
|
Proxy
|
||||||
|
|
||||||
|
Top, Left, Bottom, Right float32
|
||||||
|
|
||||||
|
bounds geom.RectangleF32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMargins(target Control, margins ...float32) *Margins {
|
||||||
|
m := &Margins{Proxy: Proxy{Target: target}}
|
||||||
|
switch len(margins) {
|
||||||
|
case 1:
|
||||||
|
m.Top, m.Left, m.Bottom, m.Right = margins[0], margins[0], margins[0], margins[0]
|
||||||
|
case 2:
|
||||||
|
m.Top, m.Left, m.Bottom, m.Right = margins[0], margins[1], margins[0], margins[1]
|
||||||
|
case 3:
|
||||||
|
m.Top, m.Left, m.Bottom, m.Right = margins[0], margins[1], margins[2], margins[1]
|
||||||
|
case 4:
|
||||||
|
m.Top, m.Left, m.Bottom, m.Right = margins[0], margins[1], margins[2], margins[3]
|
||||||
|
default:
|
||||||
|
panic("expected 1 (all same), 2 (vertical, horizontal), 3 (top, horizontal, bottom) or 4 margins (all separately specified)")
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Margins) Bounds() geom.RectangleF32 { return m.bounds }
|
||||||
|
|
||||||
|
func (m *Margins) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
return m.Proxy.DesiredSize(ctx).Add2D(m.Left+m.Right, m.Top+m.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Margins) inset(bounds geom.RectangleF32) geom.RectangleF32 {
|
||||||
|
return geom.RectF32(bounds.Min.X+m.Left, bounds.Min.Y+m.Top, bounds.Max.X-m.Right, bounds.Max.Y-m.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Margins) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
m.bounds = bounds
|
||||||
|
target := m.inset(bounds)
|
||||||
|
m.Proxy.Layout(ctx, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Margins) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
target := m.inset(bounds)
|
||||||
|
m.Proxy.Render(ctx, target)
|
||||||
|
}
|
85
alui/menu.go
Normal file
85
alui/menu.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Menu struct {
|
||||||
|
Column
|
||||||
|
|
||||||
|
OnEscape func()
|
||||||
|
|
||||||
|
active int
|
||||||
|
buttons []*Button
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMenu() *Menu {
|
||||||
|
m := &Menu{}
|
||||||
|
m.Init()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Activate(i int) {
|
||||||
|
if len(m.buttons) == 0 || i < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.active = i % len(m.buttons)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Add(text string, onClick func()) {
|
||||||
|
idx := len(m.buttons)
|
||||||
|
button := &Button{Text: text, TextAlign: allg5.AlignCenter}
|
||||||
|
button.OnClick = onClick
|
||||||
|
button.OnEnter = func() {
|
||||||
|
m.updateActiveButton(idx)
|
||||||
|
}
|
||||||
|
if idx == 0 {
|
||||||
|
button.Over = true
|
||||||
|
}
|
||||||
|
m.buttons = append(m.buttons, button)
|
||||||
|
m.AddChild(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) Handle(e allg5.Event) {
|
||||||
|
m.Column.Handle(e)
|
||||||
|
|
||||||
|
if len(m.buttons) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *allg5.KeyDownEvent:
|
||||||
|
switch e.KeyCode {
|
||||||
|
case allg5.KeyEscape:
|
||||||
|
if onEscape := m.OnEscape; onEscape != nil {
|
||||||
|
onEscape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *allg5.KeyCharEvent:
|
||||||
|
switch e.KeyCode {
|
||||||
|
case allg5.KeyDown:
|
||||||
|
m.updateActiveButton((m.active + 1) % len(m.buttons))
|
||||||
|
case allg5.KeyUp:
|
||||||
|
m.updateActiveButton((m.active + len(m.buttons) - 1) % len(m.buttons))
|
||||||
|
case allg5.KeyEnter:
|
||||||
|
if onClick := m.buttons[m.active].OnClick; onClick != nil {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *allg5.MouseMoveEvent:
|
||||||
|
for i, btn := range m.buttons {
|
||||||
|
if btn.Over {
|
||||||
|
m.updateActiveButton(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.updateActiveButton(m.active)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Menu) updateActiveButton(active int) {
|
||||||
|
m.active = active
|
||||||
|
for i, btn := range m.buttons {
|
||||||
|
btn.Over = i == m.active
|
||||||
|
}
|
||||||
|
}
|
8
alui/orientation.go
Normal file
8
alui/orientation.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
type Orientation string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrientationVertical Orientation = "vertical"
|
||||||
|
OrientationHorizontal = "horizontal"
|
||||||
|
)
|
39
alui/overlay.go
Normal file
39
alui/overlay.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Overlay struct {
|
||||||
|
ControlBase
|
||||||
|
|
||||||
|
Proxy Control
|
||||||
|
|
||||||
|
Visible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Overlay) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
if o.Visible {
|
||||||
|
return o.Proxy.DesiredSize(ctx)
|
||||||
|
}
|
||||||
|
return geom.ZeroPtF32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Overlay) Handle(e allg5.Event) {
|
||||||
|
if o.Visible {
|
||||||
|
o.Proxy.Handle(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Overlay) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
if o.Visible {
|
||||||
|
o.Proxy.Render(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Overlay) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
if o.Visible {
|
||||||
|
o.Proxy.Layout(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
15
alui/palette.go
Normal file
15
alui/palette.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Palette struct {
|
||||||
|
Primary allg5.Color
|
||||||
|
Light allg5.Color
|
||||||
|
Dark allg5.Color
|
||||||
|
Text allg5.Color
|
||||||
|
TextLight allg5.Color
|
||||||
|
Icon allg5.Color
|
||||||
|
Accent allg5.Color
|
||||||
|
}
|
44
alui/proxy.go
Normal file
44
alui/proxy.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Control = &Proxy{}
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
Target Control
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Bounds() geom.RectangleF32 {
|
||||||
|
if p.Target != nil {
|
||||||
|
return p.Target.Bounds()
|
||||||
|
}
|
||||||
|
return geom.RectangleF32{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
if p.Target != nil {
|
||||||
|
return p.Target.DesiredSize(ctx)
|
||||||
|
}
|
||||||
|
return geom.ZeroPtF32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Handle(e allg5.Event) {
|
||||||
|
if p.Target != nil {
|
||||||
|
p.Target.Handle(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
if p.Target != nil {
|
||||||
|
p.Target.Layout(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
if p.Target != nil {
|
||||||
|
p.Target.Render(ctx, bounds)
|
||||||
|
}
|
||||||
|
}
|
99
alui/stackpanel.go
Normal file
99
alui/stackpanel.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Control = &StackPanel{}
|
||||||
|
|
||||||
|
type StackPanel struct {
|
||||||
|
Container
|
||||||
|
|
||||||
|
Orientation Orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) asLength(p geom.PointF32) float32 {
|
||||||
|
switch s.Orientation {
|
||||||
|
case OrientationHorizontal:
|
||||||
|
return p.X
|
||||||
|
default:
|
||||||
|
return p.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) asWidth(p geom.PointF32) float32 {
|
||||||
|
switch s.Orientation {
|
||||||
|
case OrientationHorizontal:
|
||||||
|
return p.Y
|
||||||
|
default:
|
||||||
|
return p.X
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) asSize(length, width float32) geom.PointF32 {
|
||||||
|
switch s.Orientation {
|
||||||
|
case OrientationHorizontal:
|
||||||
|
return geom.PtF32(length, width)
|
||||||
|
default:
|
||||||
|
return geom.PtF32(width, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) CalculateLayout(ctx *Context) ([]geom.PointF32, geom.PointF32) {
|
||||||
|
desired := make([]geom.PointF32, len(s.Children))
|
||||||
|
for i, child := range s.Children {
|
||||||
|
desired[i] = child.DesiredSize(ctx)
|
||||||
|
}
|
||||||
|
var length, width float32
|
||||||
|
for _, size := range desired {
|
||||||
|
w, l := s.asWidth(size), s.asLength(size)
|
||||||
|
if geom.IsNaN32(w) {
|
||||||
|
width = geom.NaN32()
|
||||||
|
} else if !geom.IsNaN32(width) {
|
||||||
|
width = geom.Max32(width, w)
|
||||||
|
}
|
||||||
|
if geom.IsNaN32(l) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
length += l
|
||||||
|
}
|
||||||
|
switch s.Orientation {
|
||||||
|
case OrientationHorizontal:
|
||||||
|
return desired, geom.PtF32(length, width)
|
||||||
|
default:
|
||||||
|
return desired, geom.PtF32(width, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) DesiredSize(ctx *Context) geom.PointF32 {
|
||||||
|
_, size := s.CalculateLayout(ctx)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) Handle(e allg5.Event) {
|
||||||
|
s.Container.Handle(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) Layout(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
s.Container.Layout(ctx, bounds)
|
||||||
|
|
||||||
|
desired, _ := s.CalculateLayout(ctx)
|
||||||
|
width := s.asWidth(bounds.Size())
|
||||||
|
|
||||||
|
var offset = bounds.Min
|
||||||
|
for i, child := range s.Children {
|
||||||
|
length := s.asLength(desired[i])
|
||||||
|
childSize := s.asSize(length, width)
|
||||||
|
var bottomRight = offset.Add(childSize)
|
||||||
|
var childBounds = geom.RectF32(offset.X, offset.Y, bottomRight.X, bottomRight.Y)
|
||||||
|
child.Layout(ctx, childBounds)
|
||||||
|
offset = offset.Add(s.asSize(length, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StackPanel) Render(ctx *Context, bounds geom.RectangleF32) {
|
||||||
|
for _, child := range s.Children {
|
||||||
|
child.Render(ctx, child.Bounds())
|
||||||
|
}
|
||||||
|
}
|
47
alui/ui.go
Normal file
47
alui/ui.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package alui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"opslag.de/schobers/allg5"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UI struct {
|
||||||
|
ctx *Context
|
||||||
|
main Control
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUI(disp *allg5.Display, main Control) *UI {
|
||||||
|
ctx := &Context{
|
||||||
|
Display: disp,
|
||||||
|
Fonts: NewFonts(),
|
||||||
|
Palette: Palette{},
|
||||||
|
}
|
||||||
|
return &UI{ctx, main}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) Context() *Context { return ui.ctx }
|
||||||
|
|
||||||
|
func (ui *UI) Destroy() {
|
||||||
|
ui.ctx.Fonts.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) Fonts() *Fonts { return ui.ctx.Fonts }
|
||||||
|
|
||||||
|
func (ui *UI) SetPalette(p Palette) { ui.ctx.Palette = p }
|
||||||
|
|
||||||
|
func (ui *UI) layoutBounds(bounds geom.RectangleF32) {
|
||||||
|
ui.main.Layout(ui.ctx, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) Handle(e allg5.Event) { ui.main.Handle(e) }
|
||||||
|
|
||||||
|
func (ui *UI) Render() {
|
||||||
|
ui.RenderBounds(ui.ctx.DisplayBounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) RenderBounds(bounds geom.RectangleF32) {
|
||||||
|
ui.ctx.Cursor = allg5.MouseCursorDefault
|
||||||
|
ui.layoutBounds(bounds)
|
||||||
|
ui.main.Render(ui.ctx, bounds)
|
||||||
|
ui.ctx.Display.SetMouseCursor(ui.ctx.Cursor)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user