mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 13:29:55 +00:00
## Summary When the browser loses focus (e.g., switching apps, showing the dock), touchend events may not fire, causing touchCount to remain non-zero. This blocks all subsequent single-finger interactions since processMouseDown returns early when touchCount > 0. Added visibilitychange and touchcancel event listeners to reset touch state when the page becomes hidden or touch is interrupted by the system. fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/6721 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7134-fix-reset-touch-state-when-iPad-Safari-loses-focus-2be6d73d3650819abc7cf9c602909228) by [Unito](https://www.unito.io)
190 lines
6.1 KiB
TypeScript
190 lines
6.1 KiB
TypeScript
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|
|
|
import { app } from '@/scripts/app'
|
|
|
|
let touchZooming = false
|
|
let touchCount = 0
|
|
|
|
app.registerExtension({
|
|
name: 'Comfy.SimpleTouchSupport',
|
|
setup() {
|
|
let touchDist: number | null = null
|
|
let touchTime: Date | null = null
|
|
let lastTouch: { clientX: number; clientY: number } | null = null
|
|
let lastScale: number | null = null
|
|
function getMultiTouchPos(e: TouchEvent) {
|
|
return Math.hypot(
|
|
e.touches[0].clientX - e.touches[1].clientX,
|
|
e.touches[0].clientY - e.touches[1].clientY
|
|
)
|
|
}
|
|
|
|
function getMultiTouchCenter(e: TouchEvent) {
|
|
return {
|
|
clientX: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
|
clientY: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
|
}
|
|
}
|
|
|
|
app.canvasEl.parentElement?.addEventListener(
|
|
'touchstart',
|
|
(e: TouchEvent) => {
|
|
touchCount += e.changedTouches.length
|
|
|
|
lastTouch = null
|
|
lastScale = null
|
|
if (e.touches?.length === 1) {
|
|
// Store start time for press+hold for context menu
|
|
touchTime = new Date()
|
|
lastTouch = e.touches[0]
|
|
} else {
|
|
touchTime = null
|
|
if (e.touches?.length === 2) {
|
|
// Store center pos for zoom
|
|
lastScale = app.canvas.ds.scale
|
|
lastTouch = getMultiTouchCenter(e)
|
|
|
|
touchDist = getMultiTouchPos(e)
|
|
app.canvas.pointer.isDown = false
|
|
}
|
|
}
|
|
},
|
|
true
|
|
)
|
|
|
|
app.canvasEl.parentElement?.addEventListener(
|
|
'touchend',
|
|
(e: TouchEvent) => {
|
|
touchCount -= e.changedTouches.length
|
|
|
|
if (e.touches?.length !== 1) touchZooming = false
|
|
if (touchTime && !e.touches?.length) {
|
|
if (new Date().getTime() - touchTime.getTime() > 600) {
|
|
if (e.target === app.canvasEl) {
|
|
const touch = {
|
|
button: 2, // Right click
|
|
clientX: e.changedTouches[0].clientX,
|
|
clientY: e.changedTouches[0].clientY,
|
|
pointerId: 1, // changedTouches' id is 0, set it to any number
|
|
isPrimary: true // changedTouches' isPrimary is false, so set it to true
|
|
}
|
|
// context menu info set in 'pointerdown' event
|
|
app.canvasEl.dispatchEvent(new PointerEvent('pointerdown', touch))
|
|
// then, context menu open after 'pointerup' event
|
|
setTimeout(() => {
|
|
app.canvasEl.dispatchEvent(new PointerEvent('pointerup', touch))
|
|
})
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
touchTime = null
|
|
}
|
|
}
|
|
)
|
|
|
|
const resetTouchState = () => {
|
|
touchCount = 0
|
|
touchZooming = false
|
|
touchTime = null
|
|
lastTouch = null
|
|
lastScale = null
|
|
touchDist = null
|
|
}
|
|
|
|
// Reset touch state when page loses visibility (e.g., switching apps on iPad)
|
|
// This prevents touchCount from getting stuck when touchend events are missed
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
resetTouchState()
|
|
}
|
|
})
|
|
|
|
// Also handle touchcancel which fires when touch is interrupted
|
|
app.canvasEl.parentElement?.addEventListener('touchcancel', resetTouchState)
|
|
|
|
app.canvasEl.parentElement?.addEventListener(
|
|
'touchmove',
|
|
(e) => {
|
|
// make a threshold for touchmove to prevent clear touchTime for long press
|
|
if (touchTime && lastTouch && e.touches?.length === 1) {
|
|
const onlyTouch = e.touches[0]
|
|
const deltaX = onlyTouch.clientX - lastTouch.clientX
|
|
const deltaY = onlyTouch.clientY - lastTouch.clientY
|
|
if (deltaX * deltaX + deltaY * deltaY > 30) {
|
|
touchTime = null
|
|
}
|
|
}
|
|
if (e.touches?.length === 2 && lastTouch && !e.ctrlKey && !e.shiftKey) {
|
|
e.preventDefault() // Prevent browser from zooming when two textareas are touched
|
|
app.canvas.pointer.isDown = false
|
|
touchZooming = true
|
|
|
|
LiteGraph.closeAllContextMenus(window)
|
|
// @ts-expect-error
|
|
app.canvas.search_box?.close()
|
|
const newTouchDist = getMultiTouchPos(e)
|
|
|
|
const center = getMultiTouchCenter(e)
|
|
|
|
if (lastScale === null || touchDist === null) return
|
|
let scale = (lastScale * newTouchDist) / touchDist
|
|
|
|
const newX = (center.clientX - lastTouch.clientX) / scale
|
|
const newY = (center.clientY - lastTouch.clientY) / scale
|
|
|
|
// Code from LiteGraph
|
|
if (scale < app.canvas.ds.min_scale) {
|
|
scale = app.canvas.ds.min_scale
|
|
} else if (scale > app.canvas.ds.max_scale) {
|
|
scale = app.canvas.ds.max_scale
|
|
}
|
|
|
|
const oldScale = app.canvas.ds.scale
|
|
|
|
app.canvas.ds.scale = scale
|
|
|
|
// Code from LiteGraph
|
|
if (Math.abs(app.canvas.ds.scale - 1) < 0.01) {
|
|
app.canvas.ds.scale = 1
|
|
}
|
|
|
|
const newScale = app.canvas.ds.scale
|
|
|
|
const convertScaleToOffset = (scale: number) => [
|
|
center.clientX / scale - app.canvas.ds.offset[0],
|
|
center.clientY / scale - app.canvas.ds.offset[1]
|
|
]
|
|
var oldCenter = convertScaleToOffset(oldScale)
|
|
var newCenter = convertScaleToOffset(newScale)
|
|
|
|
app.canvas.ds.offset[0] += newX + newCenter[0] - oldCenter[0]
|
|
app.canvas.ds.offset[1] += newY + newCenter[1] - oldCenter[1]
|
|
|
|
lastTouch.clientX = center.clientX
|
|
lastTouch.clientY = center.clientY
|
|
|
|
app.canvas.setDirty(true, true)
|
|
}
|
|
},
|
|
true
|
|
)
|
|
}
|
|
})
|
|
|
|
const processMouseDown = LGraphCanvas.prototype.processMouseDown
|
|
LGraphCanvas.prototype.processMouseDown = function (e: PointerEvent) {
|
|
if (touchZooming || touchCount) {
|
|
return
|
|
}
|
|
app.canvas.pointer.isDown = false // Prevent context menu from opening on second tap
|
|
return processMouseDown.apply(this, [e])
|
|
}
|
|
|
|
const processMouseMove = LGraphCanvas.prototype.processMouseMove
|
|
LGraphCanvas.prototype.processMouseMove = function (e: PointerEvent) {
|
|
if (touchZooming || touchCount > 1) {
|
|
return
|
|
}
|
|
return processMouseMove.apply(this, [e])
|
|
}
|