aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go/ui/buffer.go10
-rw-r--r--go/ui/button.go53
-rw-r--r--go/ui/cardGrid.go1
-rw-r--r--go/ui/choice.go44
-rw-r--r--go/ui/clickable.go40
-rw-r--r--go/ui/collection.go153
-rw-r--r--go/ui/eventHandlers.go76
-rw-r--r--go/ui/mapView.go36
-rw-r--r--go/ui/prompt.go14
-rw-r--r--go/ui/scrollable.go5
-rw-r--r--go/ui/stateBar.go19
-rw-r--r--go/ui/textBox.go91
-rw-r--r--go/ui/textInput.go67
-rw-r--r--go/ui/textInput_generic.go16
-rw-r--r--go/ui/textInput_wasm.go49
-rw-r--r--go/ui/widget.go25
16 files changed, 509 insertions, 190 deletions
diff --git a/go/ui/buffer.go b/go/ui/buffer.go
index e46461bf..3abbc2cc 100644
--- a/go/ui/buffer.go
+++ b/go/ui/buffer.go
@@ -16,9 +16,9 @@ var (
type Buffer struct {
WidgetBase
- lines []string
- pos int
- font *text.GoTextFace
+ lines []string
+ pos int
+ font *text.GoTextFace
lineSpacing float64
xMargin float64
}
@@ -34,7 +34,7 @@ func (b *Buffer) render() *ebiten.Image {
img.Fill(BUFFER_BACKGROUND)
var y float64 = -b.font.Size
for _, line := range b.lines[b.pos:] {
- _, h := text.Measure(line, b.font, b.font.Size * b.lineSpacing)
+ _, h := text.Measure(line, b.font, b.font.Size*b.lineSpacing)
y += h
if y > float64(b.Height) {
break
@@ -72,6 +72,8 @@ func (b *Buffer) Remove(idx int) {
b.ForceRedraw()
}
+func (*Buffer) IsScrollable() bool { return true }
+
func (b *Buffer) Scroll(x, y int) {
if y == 0 {
return
diff --git a/go/ui/button.go b/go/ui/button.go
index a755c5c0..b6cec672 100644
--- a/go/ui/button.go
+++ b/go/ui/button.go
@@ -10,45 +10,68 @@ import (
"muhq.space/muhqs-game/go/font"
)
-
type Button interface {
Widget
- Clickable
}
type SimpleButton struct {
WidgetBase
- label string
- OnClick func(b *SimpleButton)
- Bg color.Color
- Fg color.Color
- font *text.GoTextFace
+ label string
+ Bg color.Color
+ Fg color.Color
+ font *text.GoTextFace
lineSpacing float64
}
func NewSimpleButton(x, y, w, h int, label string, onClick func(b *SimpleButton)) *SimpleButton {
- sb := &SimpleButton{NewWidgetBase(x, y, w, h), label, onClick, Gray, color.White, font.Font24, 1.5}
- sb.renderImpl = func() *ebiten.Image { return sb.render() }
+ sb := &SimpleButton{NewWidgetBase(x, y, w, h), label, Gray, color.White, font.Font24, 1.5}
+ sb.EventHandlersMap["click"] = func(int, int) { onClick(sb) }
+ sb.renderImpl = func() *ebiten.Image {
+ if sb.img == nil {
+ sb.img = sb.renderRect()
+ }
+ return sb.img
+ }
return sb
}
-func (b *SimpleButton) render() *ebiten.Image {
- img := ebiten.NewImage(b.Width, b.Height)
+func NewRoundSimpleButton(x, y, r int, label string, onClick func(b *SimpleButton)) *SimpleButton {
+ sb := &SimpleButton{NewWidgetBase(x, y, r, r), label, Gray, color.White, font.Font24, 1.5}
+ sb.EventHandlersMap["click"] = func(int, int) { onClick(sb) }
+ sb.renderImpl = func() *ebiten.Image {
+ if sb.img == nil {
+ sb.img = sb.renderCircle()
+ }
+ return sb.img
+ }
+ return sb
+}
- vector.DrawFilledRect(img, float32(0), float32(0), float32(b.Width), float32(b.Height), b.Bg, false)
+func (b *SimpleButton) drawLabel(img *ebiten.Image) {
- w, h := text.Measure(b.label, b.font, b.font.Size * b.lineSpacing)
+ w, h := text.Measure(b.label, b.font, b.font.Size*b.lineSpacing)
x := float64(b.Width)/2 - w/2
y := float64(b.Height)/2 - h/2
op := &text.DrawOptions{}
op.GeoM.Translate(x, y)
op.ColorScale.ScaleWithColor(b.Fg)
text.Draw(img, b.label, font.Font24, op)
+}
+
+func (b *SimpleButton) renderRect() *ebiten.Image {
+ img := ebiten.NewImage(b.Width, b.Height)
+
+ vector.DrawFilledRect(img, float32(0), float32(0), float32(b.Width), float32(b.Height), b.Bg, false)
+ b.drawLabel(img)
return img
}
-func (b *SimpleButton) Click(x, y int) {
- b.OnClick(b)
+func (b *SimpleButton) renderCircle() *ebiten.Image {
+ img := ebiten.NewImage(b.Width, b.Height)
+
+ vector.DrawFilledCircle(img, float32(b.Width)/2, float32(b.Height)/2, float32(b.Width/2), b.Bg, false)
+ b.drawLabel(img)
+ return img
}
func (b *SimpleButton) UpdateLabel(label string) {
diff --git a/go/ui/cardGrid.go b/go/ui/cardGrid.go
index 36a215d5..ec962350 100644
--- a/go/ui/cardGrid.go
+++ b/go/ui/cardGrid.go
@@ -180,6 +180,7 @@ func (w *CardGrid) ClearHighlights() {
w.setHighlights([]*game.Card{}, nil)
}
+func (*CardGrid) IsScrollable() bool { return true }
func (w *CardGrid) Scroll(x, y int) {
w.xOffset += x * 10
if w.xOffset > 0 {
diff --git a/go/ui/choice.go b/go/ui/choice.go
index 9b229cb6..a9027086 100644
--- a/go/ui/choice.go
+++ b/go/ui/choice.go
@@ -43,6 +43,7 @@ func (c *Choice) GetChoosen(x, y int) int {
return i
}
+func (*Choice) IsClickable() bool { return true }
func (c *Choice) Click(x, y int) {
c.onClick(c, x, y)
}
@@ -64,10 +65,9 @@ const (
)
type NumberChoice struct {
- Clickable
WidgetBase
- numberInput *NumberInput
- buttons [3]Button
+ numberInput *NumberInput
+ buttons [3]Button
AllowNegativ bool
}
@@ -87,31 +87,39 @@ func NewNumberChoice(x, y int, startValue int, onSubmit func(*NumberChoice)) *Nu
x+NUMBER_CHOICE_BUTTON_WIDTH+NUMBER_CHOICE_INPUT_WIDTH, y,
NUMBER_CHOICE_BUTTON_WIDTH, NUMBER_CHOICE_HEIGHT,
"-1",
- func(*SimpleButton) { if ni.Number() > 0 || nc.AllowNegativ { ni.Add(-1) }}),
- NewSimpleButton(
- x+2*NUMBER_CHOICE_BUTTON_WIDTH+NUMBER_CHOICE_INPUT_WIDTH, y,
- NUMBER_CHOICE_SUBMIT_WIDTH, NUMBER_CHOICE_HEIGHT,
- "submit",
- func(*SimpleButton) { onSubmit(nc) }),
+ func(*SimpleButton) {
+ if ni.Number() > 0 || nc.AllowNegativ {
+ ni.Add(-1)
+ }
+ }),
},
AllowNegativ: false,
}
- return nc
+ if onSubmit != nil {
+ nc.buttons[2] = NewSimpleButton(
+ x+2*NUMBER_CHOICE_BUTTON_WIDTH+NUMBER_CHOICE_INPUT_WIDTH, y,
+ NUMBER_CHOICE_SUBMIT_WIDTH, NUMBER_CHOICE_HEIGHT,
+ "submit",
+ func(*SimpleButton) { onSubmit(nc) })
}
-
-func (nc *NumberChoice) Click(x, y int) {
- for _, b := range nc.buttons {
- if b.Contains(x, y) {
- b.Click(x, y)
- return
+ nc.EventHandlersMap["click"] = func (x, y int) {
+ for _, b := range nc.buttons {
+ if b.Contains(x, y) {
+ b.Click(x, y)
+ return
+ }
}
}
+
+ return nc
}
func (nc *NumberChoice) Draw(screen *ebiten.Image) {
- for _, b := range nc.buttons {
- b.Draw(screen)
+ nc.buttons[0].Draw(screen)
+ nc.buttons[1].Draw(screen)
+ if nc.buttons[2] != nil {
+ nc.buttons[2].Draw(screen)
}
nc.numberInput.Draw(screen)
}
diff --git a/go/ui/clickable.go b/go/ui/clickable.go
deleted file mode 100644
index 761b7534..00000000
--- a/go/ui/clickable.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ui
-
-import (
- "github.com/hajimehoshi/ebiten/v2"
-)
-
-type Clickable interface {
- Click(x, y int)
-}
-
-type ClickWrapper struct {
- w Widget
- onClick func(x, y int)
-}
-
-func NewClickWrapper(w Widget, onClick func(x, y int)) *ClickWrapper {
- return &ClickWrapper{w, onClick}
-}
-
-func (w *ClickWrapper) Draw(screen *ebiten.Image) {
- w.w.Draw(screen)
-}
-
-func (w *ClickWrapper) FindObjectAt(x, y int) interface{} {
- return w.w.FindObjectAt(x, y)
-}
-
-func (w *ClickWrapper) Contains(x, y int) bool {
- return w.w.Contains(x, y)
-}
-
-func (w *ClickWrapper) Click(x, y int) {
- w.onClick(x, y)
-}
-
-func (w *ClickWrapper) Scroll(x, y int) {
- if w, ok := w.w.(Scrollable); ok {
- w.Scroll(x, y)
- }
-}
diff --git a/go/ui/collection.go b/go/ui/collection.go
index 58cc4b85..bb7a4d53 100644
--- a/go/ui/collection.go
+++ b/go/ui/collection.go
@@ -8,10 +8,14 @@ import (
"golang.org/x/exp/slices"
)
+// Container to organise multiple Widgets
+// A collection does implement the Widget interface and thus can be nested in
+// another Collection.
type Collection struct {
Width, Height int
hoverDetector hoverDetector
widgets []Widget
+ focused Widget
}
func (c *Collection) Layout() (int, int) {
@@ -69,10 +73,17 @@ func (c *Collection) Draw(screen *ebiten.Image) {
}
}
-func (c *Collection) Update() error {
- x, y := ebiten.CursorPosition()
+func (c *Collection) Contains(x, y int) bool {
+ for _, w := range c.widgets {
+ if w.Contains(x, y) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *Collection) FindWidgetAt(x, y int) Widget {
- var activeWidget Widget
// Iterate the widget in reverse order to ensure that upper widgets are
// considered first
for i := len(c.widgets) - 1; i >= 0; i-- {
@@ -80,37 +91,149 @@ func (c *Collection) Update() error {
if !w.Contains(x, y) {
continue
}
- activeWidget = w
- break
+ return w
+ }
+ return nil
+}
+
+func (c *Collection) FindObjectAt(x, y int) interface{} {
+
+ // Iterate the widget in reverse order to ensure that upper widgets are
+ // considered first
+ for i := len(c.widgets) - 1; i >= 0; i-- {
+ w := c.widgets[i]
+ if o := w.FindObjectAt(x, y); o != nil {
+ return o
+ }
+ }
+
+ return nil
+}
+
+func (c *Collection) switchFocus(fcsbl Widget) {
+ log.Printf("switch focus from %v to %v\n", c.focused, fcsbl)
+ if c.focused != nil {
+ c.focused.Focus(false)
+
+ }
+ if fcsbl != nil {
+ fcsbl.Focus(true)
+ c.focused = fcsbl
+ } else {
+ c.focused = nil
+ }
+}
+
+func (c *Collection) handleTouchInputs() error {
+ for _, tap := range TouchManager.Taps {
+ for i := len(c.widgets) - 1; i >= 0; i-- {
+ w := c.widgets[i]
+ if !w.Contains(tap.X, tap.Y) {
+ continue
+ }
+ if w.IsClickable() {
+ w.Click(tap.X, tap.Y)
+ }
+ if w.IsFocusable() {
+ c.switchFocus(w)
+ } else if c.focused != nil {
+ c.switchFocus(nil)
+ }
+ break
+ }
+ }
+
+ // TODO: handle pinching
+ if TouchManager.Pan != nil {
+ pan := TouchManager.Pan
+ iX, iY := pan.Incremental()
+
+ w := c.FindWidgetAt(int(pan.originX), (pan.originY))
+ if w != nil && w.IsScrollable() {
+ w.Scroll(int(iX)/10, int(iY)/10)
+ }
}
+ return nil
+}
+
+func passInput(textInput *TextInput) {
+ var input []rune
+ input = ebiten.AppendInputChars(input)
+ if len(input) > 0 {
+ textInput.AddInput(input)
+ }
+ textInput.HandleKey()
+}
+
+func (c *Collection) IsUpdatable() bool {
+ return true
+}
+
+func (c *Collection) Update() error {
+ // Update all updatables
+ for _, w := range c.widgets {
+ if w.IsUpdatable() {
+ if err := w.Update(); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := c.handleTouchInputs(); err != nil {
+ return err
+ }
+
+ x, y := ebiten.CursorPosition()
+
+ activeWidget := c.FindWidgetAt(x, y)
+
c.hoverDetector.update(c, x, y, activeWidget)
if activeWidget == nil {
+ if txt, ok := c.focused.(*TextInput); ok {
+ if ebiten.IsKeyPressed(ebiten.KeyA) {
+ txt.AddInput([]rune{'a'})
+ }
+ passInput(txt)
+ }
return nil
}
scrollX, scrollY := ebiten.Wheel()
if scrollX != 0 || scrollY != 0 {
- if scrollable, ok := activeWidget.(Scrollable); ok {
- scrollable.Scroll(int(scrollX), int(scrollY))
+ if activeWidget.IsScrollable() {
+ activeWidget.Scroll(int(scrollX), int(scrollY))
}
}
if textInput, ok := activeWidget.(*TextInput); ok {
- var input []rune
- input = ebiten.AppendInputChars(input)
- if len(input) > 0 {
- textInput.AddInput(input)
- }
- textInput.HandleKey()
+ passInput(textInput)
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- if btn, ok := activeWidget.(Clickable); ok {
- btn.Click(x, y)
+ if activeWidget.IsClickable() {
+ activeWidget.Click(x, y)
}
}
return nil
}
+
+// The events for the contained widgets
+// are emitted during the collections
+// Update method.
+func (c *Collection) IsClickable() bool {
+ return false
+}
+func (c *Collection) Click(int, int) {}
+
+func (c *Collection) IsScrollable() bool {
+ return false
+}
+func (c *Collection) Scroll(int, int) {}
+
+func (c *Collection) IsFocusable() bool {
+ return false
+}
+func (c *Collection) Focus(bool) {}
diff --git a/go/ui/eventHandlers.go b/go/ui/eventHandlers.go
new file mode 100644
index 00000000..b81606f8
--- /dev/null
+++ b/go/ui/eventHandlers.go
@@ -0,0 +1,76 @@
+package ui
+
+type ClickHandler = func(int, int)
+type ScrollHandler = func(int, int)
+type UpdateHandler = func() error
+type FocusHandler = func(bool)
+
+type EventHandlers interface {
+ IsClickable() bool
+ Click(int, int)
+ // IsHoverable() bool
+ // Hover(int, int)
+ IsScrollable() bool
+ Scroll(int, int)
+ IsUpdatable() bool
+ Update() error
+ IsFocusable() bool
+ Focus(bool)
+}
+
+// TODO: cache events (in a bitmask)
+type EventHandlersMap map[string]any
+
+func NewEventHandlersMap() EventHandlersMap {
+ return make(EventHandlersMap)
+}
+
+func (m EventHandlersMap) RegisterHandler(ev string, handler any) {
+ // TODO: sanitize input
+ m[ev] = handler
+}
+
+func (m EventHandlersMap) IsClickable() bool {
+ _, found := m["click"]
+ return found
+}
+
+func (m EventHandlersMap) Click(x, y int) {
+ onclick := m["click"]
+ onclick.(ClickHandler)(x, y)
+}
+
+func (m EventHandlersMap) IsScrollable() bool {
+ _, found := m["update"]
+ return found
+}
+
+func (m EventHandlersMap) Scroll(x, y int) {
+ onscroll := m["scroll"]
+ onscroll.(ScrollHandler)(x, y)
+}
+
+func (m EventHandlersMap) IsUpdatable() bool {
+ _, found := m["update"]
+ return found
+}
+
+func (m EventHandlersMap) Update() error {
+ onupdate := m["update"]
+ return onupdate.(UpdateHandler)()
+}
+
+func (m EventHandlersMap) IsHoverable() bool {
+ _, found := m["hover"]
+ return found
+}
+
+func (m EventHandlersMap) IsFocusable() bool {
+ _, found := m["focus"]
+ return found
+}
+
+func (m EventHandlersMap) Focus(focus bool) {
+ onfocus := m["focus"]
+ onfocus.(FocusHandler)(focus)
+}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index 8bd2e341..64501706 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -39,9 +39,11 @@ const (
type MapView struct {
hoverPermInfo
+ EventHandlersMap
gameState game.State
mapLayer *ebiten.Image
permanentsLayer *ebiten.Image
+ scale float64
tileHighlights map[game.Position][]color.Color
permanentsHighlights map[game.Permanent][]color.Color
}
@@ -49,6 +51,7 @@ type MapView struct {
func NewMapView(g game.State) *MapView {
vw := &MapView{
gameState: g,
+ scale: 1,
tileHighlights: make(map[game.Position][]color.Color),
permanentsHighlights: make(map[game.Permanent][]color.Color),
}
@@ -57,7 +60,7 @@ func NewMapView(g game.State) *MapView {
}
func (mv *MapView) Height() int {
- return len(mv.gameState.Map().Tiles) * TILE_HEIGHT
+ return int(float64(len(mv.gameState.Map().Tiles)*TILE_HEIGHT) * mv.scale)
}
func (mv *MapView) Width() int {
@@ -67,7 +70,7 @@ func (mv *MapView) Width() int {
maxWidth = n
}
}
- return maxWidth * TILE_WIDTH
+ return int(float64(maxWidth*TILE_WIDTH) * mv.scale)
}
func rotateTileImg(x, y int, radian float64, op *ebiten.DrawImageOptions) {
@@ -238,6 +241,7 @@ func (vw *MapView) drawMapLayer(screen *ebiten.Image) {
tile := vw.gameState.Map().TileAt(pos)
var tileImg *ebiten.Image
op := &ebiten.DrawImageOptions{}
+ op.GeoM.Scale(vw.scale, vw.scale)
switch tile.Type {
case game.TileTypes.Street:
@@ -300,6 +304,7 @@ func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) {
y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2
op := &ebiten.DrawImageOptions{}
+ op.GeoM.Scale(vw.scale, vw.scale)
op.GeoM.Translate(float64(x_px), float64(y_px))
op.ColorScale.ScaleWithColor(p.Controller().Color)
@@ -376,20 +381,22 @@ func isSymbolTransparentAt(p game.Permanent, relativeX, relativeY int) bool {
}
func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} {
- x := screenX / TILE_WIDTH
- y := screenY / TILE_HEIGHT
+ scaled_tile_wdth := int(float64(TILE_WIDTH) * vw.scale)
+ scaled_tile_hght := int(float64(TILE_HEIGHT) * vw.scale)
+ x := screenX / scaled_tile_wdth
+ y := screenY / scaled_tile_hght
if x < 0 || y < 0 {
return nil
}
- relativeX := screenX % TILE_WIDTH
- relativeY := screenY % TILE_HEIGHT
- xMargin := (TILE_WIDTH - PERMANENT_WIDTH) / 2
- yMargin := (TILE_HEIGHT - PERMANENT_HEIGHT) / 2
+ relativeX := screenX % scaled_tile_wdth
+ relativeY := screenY % scaled_tile_hght
+ xMargin := int(float64(TILE_WIDTH-PERMANENT_WIDTH)*vw.scale) / 2
+ yMargin := int(float64(TILE_HEIGHT-PERMANENT_HEIGHT)*vw.scale) / 2
// detect if a permanent or the containing tile was selected
- if relativeX >= xMargin && relativeX <= TILE_WIDTH-xMargin &&
- relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin {
+ if relativeX >= xMargin && relativeX <= scaled_tile_wdth-xMargin &&
+ relativeY >= yMargin && relativeY <= scaled_tile_hght-yMargin {
for _, p := range vw.gameState.Permanents() {
t := p.Tile()
@@ -487,3 +494,12 @@ func (mv *MapView) Contains(x, y int) bool {
func (*MapView) Render(Widget) *ebiten.Image {
return nil
}
+
+func (mv *MapView) Layout() (int, int) {
+ return mv.Width(), mv.Height()
+}
+
+func (mv *MapView) Scale(s float64) *MapView {
+ mv.scale = s
+ return mv
+}
diff --git a/go/ui/prompt.go b/go/ui/prompt.go
index c0b11d94..a7f2ce07 100644
--- a/go/ui/prompt.go
+++ b/go/ui/prompt.go
@@ -13,6 +13,7 @@ const (
)
type Prompt struct {
+ EventHandlersMap
y, width int
action game.Action
components []Widget
@@ -20,10 +21,11 @@ type Prompt struct {
func NewPrompt(y, width int, action game.Action, promptText string) *Prompt {
p := &Prompt{
- y: y,
- width: width,
- action: action,
- components: []Widget{NewFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText).Centering(true)},
+ EventHandlersMap: NewEventHandlersMap(),
+ y: y,
+ width: width,
+ action: action,
+ components: []Widget{NewFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText).Centering(true)},
}
return p
}
@@ -91,3 +93,7 @@ func (p *Prompt) stopHover(ticks int, x, y int) bool {
p.setTextBgAlpha(0xff)
return true
}
+
+func (p *Prompt) Layout() (int, int) {
+ return p.width, PROMPT_HEIGHT
+}
diff --git a/go/ui/scrollable.go b/go/ui/scrollable.go
deleted file mode 100644
index 90e0a3a5..00000000
--- a/go/ui/scrollable.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package ui
-
-type Scrollable interface {
- Scroll(x, y int)
-}
diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go
index 6f7aa986..65187e30 100644
--- a/go/ui/stateBar.go
+++ b/go/ui/stateBar.go
@@ -8,16 +8,17 @@ import (
)
type StateBar struct {
- X, Y int
- Width, Height int
- gameState game.State
- activePlayerId int
- resourceView *TextBox
- phaseView *TextBox
+ EventHandlersMap
+ X, Y int
+ Width, Height int
+ gameState game.State
+ activePlayerId int
+ resourceView *TextBox
+ phaseView *TextBox
}
func NewStateBar(x, y int, width, height int, s game.State, p *game.Player) *StateBar {
- sb := &StateBar{x, y, width, height, s, p.Id, nil, nil}
+ sb := &StateBar{NewEventHandlersMap(), x, y, width, height, s, p.Id, nil, nil}
return sb
}
@@ -62,3 +63,7 @@ func (sb *StateBar) ForceRedraw() {
func (sb *StateBar) Contains(x, y int) bool {
return sb.resourceView.Contains(x, y) || sb.phaseView.Contains(x, y)
}
+
+func (sb *StateBar) Layout() (int, int) {
+ return sb.Width, sb.Height
+}
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index f43e42b3..0e8adf5f 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -6,7 +6,6 @@ import (
"strconv"
"github.com/hajimehoshi/ebiten/v2"
- "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text/v2"
"muhq.space/muhqs-game/go/font"
"muhq.space/muhqs-game/go/game"
@@ -23,26 +22,26 @@ var (
type TextBox struct {
WidgetBase
- text string
- fg color.Color
- bg color.Color
- font *text.GoTextFace
+ text string
+ fg color.Color
+ bg color.Color
+ font *text.GoTextFace
lineSpacing float64
- XMargin int
- YMargin int
- center bool
+ XMargin int
+ YMargin int
+ center bool
}
func NewFixedTextBox(x, y int, width, height int, t string) *TextBox {
tb := &TextBox{
- WidgetBase: NewWidgetBase(x, y, width, height),
- text: t,
- fg: TEXTBOX_DEFAULT_FOREGROUND,
- bg: TEXTBOX_DEFAULT_BACKGROUND,
- font: font.Font24,
+ WidgetBase: NewWidgetBase(x, y, width, height),
+ text: t,
+ fg: TEXTBOX_DEFAULT_FOREGROUND,
+ bg: TEXTBOX_DEFAULT_BACKGROUND,
+ font: font.Font24,
lineSpacing: 1.5,
- XMargin: TEXTBOX_DEFAULT_MARGIN,
- YMargin: TEXTBOX_DEFAULT_MARGIN,
+ XMargin: TEXTBOX_DEFAULT_MARGIN,
+ YMargin: TEXTBOX_DEFAULT_MARGIN,
}
tb.renderImpl = func() *ebiten.Image { return tb.render() }
return tb
@@ -105,7 +104,7 @@ func NewPermInfo(x, y int, p game.Permanent) *TextBox {
func (tb *TextBox) render() *ebiten.Image {
var x, y float64
- w, h := text.Measure(tb.text, tb.font, tb.font.Size * tb.lineSpacing)
+ w, h := text.Measure(tb.text, tb.font, tb.font.Size*tb.lineSpacing)
if tb.Width == -1 || tb.Height == -1 {
tb.Width = int(w) + 2*tb.XMargin
tb.Height = int(h) + 2*tb.YMargin
@@ -115,10 +114,10 @@ func (tb *TextBox) render() *ebiten.Image {
if !tb.center {
x = float64(tb.XMargin)
} else {
- x = (float64(tb.Width) - w)/2
+ x = (float64(tb.Width) - w) / 2
}
- y = (float64(tb.Height) - h)/2
+ y = (float64(tb.Height) - h) / 2
}
img := ebiten.NewImage(tb.Width, tb.Height)
@@ -169,7 +168,7 @@ func (w *PocList) setText() {
w.text = "No cards"
}
- width, height := text.Measure(w.text, w.font, w.font.Size * w.lineSpacing)
+ width, height := text.Measure(w.text, w.font, w.font.Size*w.lineSpacing)
w.X = w.centerX - int(width/2) - w.XMargin
w.Y = w.bottomY - int(height) - w.YMargin
}
@@ -186,7 +185,7 @@ func (w *PocList) FindObjectAt(_x, _y int) interface{} {
y := _y - w.Y
- _, height := text.Measure(w.text, w.font, w.font.Size * w.lineSpacing)
+ _, height := text.Measure(w.text, w.font, w.font.Size*w.lineSpacing)
i := y / (int(height) / w.poc.Size())
if i >= len(cards) {
@@ -196,56 +195,6 @@ func (w *PocList) FindObjectAt(_x, _y int) interface{} {
return cards[i]
}
-type TextInput struct {
- TextBox
- input string
- label string
-}
-
-func (ti *TextInput) InitTextInput(x, y int, width, height int, label string) {
- ti.TextBox = *(NewFixedTextBox(x, y, width, height, "").Centering(true))
- ti.input = ""
- ti.label = label
-
- ti.renderImpl = func() *ebiten.Image {
- if ti.input == "" {
- ti.text = ti.label
- } else {
- ti.text = ti.input
- }
-
- return ti.render()
- }
-}
-
-func NewTextInput(x, y int, width, height int, label string) *TextInput {
- w := &TextInput{}
- w.InitTextInput(x, y, width, height, label)
- return w
-}
-
-func (ti *TextInput) SetInput(input string) {
- ti.input = input
- ti.ForceRedraw()
-}
-
-func (ti *TextInput) AddInput(input []rune) {
- ti.SetInput(ti.input + string(input))
-}
-
-func (ti *TextInput) HandleKey() {
- if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
- if len(ti.input) > 0 {
- ti.input = ti.input[:len(ti.input)-1]
- ti.ForceRedraw()
- }
- }
-}
-
-func (ti *TextInput) Text() string {
- return ti.input
-}
-
type NumberInput struct {
TextInput
n int
@@ -253,7 +202,7 @@ type NumberInput struct {
func NewNumberInput(x, y int, height, width int, number int) *NumberInput {
ni := &NumberInput{
- n: 0,
+ n: number,
}
ni.InitTextInput(x, y, width, height, strconv.Itoa(number))
return ni
diff --git a/go/ui/textInput.go b/go/ui/textInput.go
new file mode 100644
index 00000000..3a7939c8
--- /dev/null
+++ b/go/ui/textInput.go
@@ -0,0 +1,67 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
+)
+
+type TextInput struct {
+ TextBox
+ input string
+ label string
+ focused bool
+}
+
+func (ti *TextInput) InitTextInput(x, y int, width, height int, label string) {
+ ti.TextBox = *(NewFixedTextBox(x, y, width, height, "").Centering(true))
+ ti.input = ""
+ ti.label = label
+
+ ti.renderImpl = func() *ebiten.Image {
+ if ti.input == "" {
+ ti.text = ti.label
+ } else {
+ ti.text = ti.input
+ }
+
+ return ti.render()
+ }
+}
+
+func NewTextInput(x, y int, width, height int, label string) *TextInput {
+ w := &TextInput{}
+ w.InitTextInput(x, y, width, height, label)
+ return w
+}
+
+func (ti *TextInput) SetInput(input string) {
+ ti.input = input
+ ti.ForceRedraw()
+}
+
+func (ti *TextInput) AddInput(input []rune) {
+ ti.SetInput(ti.input + string(input))
+}
+
+func (ti *TextInput) HandleKey() {
+ if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
+ if len(ti.input) > 0 {
+ ti.input = ti.input[:len(ti.input)-1]
+ ti.ForceRedraw()
+ }
+ }
+}
+
+func (ti *TextInput) Text() string {
+ return ti.input
+}
+
+// return the text or use label as fallback
+func (ti *TextInput) TextOrLabel() string {
+ if ti.input != "" {
+ return ti.input
+ }
+ return ti.label
+}
+
+func (*TextInput) IsFocusable() bool { return true }
diff --git a/go/ui/textInput_generic.go b/go/ui/textInput_generic.go
new file mode 100644
index 00000000..7102eb82
--- /dev/null
+++ b/go/ui/textInput_generic.go
@@ -0,0 +1,16 @@
+//go:build !wasm
+// +build !wasm
+
+package ui
+
+func (ti *TextInput) Focus(focus bool) {
+ ti.focused = focus
+}
+
+func (*TextInput) IsUpdatable() bool { return true }
+func (ti *TextInput) Update() error {
+ if ti.focused {
+ }
+
+ return nil
+}
diff --git a/go/ui/textInput_wasm.go b/go/ui/textInput_wasm.go
new file mode 100644
index 00000000..0ced3ac4
--- /dev/null
+++ b/go/ui/textInput_wasm.go
@@ -0,0 +1,49 @@
+package ui
+
+import (
+ "log"
+ "syscall/js"
+ "unicode/utf8"
+)
+
+// rmueller's version from:
+// ://stackoverflow.com/questions/1752414/how-to-reverse-a-string-in-go?page=1&tab=scoredesc#tab-top
+func reverseString(s string) string {
+ size := len(s)
+ buf := make([]byte, size)
+ for start := 0; start < size; {
+ r, n := utf8.DecodeRuneInString(s[start:])
+ start += n
+ utf8.EncodeRune(buf[size-start:], r)
+ }
+ return string(buf)
+}
+
+func (ti *TextInput) Focus(focus bool) {
+ ti.focused = focus
+ log.Printf("%v set focus: %v\n", ti, focus)
+ if focus {
+ d := js.Global().Get("document")
+ i := d.Call("getElementById", "hiddenInput")
+ // i.Call("focus", map[string]any{"preventScroll": true, "focusVisible": false})
+ i.Call("focus")
+ log.Println("open keyboard")
+
+ log.Printf("set hidden input value to %s", ti.Text())
+ // i.Set("value", ti.Text())
+ i.Set("value", reverseString(ti.Text()))
+ }
+}
+
+func (ti *TextInput) Update() error {
+ if ti.focused {
+ d := js.Global().Get("document")
+ i := d.Call("getElementById", "hiddenInput")
+ // t := i.Get("value").String()
+ t := reverseString(i.Get("value").String())
+ ti.SetInput(t)
+ }
+ return nil
+}
+
+func (*TextInput) IsUpdatable() bool { return true }
diff --git a/go/ui/widget.go b/go/ui/widget.go
index 4ee0caac..da12929d 100644
--- a/go/ui/widget.go
+++ b/go/ui/widget.go
@@ -8,17 +8,31 @@ type Widget interface {
Draw(*ebiten.Image)
FindObjectAt(int, int) interface{}
Contains(int, int) bool
+ Layout() (int, int)
+
+ IsClickable() bool
+ Click(int, int)
+ // IsHoverable() bool
+ // Hover(int, int)
+ IsScrollable() bool
+ Scroll(int, int)
+ IsUpdatable() bool
+ Update() error
+ IsFocusable() bool
+ Focus(bool)
}
type WidgetBase struct {
X, Y int
Width, Height int
img *ebiten.Image
+ Op *ebiten.DrawImageOptions
renderImpl func() *ebiten.Image
+ EventHandlersMap
}
func NewWidgetBase(x, y, width, height int) WidgetBase {
- return WidgetBase{X: x, Y: y, Width: width, Height: height}
+ return WidgetBase{X: x, Y: y, Width: width, Height: height, Op: &ebiten.DrawImageOptions{}, EventHandlersMap: NewEventHandlersMap()}
}
func (w *WidgetBase) Draw(screen *ebiten.Image) {
@@ -26,10 +40,15 @@ func (w *WidgetBase) Draw(screen *ebiten.Image) {
w.img = w.renderImpl()
}
op := &ebiten.DrawImageOptions{}
+ op.GeoM.Concat(w.Op.GeoM)
op.GeoM.Translate(float64(w.X), float64(w.Y))
screen.DrawImage(w.img, op)
}
+func (w *WidgetBase) DrawImageOptions() *ebiten.DrawImageOptions {
+ return w.Op
+}
+
func (w *WidgetBase) ForceRedraw() {
w.img = nil
}
@@ -41,3 +60,7 @@ func (w *WidgetBase) Contains(x, y int) bool {
func (w *WidgetBase) FindObjectAt(int, int) interface{} {
return nil
}
+
+func (w *WidgetBase) Layout() (int, int) {
+ return w.Width, w.Height
+}