From c12057320c06f9b4a0bd6c9b25a1dd2d2bd8fd8d Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 16 Feb 2023 18:57:15 +0100 Subject: move more ui logic into Collection * Generalize hover detection and move it into ui.Collection * Implement transparencu for the prompt banner --- go/ui/button.go | 7 --- go/ui/choice.go | 12 +---- go/ui/collection.go | 44 +++++++++++++++++- go/ui/hoverDetector.go | 41 +++++++++++++++++ go/ui/hoverable.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ go/ui/mapView.go | 22 ++++++++- go/ui/prompt.go | 37 +++++++++++++++ go/ui/stateBar.go | 4 +- go/ui/textBox.go | 14 +++++- go/ui/widget.go | 6 +-- 10 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 go/ui/hoverDetector.go create mode 100644 go/ui/hoverable.go (limited to 'go/ui') diff --git a/go/ui/button.go b/go/ui/button.go index 78ab459f..eac3c2e4 100644 --- a/go/ui/button.go +++ b/go/ui/button.go @@ -41,13 +41,6 @@ func (b *SimpleButton) render() *ebiten.Image { return img } -func (b *SimpleButton) FindObjectAt(x, y int) interface{} { - if b.Contains(x, y) { - return b - } - return nil -} - func (b *SimpleButton) Click(x, y int) { b.OnClick(b) } diff --git a/go/ui/choice.go b/go/ui/choice.go index bacc6657..28878604 100644 --- a/go/ui/choice.go +++ b/go/ui/choice.go @@ -51,16 +51,8 @@ func (c *Choice) Contains(x, y int) bool { return x >= c.X && x <= c.X+c.Width && y >= c.Y && y <= c.Y+height } -func (c *Choice) FindObjectAt(x, y int) interface{} { - if c.Contains(x, y) { - return c - } - return nil -} - -func (c *Choice) Render() *ebiten.Image { - return nil -} +func (c *Choice) FindObjectAt(x, y int) interface{} { return nil } +func (c *Choice) Render() *ebiten.Image { return nil } func NewObjectChoice(x, y int, objs []interface{}, onClick func(*Choice, int, int)) *Choice { // TODO: use dynamic with width and height diff --git a/go/ui/collection.go b/go/ui/collection.go index ac53ba8c..d75e3267 100644 --- a/go/ui/collection.go +++ b/go/ui/collection.go @@ -1,12 +1,20 @@ package ui import ( + "log" + "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) type Collection struct { - widgets []Widget + Width, Height int + hoverDetector hoverDetector + widgets []Widget +} + +func (c *Collection) Layout() (int, int) { + return c.Width, c.Height } func (c *Collection) Widgets() []Widget { @@ -17,10 +25,40 @@ func (c *Collection) Clear() { c.widgets = []Widget{} } +func (c *Collection) ResetHover() { + c.hoverDetector.reset(0, 0) +} + func (c *Collection) AddWidget(w Widget) { + if w == nil { + log.Panicf("Adding nil widget to collection") + } c.widgets = append(c.widgets, w) } +func (c *Collection) FindWidget(toFind Widget) int { + for i, w := range c.widgets { + if w != toFind { + continue + } + return i + } + return -1 +} + +func (c *Collection) RemoveWidget(widget Widget) { + idx := c.FindWidget(widget) + if idx != -1 { + c.RemoveWidgetAt(idx) + } +} + +func (c *Collection) RemoveWidgetAt(idx int) { + nwidgets := len(c.widgets) + c.widgets[idx] = c.widgets[nwidgets-1] + c.widgets = c.widgets[:nwidgets-1] +} + func (c *Collection) Draw(screen *ebiten.Image) { for _, w := range c.widgets { w.Draw(screen) @@ -39,6 +77,8 @@ func (c *Collection) Update() error { break } + c.hoverDetector.update(c, x, y, activeWidget) + if activeWidget == nil { return nil } @@ -61,7 +101,7 @@ func (c *Collection) Update() error { } if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - if btn, ok := activeWidget.(Button); ok { + if btn, ok := activeWidget.(Clickable); ok { btn.Click(x, y) } } diff --git a/go/ui/hoverDetector.go b/go/ui/hoverDetector.go new file mode 100644 index 00000000..49161f4f --- /dev/null +++ b/go/ui/hoverDetector.go @@ -0,0 +1,41 @@ +package ui + +// A simple structure to detect if the cursor has not moved or started moving again. +// hoverDetector.update must be called during each Update invocation to track the +// frame and the current cursor position. +type hoverDetector struct { + last_x, last_y int + ticks int + hovering Hoverable +} + +func (h *hoverDetector) update(collection *Collection, x, y int, w Widget) { + if x == h.last_x && y == h.last_y { + h.hover(collection, x, y, w) + } else { + h.reset(x, y) + h.last_x, h.last_y = x, y + } +} + +func (h *hoverDetector) hover(collection *Collection, x, y int, w Widget) { + h.ticks++ + + if w == nil || h.hovering != nil { + return + } + + if hoverable, ok := w.(Hoverable); ok && hoverable.onHover(h.ticks, x, y, collection) { + h.hovering = hoverable + } +} + +func (h *hoverDetector) reset(x, y int) { + if h.hovering != nil { + if !h.hovering.stopHover(h.ticks, x, y) { + return + } + h.hovering = nil + } + h.ticks = 0 +} diff --git a/go/ui/hoverable.go b/go/ui/hoverable.go new file mode 100644 index 00000000..54d4ec15 --- /dev/null +++ b/go/ui/hoverable.go @@ -0,0 +1,120 @@ +package ui + +import ( + "muhq.space/muhqs-game/go/game" +) + +const ( + HOVER_THRESHOLD = 60 + + HOVER_CARD_WIDTH = 500 + HOVER_CARD_HEIGHT = 700 +) + +type Hoverable interface { + onHover(ticks int, x, y int, c *Collection) bool + stopHover(ticks int, x, y int) bool +} + +type hoverWidget struct { + createHint func(x, y int) Widget + reset func() + xMax, yMax int +} + +func (h *hoverWidget) showHint(x, y int, c *Collection) { + hint := h.createHint(x, y) + if hint == nil { + return + } + + c.AddWidget(hint) + h.reset = func() { + c.RemoveWidget(hint) + } +} + +func (h *hoverWidget) onHover(ticks int, x, y int, c *Collection) bool { + if h.xMax == 0 || h.yMax == 0 { + width, height := c.Layout() + h.xMax = width + h.yMax = height + } + + if ticks == HOVER_THRESHOLD { + h.showHint(x, y, c) + return true + } + + return false +} + +func (h *hoverWidget) stopHover(ticks int, x, y int) bool { + if h.reset != nil { + h.reset() + } + return true +} + +type hoverCardView struct { + hoverWidget +} + +func (h *hoverCardView) init(w Widget) { + h.createHint = func(x, y int) Widget { + card := w.FindObjectAt(x, y).(*game.Card) + if card == nil { + return nil + } + + wx, wy := x, y + if x+HOVER_CARD_WIDTH > h.xMax || y+HOVER_CARD_HEIGHT > h.yMax { + wx = x - HOVER_CARD_WIDTH + wy = y - HOVER_CARD_HEIGHT + } + if wx < 0 { + wx = 0 + } + if wy < 0 { + wy = 0 + } + + return NewScaledCardView(wx, wy, HOVER_CARD_WIDTH, HOVER_CARD_HEIGHT, card.Path()) + } +} + +type hoverPermInfo struct { + hoverWidget +} + +func (h *hoverPermInfo) init(w Widget) { + h.createHint = func(x, y int) Widget { + obj := w.FindObjectAt(x, y) + if obj == nil { + return nil + } + + perm, ok := obj.(game.Permanent) + if !ok { + return nil + } + + hint := NewPermInfo(x+TILE_WIDTH, y, perm) + + wx, wy := x, y + if hint.X+hint.Width > h.xMax || y+hint.Height > h.yMax { + wx = x - hint.Width + wy = y - hint.Height + } + if wx < 0 { + wx = 0 + } + if wy < 0 { + wy = 0 + } + + hint.X, hint.Y = wx, wy + + return hint + } +} diff --git a/go/ui/mapView.go b/go/ui/mapView.go index 155750b4..0553b348 100644 --- a/go/ui/mapView.go +++ b/go/ui/mapView.go @@ -23,6 +23,7 @@ const ( ) type MapView struct { + hoverPermInfo gameState *game.State mapLayer *ebiten.Image permanentsLayer *ebiten.Image @@ -31,14 +32,25 @@ type MapView struct { } func NewMapView(g *game.State) *MapView { - vw := MapView{g, nil, nil, []game.Position{}, []game.Permanent{}} - return &vw + vw := &MapView{gameState: g} + vw.hoverPermInfo.init(vw) + return vw } func (mv *MapView) Height() int { return len(mv.gameState.Map.Tiles) * TILE_HEIGHT } +func (mv *MapView) Width() int { + maxWidth := 0 + for _, row := range mv.gameState.Map.Tiles { + if n := len(row); n > maxWidth { + maxWidth = n + } + } + return maxWidth * TILE_WIDTH +} + func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { // Move the image's center to the screen's upper-left corner. // This is a preparation for rotating. When geometry matrices are applied, @@ -278,6 +290,12 @@ func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}) } +func (mv *MapView) Contains(x, y int) bool { + w := mv.Width() + h := mv.Height() + return x >= 0 && y >= 0 && x <= w && y <= h +} + func (*MapView) Render(Widget) *ebiten.Image { return nil } diff --git a/go/ui/prompt.go b/go/ui/prompt.go index b8d60a49..93c4b4cd 100644 --- a/go/ui/prompt.go +++ b/go/ui/prompt.go @@ -1,6 +1,8 @@ package ui import ( + "image/color" + "github.com/hajimehoshi/ebiten/v2" "muhq.space/muhqs-game/go/game" @@ -42,6 +44,10 @@ func (p *Prompt) Height() int { return PROMPT_HEIGHT } +func (p *Prompt) ClearSelections() { + p.action.Targets().ClearSelections() +} + func (p *Prompt) Draw(screen *ebiten.Image) { for _, comp := range p.components { comp.Draw(screen) @@ -54,3 +60,34 @@ func (p *Prompt) Contains(x, y int) bool { func (p *Prompt) FindObjectAt(screenX, screenY int) interface{} { return nil } func (*Prompt) Render(Widget) *ebiten.Image { return nil } + +func (p *Prompt) setTextBgAlpha(alpha uint8) { + for _, comp := range p.components { + textBox, ok := comp.(*TextBox) + if !ok { + continue + } + bg := color.RGBAModel.Convert(textBox.bg).(color.RGBA) + bg.A = alpha + textBox.Bg(bg) + textBox.ForceRedraw() + } +} + +func (p *Prompt) onHover(ticks int, x int, y int, c *Collection) bool { + if ticks == 1 { + p.setTextBgAlpha(0x7f) + return true + } + + return false +} + +func (p *Prompt) stopHover(ticks int, x, y int) bool { + if p.Contains(x, y) { + return false + } + + p.setTextBgAlpha(0xff) + return true +} diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go index 93de9af9..1686b5e1 100644 --- a/go/ui/stateBar.go +++ b/go/ui/stateBar.go @@ -44,9 +44,7 @@ func (sb *StateBar) Draw(screen *ebiten.Image) { sb.phaseView.Draw(screen) } -func (sb *StateBar) FindObjectAt(x, y int) interface{} { - return nil -} +func (sb *StateBar) FindObjectAt(x, y int) interface{} { return nil } func (sb *StateBar) ForceRedraw() { if sb.resourceView != nil { diff --git a/go/ui/textBox.go b/go/ui/textBox.go index 2635535c..66b90698 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -84,11 +84,20 @@ func NewUnitInfo(x, y int, u *game.Unit) *TextBox { return NewAutoTextBox(x, y, info) } -func NewPermInfo(x, y int, p game.Permanent) *TextBox { +func NewGenericPermInfo(x, y int, p game.Permanent) *TextBox { info := fmt.Sprintf("%s\nDamage: %d", p.String(), p.Damage()) return NewAutoTextBox(x, y, info) } +func NewPermInfo(x, y int, p game.Permanent) *TextBox { + switch p := p.(type) { + case *game.Unit: + return NewUnitInfo(x, y, p) + default: + return NewGenericPermInfo(x, y, p) + } +} + func (tb *TextBox) render() *ebiten.Image { var x, y int b := text.BoundString(font.Font24, tb.text) @@ -115,6 +124,7 @@ func (tb *TextBox) render() *ebiten.Image { type PocList struct { TextBox + hoverCardView centerX int bottomY int poc game.PileOfCards @@ -128,6 +138,8 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { poc: poc, } + w.hoverCardView.init(w) + w.renderImpl = func() *ebiten.Image { w.setText() return w.render() diff --git a/go/ui/widget.go b/go/ui/widget.go index cfcdd766..4ee0caac 100644 --- a/go/ui/widget.go +++ b/go/ui/widget.go @@ -17,12 +17,8 @@ type WidgetBase struct { renderImpl func() *ebiten.Image } -func renderDummy() *ebiten.Image { - return nil -} - func NewWidgetBase(x, y, width, height int) WidgetBase { - return WidgetBase{x, y, width, height, nil, renderDummy} + return WidgetBase{X: x, Y: y, Width: width, Height: height} } func (w *WidgetBase) Draw(screen *ebiten.Image) { -- cgit v1.2.3