Skip to content

Commit 6cbdd56

Browse files
authored
Fix missing mouse release event scenario with magic mouse (JetBrains#2909)
1 parent d97eee2 commit 6cbdd56

7 files changed

Lines changed: 264 additions & 51 deletions

File tree

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import androidx.compose.ui.input.pointer.PointerEvent
88
* The original raw native event from AWT.
99
*
1010
* Null if:
11-
* - the native event is sent by another framework (when Compose UI is embed into it)
12-
* - there is no native event (in tests, for example)
13-
* - there was a synthetic move event sent by compose on re-layout
14-
* - there was a synthetic move event sent by compose when move is missing between two non-move events
11+
* - The native event is sent by another framework (when Compose UI is embed into it)
12+
* - There is no native event (in tests, for example)
13+
* - The event is a synthetic move event sent by Compose on re-layout
14+
* - The event is a synthetic event sent by Compose to maintain a logically consistent stream of
15+
* events. For example, if a native press event is received without a corresponding move event,
16+
* Compose will synthesize a move event. That move event will have a null [awtEventOrNull].
1517
*
16-
* Always check for null when you want to handle the native event.
18+
* It is therefore recommended to always check for `null` when using this property.
1719
*/
1820
val PointerEvent.awtEventOrNull: java.awt.event.MouseEvent? get() {
1921
return nativeEvent as? java.awt.event.MouseEvent?
@@ -23,10 +25,10 @@ val PointerEvent.awtEventOrNull: java.awt.event.MouseEvent? get() {
2325
* The original raw native event from AWT.
2426
*
2527
* Null if:
26-
* - the native event is sent by another framework (when Compose UI is embed into it)
27-
* - there is no native event (in tests, for example)
28+
* - The native event is sent by another framework (when Compose UI is embed into it)
29+
* - There is no native event (in tests, for example)
2830
*
29-
* Always check for null when you want to handle the native event.
31+
* It is therefore recommended to always check for `null` when using this property.
3032
*/
3133
val KeyEvent.awtEventOrNull: java.awt.event.KeyEvent? get() {
3234
return internal.nativeEvent as? java.awt.event.KeyEvent?

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/SyntheticEventSenderTest.kt

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package androidx.compose.ui
1818

1919
import androidx.compose.ui.geometry.Offset
20+
import androidx.compose.ui.input.pointer.PointerEventType
2021
import androidx.compose.ui.input.pointer.PointerEventType.Companion.Enter
2122
import androidx.compose.ui.input.pointer.PointerEventType.Companion.Exit
2223
import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move
@@ -388,8 +389,8 @@ class SyntheticEventSenderTest {
388389
@Test
389390
fun `should update pointer position with move event after hover event`() {
390391
val received = mutableListOf<PointerInputEvent>()
391-
val sender = SyntheticEventSender {
392-
PointerEventResult(received.add(it))
392+
val sender = SyntheticEventSenderConsumingAllMovements {
393+
received.add(it)
393394
}
394395
sender.send(mouseEvent(Enter, 10f, 20f, pressed = false))
395396

@@ -416,8 +417,8 @@ class SyntheticEventSenderTest {
416417
@Test
417418
fun `should update pointer position with move event after pressed event`() {
418419
val received = mutableListOf<PointerInputEvent>()
419-
val sender = SyntheticEventSender {
420-
PointerEventResult(received.add(it))
420+
val sender = SyntheticEventSenderConsumingAllMovements {
421+
received.add(it)
421422
}
422423
sender.send(mouseEvent(Press, 10f, 20f, pressed = true))
423424

@@ -444,8 +445,8 @@ class SyntheticEventSenderTest {
444445
@Test
445446
fun `should not update pointer position with move event after touch event`() {
446447
val received = mutableListOf<PointerInputEvent>()
447-
val sender = SyntheticEventSender {
448-
PointerEventResult(received.add(it))
448+
val sender = SyntheticEventSenderConsumingAllMovements {
449+
received.add(it)
449450
}
450451
sender.send(event(Press, 1 to touch(10f, 20f, pressed = true)))
451452

@@ -470,8 +471,8 @@ class SyntheticEventSenderTest {
470471
@Test
471472
fun `should not re-enter after mouse exit`() {
472473
val received = mutableListOf<PointerInputEvent>()
473-
val sender = SyntheticEventSender {
474-
PointerEventResult(received.add(it))
474+
val sender = SyntheticEventSenderConsumingAllMovements {
475+
received.add(it)
475476
}
476477
sender.send(mouseEvent(Move, 10f, 20f, pressed = false))
477478
sender.send(mouseEvent(Exit, 10f, 20f, pressed = false))
@@ -488,8 +489,8 @@ class SyntheticEventSenderTest {
488489
@Test
489490
fun `synthetic events should not duplicate scrollDelta`() {
490491
val received = mutableListOf<PointerInputEvent>()
491-
val sender = SyntheticEventSender {
492-
PointerEventResult(received.add(it))
492+
val sender = SyntheticEventSenderConsumingAllMovements {
493+
received.add(it)
493494
}
494495

495496
sender.send(
@@ -513,8 +514,8 @@ class SyntheticEventSenderTest {
513514
@Test
514515
fun `synthetic events should not duplicate panGestureOffset`() {
515516
val received = mutableListOf<PointerInputEvent>()
516-
val sender = SyntheticEventSender {
517-
PointerEventResult(received.add(it))
517+
val sender = SyntheticEventSenderConsumingAllMovements {
518+
received.add(it)
518519
}
519520

520521
sender.send(
@@ -538,8 +539,8 @@ class SyntheticEventSenderTest {
538539
@Test
539540
fun `synthetic events should not duplicate scaleGestureFactor`() {
540541
val received = mutableListOf<PointerInputEvent>()
541-
val sender = SyntheticEventSender {
542-
PointerEventResult(received.add(it))
542+
val sender = SyntheticEventSenderConsumingAllMovements {
543+
received.add(it)
543544
}
544545

545546
sender.send(
@@ -715,8 +716,8 @@ class SyntheticEventSenderTest {
715716
@Test
716717
fun `scale synthetic events should not duplicate scaleGestureFactor`() {
717718
val received = mutableListOf<PointerInputEvent>()
718-
val sender = SyntheticEventSender {
719-
PointerEventResult(received.add(it))
719+
val sender = SyntheticEventSenderConsumingAllMovements {
720+
received.add(it)
720721
}
721722

722723
sender.send(
@@ -733,8 +734,8 @@ class SyntheticEventSenderTest {
733734
@Test
734735
fun `pan synthetic events should not duplicate panGestureOffset`() {
735736
val received = mutableListOf<PointerInputEvent>()
736-
val sender = SyntheticEventSender {
737-
PointerEventResult(received.add(it))
737+
val sender = SyntheticEventSenderConsumingAllMovements {
738+
received.add(it)
738739
}
739740

740741
sender.send(
@@ -748,6 +749,75 @@ class SyntheticEventSenderTest {
748749
assertEquals(Offset(5f, 0f), totalOffset)
749750
}
750751

752+
// https://youtrack.jetbrains.com/issue/CMP-9964
753+
@Test
754+
fun `mouse move unpressing buttons sends release event`() {
755+
val received = mutableListOf<PointerInputEvent>()
756+
val sender = SyntheticEventSenderConsumingAllMovements {
757+
received.add(it)
758+
}
759+
760+
sender.send(mouseEvent(Press, 10f, 10f, pressed = true, nativeEvent = 1))
761+
sender.send(mouseEvent(Move, 10f, 10f, pressed = true, nativeEvent = 2))
762+
assertEquals(0, received.count { it.eventType == Release })
763+
764+
sender.send(mouseEvent(Move, 10f, 10f, pressed = false, nativeEvent = 3))
765+
assertEquals(1, received.count { it.eventType == Release }, "Release event not sent")
766+
767+
// Also, make sure we don't send an extra release event afterward
768+
sender.send(mouseEvent(Release, 10f, 10f, pressed = false, nativeEvent = 4))
769+
assertEquals(1, received.count { it.eventType == Release }, "Extra release event sent")
770+
771+
// But it should be sent as an `Unknown` event
772+
assertEquals(4, received.count { it.nativeEvent != null }, "Missing native event")
773+
assertEquals(PointerEventType.Unknown, received.last { it.nativeEvent != null }.eventType)
774+
}
775+
776+
@Test
777+
fun `extra mouse press events are not sent`() {
778+
val received = mutableListOf<PointerInputEvent>()
779+
val sender = SyntheticEventSenderConsumingAllMovements {
780+
received.add(it)
781+
}
782+
783+
sender.send(mouseEvent(Press, 10f, 10f, pressed = true, nativeEvent = 1))
784+
assertEquals(1, received.count { it.eventType == Press }, "Press event not sent!?")
785+
sender.send(mouseEvent(Press, 20f, 20f, pressed = true, nativeEvent = 2))
786+
assertEquals(1, received.count { it.eventType == Press }, "Extra press event sent")
787+
788+
// But it should be sent as an `Unknown` event
789+
assertEquals(2, received.count { it.nativeEvent != null }, "Missing native event")
790+
assertEquals(PointerEventType.Unknown, received.last { it.nativeEvent != null }.eventType)
791+
}
792+
793+
@Test
794+
fun `extra mouse release events are not sent`() {
795+
val received = mutableListOf<PointerInputEvent>()
796+
val sender = SyntheticEventSenderConsumingAllMovements {
797+
received.add(it)
798+
}
799+
800+
sender.send(mouseEvent(Press, 10f, 10f, pressed = true, nativeEvent = 1))
801+
assertEquals(1, received.count { it.eventType == Press }, "Press event not sent!?")
802+
sender.send(mouseEvent(Release, 10f, 10f, pressed = false, nativeEvent = 2))
803+
assertEquals(1, received.count { it.eventType == Release }, "Release event not sent!?")
804+
sender.send(mouseEvent(Release, 20f, 20f, pressed = false, nativeEvent = 3))
805+
assertEquals(1, received.count { it.eventType == Release }, "Extra release event sent")
806+
807+
// But it should be sent as an `Unknown` event
808+
assertEquals(3, received.count { it.nativeEvent != null }, "Missing native event")
809+
assertEquals(PointerEventType.Unknown, received.last { it.nativeEvent != null }.eventType)
810+
}
811+
812+
private fun SyntheticEventSenderConsumingAllMovements(send: (PointerInputEvent) -> Unit) =
813+
SyntheticEventSender {
814+
send(it)
815+
PointerEventResult(
816+
dispatchedToAPointerInputModifier = true,
817+
anyMovementConsumed = true
818+
)
819+
}
820+
751821
private fun eventsSentBy(
752822
vararg inputEvents: PointerInputEvent
753823
): List<PointerInputEvent> {

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseMoveTest.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
package androidx.compose.ui.input.mouse
2020

2121
import androidx.compose.foundation.background
22+
import androidx.compose.foundation.clickable
2223
import androidx.compose.foundation.layout.Box
2324
import androidx.compose.foundation.layout.Column
2425
import androidx.compose.foundation.layout.Row
26+
import androidx.compose.foundation.layout.fillMaxSize
2527
import androidx.compose.foundation.layout.offset
2628
import androidx.compose.foundation.layout.size
2729
import androidx.compose.foundation.lazy.LazyColumn
@@ -41,7 +43,9 @@ import androidx.compose.ui.Modifier
4143
import androidx.compose.ui.geometry.Offset
4244
import androidx.compose.ui.geometry.Size
4345
import androidx.compose.ui.graphics.Color
46+
import androidx.compose.ui.input.pointer.PointerButtons
4447
import androidx.compose.ui.input.pointer.PointerEvent
48+
import androidx.compose.ui.input.pointer.PointerEventPass
4549
import androidx.compose.ui.input.pointer.PointerEventType
4650
import androidx.compose.ui.input.pointer.onPointerEvent
4751
import androidx.compose.ui.input.pointer.pointerInput
@@ -63,6 +67,8 @@ import com.google.common.truth.Truth.assertThat
6367
import com.google.common.truth.Truth.assertWithMessage
6468
import java.util.concurrent.Executors
6569
import kotlin.random.Random
70+
import kotlin.test.assertContentEquals
71+
import kotlin.test.assertEquals
6672
import kotlin.test.assertNull
6773
import kotlinx.coroutines.asCoroutineDispatcher
6874
import kotlinx.coroutines.runBlocking
@@ -734,6 +740,65 @@ class MouseMoveTest {
734740

735741
assertNull(composeScene.lastKnownPointerPosition)
736742
}
743+
744+
// https://youtrack.jetbrains.com/issue/CMP-9964
745+
@Test
746+
fun magicMouseScenario() = ImageComposeScene(
747+
width = 100,
748+
height = 100,
749+
).useInUiThread { scene ->
750+
var clicksCount = 0
751+
scene.setContent {
752+
Box(modifier = Modifier
753+
.fillMaxSize()
754+
.clickable {
755+
clicksCount++
756+
}
757+
)
758+
}
759+
760+
// The bug in this scenario is that no mouse-release event was generated
761+
// - SyntheticEventSender.sendMissingReleases didn't include the last released pointer
762+
// when receiving the 3rd event.
763+
// - HitPathTracker ignores the 3rd event because it's a Move event at the same
764+
// coordinates as the previous (2nd) event.
765+
// - CanvasLayersComposeScene.processPointerInputEvent clears `gestureOwner` after
766+
// receiving the 3rd event, so when it receives the 4th event, it doesn't send it.
767+
scene.sendPointerEvent(PointerEventType.Press, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = true))
768+
scene.sendPointerEvent(PointerEventType.Move, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = true))
769+
scene.sendPointerEvent(PointerEventType.Move, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = false))
770+
scene.sendPointerEvent(PointerEventType.Release, Offset(10f, 10f))
771+
772+
assertEquals(1, clicksCount)
773+
}
774+
775+
@Test
776+
fun allNativeMouseEventsAreSent1() = ImageComposeScene(
777+
width = 100,
778+
height = 100,
779+
).useInUiThread { scene ->
780+
val nativeEventsReceived = mutableListOf<Any>()
781+
scene.setContent {
782+
Box(modifier = Modifier
783+
.fillMaxSize()
784+
.pointerInput(Unit) {
785+
awaitPointerEventScope {
786+
while (true) {
787+
val event = awaitPointerEvent(PointerEventPass.Main)
788+
event.nativeEvent?.let { nativeEventsReceived.add(it) }
789+
}
790+
}
791+
}
792+
)
793+
}
794+
795+
scene.sendPointerEvent(PointerEventType.Press, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = true), nativeEvent = 1)
796+
scene.sendPointerEvent(PointerEventType.Move, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = true), nativeEvent = 2)
797+
scene.sendPointerEvent(PointerEventType.Move, Offset(10f, 10f), buttons = PointerButtons(isPrimaryPressed = false), nativeEvent = 3)
798+
scene.sendPointerEvent(PointerEventType.Release, Offset(10f, 10f), nativeEvent = 4)
799+
800+
assertContentEquals(listOf(1, 2, 3, 4), nativeEventsReceived)
801+
}
737802
}
738803

739804
private fun Modifier.collectPointerEvents(

0 commit comments

Comments
 (0)