Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import org.w3c.dom.events.Event
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
import org.w3c.dom.pointerevents.PointerEventInit

private val actualDensity
get() = window.devicePixelRatio
Expand Down Expand Up @@ -505,27 +506,23 @@ internal class ComposeWindow(
}

/**
* We use both targetTouches and changedTouches:
* - targetTouches is empty when a last pointer is released, but changedTouches won't be empty;
* - changedTouches contains only a Touch of a changed pointer, but compose needs all pointers,
* therefore we take targetTouches in this case;
* The set of touches needed for compose are:
* - targetTouches: contains all pressed touches for the current target element
* - changedTouches when the event is 'touchend' or 'touchcancel': contains released touches
*/
val touches = if (event.targetTouches.length > event.changedTouches.length) {
event.targetTouches.asList()
} else {
event.changedTouches.asList()
val touches = event.targetTouches.asList().fastMap { it to true }.toMutableList()
if (eventType == PointerEventType.Release) {
touches.addAll(event.changedTouches.asList().fastMap { it to false } )
}
val pointers = touches.fastMap { touch ->

val pointers = touches.fastMap { (touch, pressed) ->
ComposeScenePointer(
id = PointerId(touch.identifier.toLong()),
position = Offset(
x = touch.clientX - offset.x,
y = touch.clientY - offset.y
) * density.density,
pressed = when (eventType) {
PointerEventType.Press, PointerEventType.Move -> true
else -> false
},
pressed = pressed,
type = PointerType.Touch,
pressure = touch.unsafeCast<ExtendedTouchEvent>().force.toFloat()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import androidx.compose.ui.events.TouchEventInit
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
Expand Down Expand Up @@ -54,11 +58,11 @@ class GesturesTest : OnCanvasTests {
}

dispatchEvents(
TouchEvent("touchstart", touchEventInit(createTouch(0, getCanvas()))),
TouchEvent("touchstart", touchEventWithChangedTouchesInit(createTouch(0, getCanvas()))),
// first move to exceed the touch slop
TouchEvent("touchmove", touchEventInit(createTouch(0, getCanvas(), clientX = 10.0, clientY = 10.0))),
TouchEvent("touchmove", touchEventInit(createTouch(0, getCanvas(), clientX = 10.0, clientY = 20.0))),
TouchEvent("touchmove", touchEventInit(createTouch(0, getCanvas(), clientX = 20.0, clientY = 20.0))),
TouchEvent("touchmove", touchEventWithChangedTouchesInit(createTouch(0, getCanvas(), clientX = 10.0, clientY = 10.0))),
TouchEvent("touchmove", touchEventWithChangedTouchesInit(createTouch(0, getCanvas(), clientX = 10.0, clientY = 20.0))),
TouchEvent("touchmove", touchEventWithChangedTouchesInit(createTouch(0, getCanvas(), clientX = 20.0, clientY = 20.0))),
)

val actualPan = 10f * currentDensity.density
Expand Down Expand Up @@ -87,45 +91,45 @@ class GesturesTest : OnCanvasTests {
// Simulate two touch points starting fairly close together
TouchEvent(
"touchstart",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 50.0, clientY = 50.0),
createTouch(1, getCanvas(), clientX = 60.0, clientY = 50.0)
)
),
// first move to exceed the touch slop
TouchEvent(
"touchmove",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 45.0, clientY = 60.0),
createTouch(1, getCanvas(), clientX = 65.0, clientY = 50.0)
)
),
// Zoom in, zoom > 1
TouchEvent(
"touchmove",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 40.0, clientY = 50.0),
createTouch(1, getCanvas(), clientX = 70.0, clientY = 50.0)
)
),
TouchEvent(
"touchmove",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 30.0, clientY = 50.0),
createTouch(1, getCanvas(), clientX = 80.0, clientY = 50.0)
)
),
// and now zoom out, zoom < 1
TouchEvent(
"touchmove",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 35.0, clientY = 50.0),
createTouch(1, getCanvas(), clientX = 75.0, clientY = 50.0)
)
),
TouchEvent(
"touchmove",
touchEventInit(
touchEventWithChangedTouchesInit(
createTouch(0, getCanvas(), clientX = 37.0, clientY = 50.0),
createTouch(1, getCanvas(), clientX = 73.0, clientY = 50.0)
)
Expand Down Expand Up @@ -157,8 +161,8 @@ class GesturesTest : OnCanvasTests {
assertNull(lastPointerEvent)

dispatchEvents(
TouchEvent("touchstart", touchEventInit(createTouch(0, getCanvas(), clientX = 50.0, clientY = 50.0))),
TouchEvent("touchmove", touchEventInit(createTouch(0, getCanvas(), clientX = 60.0, clientY = 60.0)))
TouchEvent("touchstart", touchEventWithChangedTouchesInit(createTouch(0, getCanvas(), clientX = 50.0, clientY = 50.0))),
TouchEvent("touchmove", touchEventWithChangedTouchesInit(createTouch(0, getCanvas(), clientX = 60.0, clientY = 60.0)))
)

awaitIdle()
Expand All @@ -181,6 +185,99 @@ class GesturesTest : OnCanvasTests {
assertEquals(PointerEventType.Move, lastPointerEvent!!.type)
}

@Test
fun threeTouchesWithTouchEnd() = runApplicationTest {
val pointerEvents = mutableListOf<PointerEvent>()

createComposeWindow {
Box(modifier = Modifier.fillMaxSize().pointerInput(Unit) {
awaitPointerEventScope {
while (coroutineContext.isActive) {
pointerEvents.add(awaitPointerEvent())
}
}
})
}

assertTrue(pointerEvents.isEmpty())

val touch1 = createTouch(1, getCanvas(), clientX = 10.0, clientY = 10.0)
val touch2 = createTouch(2, getCanvas(), clientX = 20.0, clientY = 20.0)
val touch3 = createTouch(3, getCanvas(), clientX = 30.0, clientY = 30.0)

dispatchEvents(
// +1
TouchEvent(
"touchstart",
touchEventInit(
changedTouches = listOf(touch1),
targetTouches = listOf(touch1),
)
),
// +2
TouchEvent(
"touchstart",
touchEventInit(
changedTouches = listOf(touch2),
targetTouches = listOf(touch1, touch2),
)
),
// +3
TouchEvent(
"touchstart",
touchEventInit(
changedTouches = listOf(touch3),
targetTouches = listOf(touch1, touch2, touch3),
)
),
// -3
TouchEvent(
"touchend",
touchEventInit(
changedTouches = listOf(touch3),
targetTouches = listOf(touch1, touch2),
)
),
// -2
TouchEvent(
"touchend",
touchEventInit(
changedTouches = listOf(touch2),
targetTouches = listOf(touch1),
)
),
// -1
TouchEvent(
"touchend",
touchEventInit(
changedTouches = listOf(touch1),
targetTouches = listOf(),
)
)
)

awaitIdle()

val expected = """
+ 1
+ 1; + 2
+ 1; + 2; + 3
+ 1; + 2; - 3
+ 1; - 2
- 1
""".trimIndent()

val actual = pointerEvents.joinToString("\n") { event ->
event.changes.sortedBy { it.id.value }.joinToString("; ") {
if (it.pressed) {
"+ ${it.id.value}"
} else {
"- ${it.id.value}"
}
}
}
assertEquals(expected, actual)
}
}

external interface Touch
Expand All @@ -206,14 +303,14 @@ private fun createTouch(
"""
)

private fun touchEventInit(vararg touches: Touch): TouchEventInit = js(
private fun touchEventWithChangedTouchesInit(vararg touches: Touch): TouchEventInit = js(
"""
({
bubbles: true,
cancelable: true,
composed: true,
changedTouches: touches,
targetTouches: [],
targetTouches: touches,
touches: []
})
"""
Expand All @@ -232,3 +329,30 @@ private fun touchEventWithTargetTouchesInit(vararg touches: Touch): TouchEventIn
"""
)

@OptIn(ExperimentalJsCollectionsApi::class)
private fun touchEventInit(
changedTouchesBeforeIndex: Int,
vararg touches: Touch,
): TouchEventInit = js(
"""
({
bubbles: true,
cancelable: true,
composed: true,
changedTouches: touches.slice(0, changedTouchesBeforeIndex),
targetTouches: touches.slice(changedTouchesBeforeIndex),
touches: []
})
"""
)

@OptIn(ExperimentalJsCollectionsApi::class, ExperimentalJsExport::class)
private fun touchEventInit(
changedTouches: List<Touch>,
targetTouches: List<Touch>,
): TouchEventInit = touchEventInit(
changedTouchesBeforeIndex = changedTouches.size,
*arrayOf(*changedTouches.toTypedArray(), *targetTouches.toTypedArray())
)