From 8d5f5af3c5ac1414ff7e4fe1c7556ed5b6be39f4 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 7 Jul 2022 14:53:14 -0700 Subject: [PATCH] Add widgets --- README.md | 1 - button.go | 56 +++++++++++--------------- examples/flex/game.go | 1 + examples/flex/main.go | 22 +++++++--- examples/showcase/main.go | 83 +++++++++++++++++++++++++++++++------- flex.go | 18 +-------- game.go | 84 ++++++++++++++++++++++++++------------- go.mod | 12 +++--- go.sum | 24 +++++------ input.go | 63 +++++++++++++++++++++++++++++ keybind.go | 15 +++++++ style.go | 46 +++++++++++++++++++-- text.go | 55 +++++++++++++++++++++++++ widget.go | 2 +- window.go | 84 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 446 insertions(+), 120 deletions(-) create mode 100644 input.go create mode 100644 keybind.go create mode 100644 text.go create mode 100644 window.go diff --git a/README.md b/README.md index a19837b..6582f03 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [Ebitengine](https://github.com/hajimehoshi/ebiten) tool kit for creating graphical user interfaces - **Note:** This library is still in development. Breaking changes may be made until v1.0 is released. [IME](https://en.wikipedia.org/wiki/Input_method) is not yet supported. diff --git a/button.go b/button.go index 6306d89..5b1e550 100644 --- a/button.go +++ b/button.go @@ -2,58 +2,36 @@ package etk import ( "image" - "image/color" - "log" "code.rocketnine.space/tslocum/messeji" - "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" - "golang.org/x/image/font" - "golang.org/x/image/font/opentype" - "github.com/hajimehoshi/ebiten/v2" ) -// TODO -var mplusNormalFont font.Face - -func init() { - tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) - if err != nil { - log.Fatal(err) - } - const dpi = 72 - mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ - Size: 32, - DPI: dpi, - Hinting: font.HintingFull, - }) - if err != nil { - log.Fatal(err) - } -} - type Button struct { *Box label *messeji.TextField + + onSelected func() error } -func NewButton(label string, onSelected func()) *Button { +func NewButton(label string, onSelected func() error) *Button { textColor := Style.ButtonTextColor if textColor == nil { - textColor = Style.TextColor + textColor = Style.TextColorDark } - l := messeji.NewTextField(mplusNormalFont) + l := messeji.NewTextField(Style.TextFont) l.SetText(label) l.SetForegroundColor(textColor) - l.SetBackgroundColor(color.RGBA{0, 0, 0, 0}) + l.SetBackgroundColor(transparent) l.SetHorizontal(messeji.AlignCenter) l.SetVertical(messeji.AlignCenter) return &Button{ - Box: NewBox(), - label: l, // TODO + Box: NewBox(), + label: l, + onSelected: onSelected, } } @@ -63,8 +41,20 @@ func (b *Button) SetRect(r image.Rectangle) { b.label.SetRect(r) } -func (b *Button) HandleMouse() (handled bool, err error) { - return false, nil +func (b *Button) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) { + if !clicked { + return true, nil + } + + b.Lock() + onSelected := b.onSelected + if onSelected == nil { + b.Unlock() + return true, nil + } + b.Unlock() + + return true, onSelected() } func (b *Button) HandleKeyboard() (handled bool, err error) { diff --git a/examples/flex/game.go b/examples/flex/game.go index 95f31ef..ad76f79 100644 --- a/examples/flex/game.go +++ b/examples/flex/game.go @@ -30,4 +30,5 @@ func (g *game) Draw(screen *ebiten.Image) { if err != nil { panic(err) } + } diff --git a/examples/flex/main.go b/examples/flex/main.go index 442ee30..9330fa1 100644 --- a/examples/flex/main.go +++ b/examples/flex/main.go @@ -4,24 +4,34 @@ package main import ( + "fmt" + "log" + "code.rocketnine.space/tslocum/etk" "github.com/hajimehoshi/ebiten/v2" ) func main() { - ebiten.SetWindowTitle("etk showcase") + ebiten.SetWindowTitle("etk flex example") + + newButton := func(i int) *etk.Button { + return etk.NewButton(fmt.Sprintf("Button %d", i), func() error { + log.Printf("Pressed button %d", i) + return nil + }) + } g := newGame() - b1 := etk.NewButton("Button 1", nil) - b2 := etk.NewButton("Button 2", nil) + b1 := newButton(1) + b2 := newButton(2) topFlex := etk.NewFlex() topFlex.AddChild(b1, b2) - b3 := etk.NewButton("Button 3", nil) - b4 := etk.NewButton("Button 4", nil) - b5 := etk.NewButton("Button 5", nil) + b3 := newButton(3) + b4 := newButton(4) + b5 := newButton(5) bottomFlex := etk.NewFlex() bottomFlex.AddChild(b3, b4, b5) diff --git a/examples/showcase/main.go b/examples/showcase/main.go index 442ee30..4429cfb 100644 --- a/examples/showcase/main.go +++ b/examples/showcase/main.go @@ -4,33 +4,88 @@ package main import ( + "flag" + "fmt" + "log" + "net/http" + _ "net/http/pprof" + "code.rocketnine.space/tslocum/etk" "github.com/hajimehoshi/ebiten/v2" ) func main() { - ebiten.SetWindowTitle("etk showcase") + var debugAddress string + flag.StringVar(&debugAddress, "debug", "", "serve debug information on address") + flag.Parse() + + if debugAddress != "" { + go func() { + err := http.ListenAndServe(debugAddress, nil) + if err != nil { + log.Fatal(err) + } + }() + } + + ebiten.SetWindowTitle("etk widget showcase") g := newGame() - b1 := etk.NewButton("Button 1", nil) - b2 := etk.NewButton("Button 2", nil) + w := etk.NewWindow() - topFlex := etk.NewFlex() - topFlex.AddChild(b1, b2) + // Input demo. + { + buffer := etk.NewText("Press enter to append input below to this buffer.") - b3 := etk.NewButton("Button 3", nil) - b4 := etk.NewButton("Button 4", nil) - b5 := etk.NewButton("Button 5", nil) + onselected := func(text string) (handled bool) { + buffer.Write([]byte("\nInput: " + text)) + return true + } - bottomFlex := etk.NewFlex() - bottomFlex.AddChild(b3, b4, b5) + input := etk.NewInput(">", "", onselected) - rootFlex := etk.NewFlex() - rootFlex.SetVertical(true) - rootFlex.AddChild(topFlex, bottomFlex) + inputDemo := etk.NewFlex() + inputDemo.SetVertical(true) - etk.SetRoot(rootFlex) + t := etk.NewText("Input") + inputDemo.AddChild(t, buffer, input) + + w.AddChildWithLabel(inputDemo, "Input") + } + + // Flex demo. + { + newButton := func(i int) *etk.Button { + return etk.NewButton(fmt.Sprintf("Button %d", i), func() error { + log.Printf("Pressed button %d", i) + return nil + }) + } + + b1 := newButton(1) + b2 := newButton(2) + + topFlex := etk.NewFlex() + topFlex.AddChild(b1, b2) + + b3 := newButton(3) + b4 := newButton(4) + b5 := newButton(5) + + bottomFlex := etk.NewFlex() + bottomFlex.AddChild(b3, b4, b5) + + flexDemo := etk.NewFlex() + flexDemo.SetVertical(true) + + t := etk.NewText("Flex") + flexDemo.AddChild(t, topFlex, bottomFlex) + + w.AddChildWithLabel(flexDemo, "Flex") + } + + etk.SetRoot(w) err := ebiten.RunGame(g) if err != nil { diff --git a/flex.go b/flex.go index ac2826f..0841393 100644 --- a/flex.go +++ b/flex.go @@ -2,7 +2,6 @@ package etk import ( "image" - "log" "github.com/hajimehoshi/ebiten/v2" ) @@ -11,8 +10,6 @@ type Flex struct { *Box vertical bool - - lastRect image.Rectangle } func NewFlex() *Flex { @@ -26,11 +23,7 @@ func (f *Flex) SetRect(r image.Rectangle) { defer f.Unlock() f.Box.rect = r - - // TODO - for _, child := range f.children { - child.SetRect(r) - } + f.reposition() } func (f *Flex) SetVertical(v bool) { @@ -45,7 +38,7 @@ func (f *Flex) SetVertical(v bool) { f.reposition() } -func (f *Flex) HandleMouse() (handled bool, err error) { +func (f *Flex) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) { return false, nil } @@ -57,11 +50,6 @@ func (f *Flex) Draw(screen *ebiten.Image) error { f.Lock() defer f.Unlock() - if !f.rect.Eq(f.lastRect) { - f.reposition() - f.lastRect = f.rect - } - for _, child := range f.children { err := child.Draw(screen) if err != nil { @@ -85,7 +73,6 @@ func (f *Flex) reposition() { if i == l-1 { maxY = r.Max.Y } - log.Println(i, maxY, image.Rect(r.Min.X, minY, r.Max.X, maxY)) child.SetRect(image.Rect(r.Min.X, minY, r.Max.X, maxY)) minY = maxY @@ -102,7 +89,6 @@ func (f *Flex) reposition() { if i == l-1 { maxX = r.Max.X } - log.Println(i, maxX, image.Rect(minX, r.Min.Y, maxX, r.Max.Y)) child.SetRect(image.Rect(minX, r.Min.Y, maxX, r.Max.Y)) minX = maxX diff --git a/game.go b/game.go index 8a2aedb..aaeddd5 100644 --- a/game.go +++ b/game.go @@ -3,6 +3,9 @@ package etk import ( "fmt" "image" + "math" + + "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2" ) @@ -11,6 +14,8 @@ var root Widget var ( lastWidth, lastHeight int + + lastX, lastY = -math.MaxInt, -math.MaxInt ) func SetRoot(w Widget) { @@ -33,41 +38,59 @@ func Update() error { panic("no root widget specified") } - var mouseHandled bool - var keyboardHandled bool - var err error + x, y := ebiten.CursorPosition() + cursor := image.Point{x, y} - children := root.Children() - for _, child := range children { - if !mouseHandled { - mouseHandled, err = child.HandleMouse() - if err != nil { - return fmt.Errorf("failed to handle widget mouse input: %s", err) - } - } - if !keyboardHandled { - keyboardHandled, err = child.HandleKeyboard() - if err != nil { - return fmt.Errorf("failed to handle widget keyboard input: %s", err) - } - } - if mouseHandled && keyboardHandled { - return nil + if lastX == -math.MaxInt && lastY == -math.MaxInt { + lastX, lastY = x, y + } + + // TODO handle touch input + + var pressed bool + for _, binding := range Bindings.ConfirmMouse { + pressed = ebiten.IsMouseButtonPressed(binding) + if pressed { + break } } - if !mouseHandled { - _, err = root.HandleMouse() + + var clicked bool + for _, binding := range Bindings.ConfirmMouse { + clicked = inpututil.IsMouseButtonJustReleased(binding) + if clicked { + break + } + } + + _, _, err := update(root, cursor, pressed, clicked, false, false) + return err +} + +func update(w Widget, cursor image.Point, pressed bool, clicked bool, mouseHandled bool, keyboardHandled bool) (bool, bool, error) { + var err error + children := w.Children() + for _, child := range children { + mouseHandled, keyboardHandled, err = update(child, cursor, pressed, clicked, mouseHandled, keyboardHandled) if err != nil { - return fmt.Errorf("failed to handle widget mouse input: %s", err) + return false, false, err + } else if mouseHandled && keyboardHandled { + return true, true, nil + } + } + if !mouseHandled && cursor.In(w.Rect()) { + _, err = w.HandleMouse(cursor, pressed, clicked) + if err != nil { + return false, false, fmt.Errorf("failed to handle widget mouse input: %s", err) } } if !keyboardHandled { - _, err = root.HandleKeyboard() + _, err = w.HandleKeyboard() if err != nil { - return fmt.Errorf("failed to handle widget keyboard input: %s", err) + return false, false, fmt.Errorf("failed to handle widget keyboard input: %s", err) } } - return nil + return mouseHandled, keyboardHandled, nil } func Draw(screen *ebiten.Image) error { @@ -75,17 +98,22 @@ func Draw(screen *ebiten.Image) error { panic("no root widget specified") } - err := root.Draw(screen) + return draw(root, screen) +} + +func draw(w Widget, screen *ebiten.Image) error { + err := w.Draw(screen) if err != nil { return fmt.Errorf("failed to draw widget: %s", err) } - children := root.Children() + children := w.Children() for _, child := range children { - err = child.Draw(screen) + err = draw(child, screen) if err != nil { return fmt.Errorf("failed to draw widget: %s", err) } } + return nil } diff --git a/go.mod b/go.mod index 1814411..1ffb5b9 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,18 @@ module code.rocketnine.space/tslocum/etk go 1.18 require ( - code.rocketnine.space/tslocum/messeji v1.0.0 - github.com/hajimehoshi/ebiten/v2 v2.3.3 - golang.org/x/image v0.0.0-20220601225756-64ec528b34cd + code.rocketnine.space/tslocum/messeji v1.0.2 + github.com/hajimehoshi/ebiten/v2 v2.3.5 + golang.org/x/image v0.0.0-20220617043117-41969df76e82 ) require ( - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220622232848-a6c407ee30a0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/jezek/xgb v1.0.1 // indirect - golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498 // indirect + golang.org/x/exp/shiny v0.0.0-20220706164943-b4a6d9510983 // indirect golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect - golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect + golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index b4997e4..47e2c5b 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,15 @@ -code.rocketnine.space/tslocum/messeji v1.0.0 h1:GRZ8/ExI/syR3+0UH3cMjnJFhJnGxQOMSMoCf/v7XLM= -code.rocketnine.space/tslocum/messeji v1.0.0/go.mod h1:o3MnboWYp/W/ZsYCzga4t/pyzLfXnf6iK8R3KBJuHIM= +code.rocketnine.space/tslocum/messeji v1.0.2 h1:3/68FnXWaBDMhfUGb8FvNpVgAHY8DX+VL7pyA/CcY94= +code.rocketnine.space/tslocum/messeji v1.0.2/go.mod h1:bSXsyjvKhFXQ7GsUxWZdO2JX83xOT/VTqFCR04thk+c= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661 h1:1bpooddSK2996NWM/1TW59cchQOm9MkoV9DkhSJH1BI= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220516021902-eb3e265c7661/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220622232848-a6c407ee30a0 h1:ZWsNtyC3mgUL48DikCfjkyiaRYZ3OL2XBfn7JJs2/ZE= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220622232848-a6c407ee30a0/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/hajimehoshi/bitmapfont/v2 v2.2.0 h1:E6vzlchynZj6OVohVKFqWkKW348EmDW62K5zPXDi7A8= github.com/hajimehoshi/bitmapfont/v2 v2.2.0/go.mod h1:Llj2wTYXMuCTJEw2ATNIO6HbFPOoBYPs08qLdFAxOsQ= -github.com/hajimehoshi/ebiten/v2 v2.3.3 h1:v72UzprVvWGE+HGcypkLI9Ikd237fqzpio5idPk9KNI= -github.com/hajimehoshi/ebiten/v2 v2.3.3/go.mod h1:vxwpo0q0oSi1cIll0Q3Ui33TVZgeHuFVYzIRk7FwuVk= +github.com/hajimehoshi/ebiten/v2 v2.3.5 h1:GG2XMNu9Yf/CCopxhdIRS1IREvx3gWCZ9RMP3rKkZcc= +github.com/hajimehoshi/ebiten/v2 v2.3.5/go.mod h1:vxwpo0q0oSi1cIll0Q3Ui33TVZgeHuFVYzIRk7FwuVk= github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= @@ -34,14 +34,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498 h1:mJjyic/dxHcz1W6IUE8zf6+RltuO8+9mS45tTtb4F6k= -golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= +golang.org/x/exp/shiny v0.0.0-20220706164943-b4a6d9510983 h1:z34Buq9ijQFAoTegl58EYWYLBAzEDT0BTzglEJ+AmEo= +golang.org/x/exp/shiny v0.0.0-20220706164943-b4a6d9510983/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220601225756-64ec528b34cd h1:9NbNcTg//wfC5JskFW4Z3sqwVnjmJKHxLAol1bW2qgw= -golang.org/x/image v0.0.0-20220601225756-64ec528b34cd/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= +golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd h1:x1GptNaTtxPAlTVIAJk61fuXg0y17h09DTxyb+VNC/k= @@ -70,8 +70,8 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/input.go b/input.go new file mode 100644 index 0000000..6c8add2 --- /dev/null +++ b/input.go @@ -0,0 +1,63 @@ +package etk + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" + + "code.rocketnine.space/tslocum/messeji" +) + +type Input struct { + *Box + field *messeji.InputField +} + +func NewInput(prefix string, text string, onSelected func(text string) (handled bool)) *Input { + textColor := Style.TextColorDark + /*if TextColor == nil { + textColor = Style.InputColor + }*/ + + i := messeji.NewInputField(Style.TextFont) + i.SetPrefix(prefix) + i.SetText(text) + i.SetForegroundColor(textColor) + i.SetBackgroundColor(Style.InputBgColor) + i.SetHandleKeyboard(true) + i.SetSelectedFunc(func() (accept bool) { + return onSelected(i.Text()) + }) + + return &Input{ + Box: NewBox(), + field: i, + } +} + +// Write writes to the field's buffer. +func (i *Input) Write(p []byte) (n int, err error) { + return i.field.Write(p) +} + +func (i *Input) SetRect(r image.Rectangle) { + i.Box.rect = r + + i.field.SetRect(r) +} + +func (i *Input) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) { + return false, nil +} + +func (i *Input) HandleKeyboard() (handled bool, err error) { + err = i.field.Update() + + return false, err +} + +func (i *Input) Draw(screen *ebiten.Image) error { + // Draw label. + i.field.Draw(screen) + return nil +} diff --git a/keybind.go b/keybind.go new file mode 100644 index 0000000..ea69a52 --- /dev/null +++ b/keybind.go @@ -0,0 +1,15 @@ +package etk + +import "github.com/hajimehoshi/ebiten/v2" + +type Shortcuts struct { + ConfirmKeyboard []ebiten.Key + ConfirmMouse []ebiten.MouseButton + ConfirmGamepad []ebiten.GamepadButton +} + +var Bindings = &Shortcuts{ + ConfirmKeyboard: []ebiten.Key{ebiten.KeyEnter, ebiten.KeyKPEnter}, + ConfirmMouse: []ebiten.MouseButton{ebiten.MouseButtonLeft}, + ConfirmGamepad: []ebiten.GamepadButton{ebiten.GamepadButton0}, +} diff --git a/style.go b/style.go index 4205f43..f311ff6 100644 --- a/style.go +++ b/style.go @@ -1,22 +1,62 @@ package etk -import "image/color" +import ( + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" +) + +var transparent = color.RGBA{0, 0, 0, 0} + +func defaultFont() font.Face { + tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) + if err != nil { + log.Fatal(err) + } + const dpi = 72 + defaultFont, err := opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 32, + DPI: dpi, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatal(err) + } + return defaultFont +} type Attributes struct { - TextColor color.Color + TextFont font.Face + + TextColorLight color.Color + TextColorDark color.Color + + TextBgColor color.Color BorderColor color.Color + InputBgColor color.Color + ButtonTextColor color.Color ButtonBgColor color.Color ButtonBgColorDisabled color.Color } var Style = &Attributes{ - TextColor: color.RGBA{0, 0, 0, 255}, + TextFont: defaultFont(), + + TextColorLight: color.RGBA{255, 255, 255, 255}, + TextColorDark: color.RGBA{0, 0, 0, 255}, + + TextBgColor: transparent, BorderColor: color.RGBA{0, 0, 0, 255}, + InputBgColor: color.RGBA{0, 128, 0, 255}, + ButtonBgColor: color.RGBA{255, 255, 255, 255}, ButtonBgColorDisabled: color.RGBA{110, 110, 110, 255}, } diff --git a/text.go b/text.go new file mode 100644 index 0000000..2dffe27 --- /dev/null +++ b/text.go @@ -0,0 +1,55 @@ +package etk + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" + + "code.rocketnine.space/tslocum/messeji" +) + +type Text struct { + *Box + field *messeji.TextField +} + +func NewText(text string) *Text { + textColor := Style.TextColorLight + + l := messeji.NewTextField(Style.TextFont) + l.SetText(text) + l.SetForegroundColor(textColor) + l.SetBackgroundColor(Style.TextBgColor) + l.SetHorizontal(messeji.AlignCenter) + l.SetVertical(messeji.AlignCenter) + + return &Text{ + Box: NewBox(), + field: l, + } +} + +// Write writes to the field's buffer. +func (t *Text) Write(p []byte) (n int, err error) { + return t.field.Write(p) +} + +func (t *Text) SetRect(r image.Rectangle) { + t.Box.rect = r + + t.field.SetRect(r) +} + +func (t *Text) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) { + return false, nil +} + +func (t *Text) HandleKeyboard() (handled bool, err error) { + return false, nil +} + +func (t *Text) Draw(screen *ebiten.Image) error { + // Draw label. + t.field.Draw(screen) + return nil +} diff --git a/widget.go b/widget.go index 81afe1a..8117285 100644 --- a/widget.go +++ b/widget.go @@ -9,7 +9,7 @@ import ( type Widget interface { Rect() image.Rectangle SetRect(r image.Rectangle) - HandleMouse() (handled bool, err error) + HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) HandleKeyboard() (handled bool, err error) Draw(screen *ebiten.Image) error Children() []Widget diff --git a/window.go b/window.go new file mode 100644 index 0000000..8daab27 --- /dev/null +++ b/window.go @@ -0,0 +1,84 @@ +package etk + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" +) + +// Window displays and passes input to only one child widget at a time. +type Window struct { + *Box + + allChildren []Widget + + active int + labels []string + hasLabel bool +} + +func NewWindow() *Window { + return &Window{ + Box: NewBox(), + } +} + +func (w *Window) childrenUpdated() { + if len(w.allChildren) == 0 { + w.children = nil + return + } + w.children = []Widget{w.allChildren[w.active]} +} + +func (w *Window) SetRect(r image.Rectangle) { + w.Lock() + defer w.Unlock() + + w.rect = r + for _, wgt := range w.children { + wgt.SetRect(r) + } +} + +func (w *Window) AddChild(wgt ...Widget) { + w.allChildren = append(w.allChildren, wgt...) + + for _, widget := range wgt { + widget.SetRect(w.rect) + } + + blankLabels := make([]string, len(wgt)) + w.labels = append(w.labels, blankLabels...) + + w.childrenUpdated() +} + +func (w *Window) AddChildWithLabel(wgt Widget, label string) { + w.Lock() + defer w.Unlock() + + wgt.SetRect(w.rect) + + w.allChildren = append(w.allChildren, wgt) + w.labels = append(w.labels, label) + + if label != "" { + w.hasLabel = true + } + + w.childrenUpdated() +} + +func (w *Window) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) { + return true, nil +} + +func (w *Window) HandleKeyboard() (handled bool, err error) { + return true, nil +} + +func (w *Window) Draw(screen *ebiten.Image) error { + // TODO draw labels + return nil +}