diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-04-29 14:44:48 -0500 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:16 +0200 |
| commit | be2aad1142bde494229d9d27d4123923aa578c1a (patch) | |
| tree | a564d1f6655c29426e1175b6d377769037e041ad /go | |
| parent | 24d6e81d6f878f0ef4910ac125101dab963a913a (diff) | |
| download | muhqs-game-be2aad1142bde494229d9d27d4123923aa578c1a.tar.gz muhqs-game-be2aad1142bde494229d9d27d4123923aa578c1a.zip | |
overhaul ui
* implement events instead of interfaces
Interfaces and the wrappers head the problem that they could not support multiple events at once or only a single one without loosing the others.
* add text input support for WASM on mobile
A hidden html input is required to open the keyboard and receive the
input.
* improve the number choice
* fix using the default number
* add submit button only if a onclick method was provided
Diffstat (limited to 'go')
| -rw-r--r-- | go/ui/buffer.go | 10 | ||||
| -rw-r--r-- | go/ui/button.go | 53 | ||||
| -rw-r--r-- | go/ui/cardGrid.go | 1 | ||||
| -rw-r--r-- | go/ui/choice.go | 44 | ||||
| -rw-r--r-- | go/ui/clickable.go | 40 | ||||
| -rw-r--r-- | go/ui/collection.go | 153 | ||||
| -rw-r--r-- | go/ui/eventHandlers.go | 76 | ||||
| -rw-r--r-- | go/ui/mapView.go | 36 | ||||
| -rw-r--r-- | go/ui/prompt.go | 14 | ||||
| -rw-r--r-- | go/ui/scrollable.go | 5 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 19 | ||||
| -rw-r--r-- | go/ui/textBox.go | 91 | ||||
| -rw-r--r-- | go/ui/textInput.go | 67 | ||||
| -rw-r--r-- | go/ui/textInput_generic.go | 16 | ||||
| -rw-r--r-- | go/ui/textInput_wasm.go | 49 | ||||
| -rw-r--r-- | go/ui/widget.go | 25 |
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 +} |
