diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-08-06 18:02:44 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:35 +0200 |
| commit | 778487d25a79721539cea1e47331e3c2f985c8cc (patch) | |
| tree | 1762762f69895465e7d1a0c9844ed9dbb28b2a71 | |
| parent | 121016509135952eb1144abdaa8aeabe5bebe682 (diff) | |
| download | muhqs-game-778487d25a79721539cea1e47331e3c2f985c8cc.tar.gz muhqs-game-778487d25a79721539cea1e47331e3c2f985c8cc.zip | |
rework hover detection
| -rw-r--r-- | go/activities/draft.go | 3 | ||||
| -rw-r--r-- | go/activities/sealed.go | 6 | ||||
| -rw-r--r-- | go/client/game.go | 12 | ||||
| -rw-r--r-- | go/ui/collection.go | 20 | ||||
| -rw-r--r-- | go/ui/eventHandlers.go | 20 | ||||
| -rw-r--r-- | go/ui/hoverDetector.go | 45 | ||||
| -rw-r--r-- | go/ui/hoverable.go | 48 | ||||
| -rw-r--r-- | go/ui/mapView.go | 5 | ||||
| -rw-r--r-- | go/ui/textBox.go | 4 | ||||
| -rw-r--r-- | go/ui/touchManager.go | 30 | ||||
| -rw-r--r-- | go/ui/update.go | 24 | ||||
| -rw-r--r-- | go/ui/widget.go | 4 |
12 files changed, 137 insertions, 84 deletions
diff --git a/go/activities/draft.go b/go/activities/draft.go index 742855c0..53e762a6 100644 --- a/go/activities/draft.go +++ b/go/activities/draft.go @@ -248,7 +248,8 @@ func (d *Draft) startDraft(desc string, setList string, aiN int) error { d.Width-SEALED_DECK_STRIP_WIDTH/2, d.Height-DRAFT_BUTTON_HEIGHT-20, d.player.Deck, - ) + &d.Collection) + d.deckList.Bg(ui.Gray) { cardsInDeck := 0 diff --git a/go/activities/sealed.go b/go/activities/sealed.go index 512bd604..1399e144 100644 --- a/go/activities/sealed.go +++ b/go/activities/sealed.go @@ -101,7 +101,11 @@ func (s *Sealed) startSealed(setList string) { setDeck.MoveInto(s.pool) } - deckList := ui.NewPocList(s.Width-SEALED_DECK_STRIP_WIDTH/2, s.Height-SEALED_BUTTON_HEIGHT-20, s.Deck) + deckList := ui.NewPocList( + s.Width-SEALED_DECK_STRIP_WIDTH/2, + s.Height-SEALED_BUTTON_HEIGHT-20, + s.Deck, + &s.Collection) deckList.Bg(ui.Gray) s.AddWidget(deckList) diff --git a/go/client/game.go b/go/client/game.go index d525a49c..f726c95d 100644 --- a/go/client/game.go +++ b/go/client/game.go @@ -122,7 +122,7 @@ func (g *Game) loadMap(mapName string) *Game { } func (g *Game) initMapUi() { - g.mapView = ui.NewMapView(g.gameState) + g.mapView = ui.NewMapView(g.gameState, &g.Collection) g.AddWidget(g.mapView) storeTiles := g.gameState.Map().FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store }) @@ -179,7 +179,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game { x = g.discardPileButton.X + g.discardPileButton.Width/2 y = g.Height - DEFAULT_BUTTON_HEIGHT - g.discardPileView = ui.NewPocList(x, y, g.activePlayer().DiscardPile) + g.discardPileView = ui.NewPocList(x, y, g.activePlayer().DiscardPile, &g.Collection) if !g.storesOnMap { x = g.stateBar.Width + g.discardPileButton.Width @@ -199,7 +199,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game { x = g.storeButton.X + g.storeButton.Width/2 y = g.Height - DEFAULT_BUTTON_HEIGHT - g.storeView = ui.NewPocList(x, y, g.activePlayer().Store) + g.storeView = ui.NewPocList(x, y, g.activePlayer().Store, &g.Collection) } return g @@ -461,6 +461,10 @@ func (g *Game) updateButtons() { g.passButton.UpdateLabel(passButtonLabel) } +func (g *Game) ResetHover() { + ui.AppendInput(ui.InputEvent{ui.HoverEnd, -1, -1, nil}) +} + func (g *Game) reset() { g.removeChoice() g.selectedObject = nil @@ -582,7 +586,7 @@ func (g *Game) handleSelection(obj any, x, y int) { g.hideStore() } - g.storeView = ui.NewPocList(x, y, g.gameState.Map().StoreOn(obj.Position)) + g.storeView = ui.NewPocList(x, y, g.gameState.Map().StoreOn(obj.Position), &g.Collection) g.showStore() } diff --git a/go/ui/collection.go b/go/ui/collection.go index 78944814..c5f59540 100644 --- a/go/ui/collection.go +++ b/go/ui/collection.go @@ -12,9 +12,9 @@ import ( // another Collection. type Collection struct { Width, Height int - hoverDetector hoverDetector widgets []Widget focused Widget + hovering Widget } type CollectionInterface interface { @@ -39,10 +39,6 @@ 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") @@ -191,6 +187,16 @@ func (c *Collection) Update() error { passInput(ev, txt) ConsumeInput(i) } + case HoverEnd: + if c.hovering != nil { + w.Hover(false, ev.X, ev.Y) + c.hovering = nil + } + case HoverStart: + if w.IsHoverable() { + w.Hover(true, ev.X, ev.Y) + c.hovering = w + } } // handle focused text input } else if ev.Kind == Text { @@ -201,10 +207,6 @@ func (c *Collection) Update() error { } } - x, y := ebiten.CursorPosition() - activeWidget := c.FindWidgetAt(x, y) - c.hoverDetector.update(c, x, y, activeWidget) - return nil } diff --git a/go/ui/eventHandlers.go b/go/ui/eventHandlers.go index b81606f8..701190cf 100644 --- a/go/ui/eventHandlers.go +++ b/go/ui/eventHandlers.go @@ -1,6 +1,7 @@ package ui type ClickHandler = func(int, int) +type HoverHandler = func(bool, int, int) type ScrollHandler = func(int, int) type UpdateHandler = func() error type FocusHandler = func(bool) @@ -8,8 +9,8 @@ type FocusHandler = func(bool) type EventHandlers interface { IsClickable() bool Click(int, int) - // IsHoverable() bool - // Hover(int, int) + IsHoverable() bool + Hover(bool, int, int) IsScrollable() bool Scroll(int, int) IsUpdatable() bool @@ -40,6 +41,16 @@ func (m EventHandlersMap) Click(x, y int) { onclick.(ClickHandler)(x, y) } +func (m EventHandlersMap) IsHoverable() bool { + _, found := m["hover"] + return found +} + +func (m EventHandlersMap) Hover(start bool, x, y int) { + onhover := m["hover"] + onhover.(HoverHandler)(start, x, y) +} + func (m EventHandlersMap) IsScrollable() bool { _, found := m["update"] return found @@ -60,11 +71,6 @@ func (m EventHandlersMap) Update() error { return onupdate.(UpdateHandler)() } -func (m EventHandlersMap) IsHoverable() bool { - _, found := m["hover"] - return found -} - func (m EventHandlersMap) IsFocusable() bool { _, found := m["focus"] return found diff --git a/go/ui/hoverDetector.go b/go/ui/hoverDetector.go index 49161f4f..385b2ffd 100644 --- a/go/ui/hoverDetector.go +++ b/go/ui/hoverDetector.go @@ -3,39 +3,32 @@ 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 { +type _hoverDetector struct { last_x, last_y int ticks int - hovering Hoverable } -func (h *hoverDetector) update(collection *Collection, x, y int, w Widget) { +const HOVER_THRESHOLD = 60 + +var ( + hoverDetector _hoverDetector + longtapHoverDetector _hoverDetector +) + +func (h *_hoverDetector) update(x, y int) (ev InputEvent, ch bool) { if x == h.last_x && y == h.last_y { - h.hover(collection, x, y, w) + h.ticks = h.ticks + 1 + if h.ticks == HOVER_THRESHOLD { + return InputEvent{HoverStart, x, y, nil}, true + } } else { - h.reset(x, y) + if h.ticks > HOVER_THRESHOLD { + ev = InputEvent{HoverEnd, h.last_x, h.last_y, nil} + ch = true + } + h.ticks = 0 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 + return } diff --git a/go/ui/hoverable.go b/go/ui/hoverable.go index 30415c8d..3f7e8def 100644 --- a/go/ui/hoverable.go +++ b/go/ui/hoverable.go @@ -5,63 +5,58 @@ import ( ) 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 { + c *Collection createHint func(x, y int) Widget reset func() xMax, yMax int } -func (h *hoverWidget) showHint(x, y int, c *Collection) { +func (h *hoverWidget) Hover(started bool, x, y int) { + if started { + h.onHover(x, y) + } else { + h.stopHover(x, y) + } +} + +func (h *hoverWidget) showHint(x, y int) { hint := h.createHint(x, y) if hint == nil { return } - c.AddWidget(hint) + h.c.AddWidget(hint) h.reset = func() { - c.RemoveWidget(hint) + h.c.RemoveWidget(hint) } } -func (h *hoverWidget) onHover(ticks int, x, y int, c *Collection) bool { +func (h *hoverWidget) onHover(x, y int) { if h.xMax == 0 || h.yMax == 0 { - width, height := c.Layout() + width, height := h.c.Layout() h.xMax = width h.yMax = height } - - if ticks == HOVER_THRESHOLD { - h.showHint(x, y, c) - return true - } - - return false + h.showHint(x, y) } -func (h *hoverWidget) stopHover(ticks int, x, y int) bool { - if h.reset != nil { - h.reset() - } - return true +func (h *hoverWidget) stopHover(x, y int) { + if h.reset != nil { h.reset() } } type hoverCardView struct { hoverWidget } -func (h *hoverCardView) init(w Widget) { +func (h *hoverCardView) init(w Widget, c *Collection) { + h.c = c h.createHint = func(x, y int) Widget { + obj := w.FindObjectAt(x, y) if obj == nil { return nil @@ -89,7 +84,8 @@ type hoverPermInfo struct { hoverWidget } -func (h *hoverPermInfo) init(w Widget) { +func (h *hoverPermInfo) init(w Widget, c *Collection) { + h.c = c h.createHint = func(x, y int) Widget { obj := w.FindObjectAt(x, y) if obj == nil { diff --git a/go/ui/mapView.go b/go/ui/mapView.go index d73f3573..36455d8c 100644 --- a/go/ui/mapView.go +++ b/go/ui/mapView.go @@ -48,7 +48,7 @@ type MapView struct { permanentsHighlights map[game.Permanent][]color.Color } -func NewMapView(g game.State) *MapView { +func NewMapView(g game.State, c *Collection) *MapView { vw := &MapView{ EventHandlersMap: NewEventHandlersMap(), gameState: g, @@ -56,7 +56,8 @@ func NewMapView(g game.State) *MapView { tileHighlights: make(map[game.Position][]color.Color), permanentsHighlights: make(map[game.Permanent][]color.Color), } - vw.hoverPermInfo.init(vw) + vw.hoverPermInfo.init(vw, c) + vw.RegisterHandler("hover", vw.hoverPermInfo.Hover) return vw } diff --git a/go/ui/textBox.go b/go/ui/textBox.go index 287c2a89..9f5ff0f4 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -144,7 +144,7 @@ type PocList struct { poc game.PileOfCards } -func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { +func NewPocList(centerX, bottomY int, poc game.PileOfCards, c *Collection) *PocList { w := &PocList{ TextBox: *(NewAutoTextBox(-1, -1, "").Centering(true)), centerX: centerX, @@ -152,7 +152,7 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { poc: poc, } - w.hoverCardView.init(w) + w.hoverCardView.init(w, c) w.renderImpl = func() *ebiten.Image { w.setText() diff --git a/go/ui/touchManager.go b/go/ui/touchManager.go index 462b9934..b675ff0e 100644 --- a/go/ui/touchManager.go +++ b/go/ui/touchManager.go @@ -22,20 +22,25 @@ import ( "github.com/hajimehoshi/ebiten/v2/inpututil" ) +const LONGTAP_THRESHOLD = 75 + type TouchType int const ( _tap TouchType = iota + 1 + _longtap _pan _pinch ) var touchTypes = struct { tap TouchType + longtap TouchType pan TouchType pinch TouchType }{ tap: _tap, + longtap: _longtap, pan: _pan, pinch: _pinch, } @@ -48,6 +53,7 @@ type TouchInput struct { pinch *pinch pan *pan + longtap *longtap taps []tap } @@ -99,9 +105,14 @@ func (in *TouchInput) Update() error { in.pan = nil } + if in.longtap != nil && id == in.longtap.id { + // FIXME: what about this frame's movement? + in.longtap = nil + } + // If this one has not been touched long, or moved far, then it's a tap. diff := hypotenuse(t.originX, t.originY, t.currX, t.currY) - if !t.wasPinch && !t.isPan && (t.duration <= 75 || diff < 5) { + if !t.wasPinch && !t.isPan && (t.duration <= LONGTAP_THRESHOLD || diff < 5) { in.taps = append(in.taps, tap{ x: t.currX, y: t.currY, @@ -165,6 +176,15 @@ func (in *TouchInput) Update() error { prevY: t.originY, } } + } else if t.duration > LONGTAP_THRESHOLD { + id := in.touchIDs[0] + t := in.touches[id] + t.isLongtap = true + in.longtap = &longtap{ + id: id, + tap: tap{t.originX, t.originY}, + duration: t.duration, + } } } @@ -175,7 +195,7 @@ type touch struct { originX, originY int currX, currY int duration int - wasPinch, isPan bool + wasPinch, isPan, isLongtap bool } type pinch struct { @@ -228,3 +248,9 @@ func (p *pan) Incremental() (float64, float64) { type tap struct { x, y int } + +type longtap struct { + tap + id ebiten.TouchID + duration int +} diff --git a/go/ui/update.go b/go/ui/update.go index 0bdb8428..c4ed3224 100644 --- a/go/ui/update.go +++ b/go/ui/update.go @@ -21,6 +21,12 @@ const ( Pan // Pinch represents a moving multi touch input. Pinch + // HoverStart represents the start of a hover input. + HoverStart + // HoverEnd represents the end of a hover input. + HoverEnd + // Longtap represents a longer touch input. + Longtap ) // InputEvent contains all information associated with an occurred user input. @@ -84,13 +90,27 @@ func Update() error { } + var hoverEv InputEvent + var hoverChange bool + if TouchManager.longtap != nil { + hoverEv, hoverChange = longtapHoverDetector.update(TouchManager.longtap.x, TouchManager.longtap.y) + } else { + hoverEv, hoverChange = longtapHoverDetector.update(-1, -1) + } + if hoverChange { + AppendInput(hoverEv) + } + // TODO: handle pinching if TouchManager.pinch != nil { } - // TODO: make hover detector a singleton - x, y := ebiten.CursorPosition() + hoverEv, hoverChange = hoverDetector.update(x, y) + if hoverChange { + AppendInput(hoverEv) + } + scrollX, scrollY := ebiten.Wheel() if scrollX != 0 || scrollY != 0 { AppendInput(InputEvent{Scroll, x, y, DistanceCtx{int(scrollX), int(scrollY)}}) diff --git a/go/ui/widget.go b/go/ui/widget.go index 4082c828..f9451a33 100644 --- a/go/ui/widget.go +++ b/go/ui/widget.go @@ -12,8 +12,8 @@ type Widget interface { IsClickable() bool Click(int, int) - // IsHoverable() bool - // Hover(int, int) + IsHoverable() bool + Hover(bool, int, int) IsScrollable() bool Scroll(int, int) IsUpdatable() bool |
