// Copyright 2021 Huw Griffiths // Copyright 2025 Florian Fischer // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ui import ( "math" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) const LONGTAP_THRESHOLD = 60 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, } // TouchInput is a "manager" for touch input and provides logical encapsulations of // touch interactions like panning, pinching, and tapping. type TouchInput struct { touchIDs []ebiten.TouchID touches map[ebiten.TouchID]*touch pinch *pinch pan *pan longtap *longtap taps []tap } func (t *TouchInput) consumetap(i int) { if len(t.taps) == 1 { t.taps = []tap{} } else { t.taps[i] = t.taps[len(t.taps)-1] } } var TouchManager *TouchInput func init() { TouchManager = NewTouchInput() } func NewTouchInput() *TouchInput { return &TouchInput{ touches: map[ebiten.TouchID]*touch{}, } } // hypotenuse of a right triangle. func hypotenuse(xa, ya, xb, yb int) float64 { x := math.Abs(float64(xa - xb)) y := math.Abs(float64(ya - yb)) return math.Sqrt(x*x + y*y) } // Update the touch input manager. // After a call to Update, if we have two touches, then we have a pinch, if we // have one touch, held longer than (N)ms, then we have a pan. // If we have quickly released presses, then they are represented in taps. func (in *TouchInput) Update() error { in.taps = []tap{} // What's gone? for id, t := range in.touches { if inpututil.IsTouchJustReleased(id) { if in.pinch != nil && (id == in.pinch.id1 || id == in.pinch.id2) { // FIXME: what about this frame's movement? in.pinch = nil } if in.pan != nil && id == in.pan.id { // FIXME: what about this frame's movement? 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 <= LONGTAP_THRESHOLD && diff < 5 { in.taps = append(in.taps, tap{ x: t.currX, y: t.currY, }) } delete(in.touches, id) } } // What's new? in.touchIDs = inpututil.AppendJustPressedTouchIDs(in.touchIDs[:0]) for _, id := range in.touchIDs { x, y := ebiten.TouchPosition(id) in.touches[ebiten.TouchID(id)] = &touch{ originX: x, originY: y, currX: x, currY: y, } } // What's going on? in.touchIDs = ebiten.AppendTouchIDs(in.touchIDs[:0]) for _, id := range in.touchIDs { t := in.touches[id] t.duration = inpututil.TouchPressDuration(id) t.currX, t.currY = ebiten.TouchPosition(id) } if len(in.touches) == 2 { // Potential pinch? // If the diff between their origins is different to the diff between // their currents and if these two are not already a pinch, then this is // a new pinch! id1, id2 := in.touchIDs[0], in.touchIDs[1] t1, t2 := in.touches[id1], in.touches[id2] originDiff := hypotenuse(t1.originX, t1.originY, t2.originX, t2.originY) currDiff := hypotenuse(t1.currX, t1.currY, t2.currX, t2.currY) if in.pinch == nil && in.pan == nil && in.longtap == nil && math.Abs(originDiff-currDiff) > 3 { t1.wasPinch = true t2.wasPinch = true in.pinch = &pinch{ id1: id1, id2: id2, originH: originDiff, prevH: originDiff, } } } else if len(in.touches) == 1 { // Potential pan or long tap. id := in.touchIDs[0] t := in.touches[id] // The input type is not decided yet if !t.wasPinch && !t.isPan && !t.isLongtap { diff := math.Abs(hypotenuse(t.originX, t.originY, t.currX, t.currY)) if diff > 0.5 { t.isPan = true in.pan = &pan{ id: id, originX: t.originX, originY: t.originY, prevX: t.originX, 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, } } } } return nil } type touch struct { originX, originY int currX, currY int duration int wasPinch, isPan, isLongtap bool } type pinch struct { id1, id2 ebiten.TouchID originH float64 prevH float64 } func (p *pinch) currentH() float64 { x1, y1 := ebiten.TouchPosition(p.id1) x2, y2 := ebiten.TouchPosition(p.id2) return hypotenuse(x1, y1, x2, y2) } func (p *pinch) Total() float64 { return -(p.currentH() - p.originH) } // Incremental zoom that has occurred in this frame. func (p *pinch) Incremental() float64 { curr := p.currentH() delta := curr - p.prevH p.prevH = curr return -delta } type pan struct { id ebiten.TouchID prevX, prevY int originX, originY int } // Total panning that has occurred since this pan started. func (p *pan) Total() (float64, float64) { currX, currY := ebiten.TouchPosition(p.id) return float64(currX - p.originX), float64(currY - p.originY) } // Incremental panning that has occurred in this frame. func (p *pan) Incremental() (float64, float64) { currX, currY := ebiten.TouchPosition(p.id) deltaX, deltaY := currX-p.prevX, currY-p.prevY p.prevX, p.prevY = currX, currY return float64(deltaX), float64(deltaY) } type tap struct { x, y int } type longtap struct { tap id ebiten.TouchID duration int }