Fixes additional dynamic modifier input failure cases
Bug: 286578275
Test: Adds additional suite of tests
Change-Id: I4168d6d8093f69ce88c01ab5b9516b2deedd0982
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 7d1d663..a52b748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -1338,15 +1338,20 @@
private fun Modifier.dynamicPointerInputModifier(
enabled: Boolean,
key: Any? = Unit,
- onPress: () -> Unit = { },
+ onEnter: () -> Unit = { },
onMove: () -> Unit = { },
+ onPress: () -> Unit = { },
onRelease: () -> Unit = { },
- ) = if (enabled) {
+ onExit: () -> Unit = { },
+ ) = if (enabled) {
pointerInput(key) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
+ PointerEventType.Enter -> {
+ onEnter()
+ }
PointerEventType.Press -> {
onPress()
}
@@ -1356,12 +1361,29 @@
PointerEventType.Release -> {
onRelease()
}
+ PointerEventType.Exit -> {
+ onExit()
+ }
}
}
}
}
} else this
+ private fun Modifier.dynamicPointerInputModifierWithDetectTapGestures(
+ enabled: Boolean,
+ key: Any? = Unit,
+ onTap: () -> Unit = { }
+ ) = if (enabled) {
+ pointerInput(key) {
+ detectTapGestures {
+ onTap()
+ }
+ }
+ } else {
+ this
+ }
+
private fun Modifier.dynamicClickableModifier(
enabled: Boolean,
onClick: () -> Unit
@@ -1372,8 +1394,20 @@
) { onClick() }
} else this
+ // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+ // The next ~20 tests test enabling/disabling dynamic input modifiers (both pointer input and
+ // clickable) using various combinations (touch vs. mouse, Unit vs. unique keys, nested UI
+ // elements vs. all modifiers on one UI element, etc.)
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicClickableModifierTouch_addsAbovePointerInputWithKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithKeyTouchEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1417,8 +1451,15 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicClickableModifierTouch_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyTouchEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1462,9 +1503,18 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
+ */
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicClickableModifierMouse_addsAbovePointerInputWithKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithKeyMouseEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1516,9 +1566,18 @@
}
}
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+ * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
+ */
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicClickableModifierMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
// This is part of a dynamic modifier
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1570,8 +1629,16 @@
}
}
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicInputModifierTouch_addsAboveClickableWithKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithKeyTouchEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1610,8 +1677,16 @@
}
}
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
@Test
- fun dynamicInputModifierTouch_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithUnitKeyTouchEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1649,9 +1724,22 @@
}
}
- // Tests a dynamic pointer input AND a dynamic clickable{} above an existing pointer input.
+ /* Uses pointer input block for the non-dynamic pointer input and BOTH a clickable{} and
+ * pointer input block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
+ * inputs (both on same Box).
+ * Both the dynamic Pointer and clickable{} are disabled to start and then enabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ * 3. Touch down
+ * 4. Assert
+ * 5. Touch move
+ * 6. Assert
+ * 7. Touch up
+ * 8. Assert
+ */
@Test
- fun dynamicInputModifiersInTouchStream_addsAboveClickableWithUnitKey_triggersAllModifiers() {
+ fun dynamicInputAndClickableModifier_addsAbovePointerInputWithUnitKeyTouchEventsWithMove() {
var activeDynamicClickable by mutableStateOf(false)
var dynamicClickableCounter by mutableStateOf(0)
@@ -1683,6 +1771,10 @@
.dynamicClickableModifier(activeDynamicClickable) {
dynamicClickableCounter++
}
+ // Note the .background() above the static pointer input block
+ // TODO (jjw): Remove once bug fixed for when a dynamic pointer input follows
+ // directly after another pointer input (both using Unit key).
+ // Workaround: add a modifier between them OR use unique keys (that is, not Unit)
.background(Color.Green)
.pointerInput(Unit) {
originalPointerInputLambdaExecutionCount++
@@ -1775,13 +1867,19 @@
}
}
- /*
- * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
- * using a hardware device with an Android device.
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierMouse_addsAboveClickableWithKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithKeyMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1827,13 +1925,19 @@
}
}
- /*
- * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
- * using a hardware device with an Android device.
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Mouse "click()" (press/release)
+ * 3. Mouse exit
+ * 4. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierMouse_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableWithUnitKeyMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1878,15 +1982,26 @@
}
}
- /* Tests dynamically adding a pointer input DURING an event stream (specifically, Hover).
- * Hover is the only scenario where you can add a new pointer input modifier during the event
- * stream AND receive events in the same active stream from that new pointer input modifier.
- * It isn't possible in the down/up scenario because you add the new modifier during the down
- * but you don't get another down until the next event stream.
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ *
+ * Tests dynamically adding a pointer input ABOVE an existing pointer input DURING an
+ * event stream (specifically, Hover).
+ *
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Assert
+ * 3. Mouse press
+ * 4. Assert
+ * 5. Mouse release
+ * 6. Assert
+ * 7. Mouse exit
+ * 8. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierHoverMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -1966,14 +2081,17 @@
}
}
- /* This is the same as the test above, but
- * 1. Using clickable{}
- * 2. It enables the dynamic pointer input and starts the hover event stream in a more
- * hacky way (using mouse click without hover which triggers hover enter on release).
+ /* Uses clickable{} for the non-dynamic pointer input and pointer input
+ * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+ * on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierIncompleteMouse_addsAboveClickableHackyEvents_triggersBothModifiers() {
+ fun dynamicInputModifier_addsAboveClickableIncompleteMouseEvents_correctEvents() {
var clickableClickCounter by mutableStateOf(0)
// Note: I'm tracking press instead of release because clickable{} consumes release
var dynamicPressCounter by mutableStateOf(0)
@@ -1996,16 +2114,6 @@
)
}
- // Usually, a proper event stream from hardware for mouse input would be:
- // - enter() (hover enter)
- // - click()
- // - exit()
- // However, in this case, I'm just calling click() which triggers actions:
- // - press
- // - release
- // - hover enter
- // This starts a hover event stream (in a more hacky way) and also enables the dynamic
- // pointer input to start recording events.
rule.onNodeWithTag("myClickable").performMouseInput {
click()
}
@@ -2025,16 +2133,30 @@
}
}
- /* Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ *
+ * Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
* event stream (specifically, Hover).
* Hover is the only scenario where you can add a new pointer input modifier during the event
* stream AND receive events in the same active stream from that new pointer input modifier.
* It isn't possible in the down/up scenario because you add the new modifier during the down
* but you don't get another down until the next event stream.
+ *
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse enter
+ * 2. Assert
+ * 3. Mouse press
+ * 4. Assert
+ * 5. Mouse release
+ * 6. Assert
+ * 7. Mouse exit
+ * 8. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierHoverMouse_addsBelowPointerInputWithUnitKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -2092,6 +2214,8 @@
rule.runOnIdle {
assertTrue(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
+ // Both the original and enabled dynamic pointer input modifiers will get the event
+ // since they are on the same Box.
assertEquals(2, originalPointerInputEventCounter)
assertEquals(1, dynamicPressCounter)
assertEquals(0, dynamicReleaseCounter)
@@ -2123,14 +2247,16 @@
}
}
- /* This is the same as the test above, but
- * 1. Using pointer input
- * 2. It enables the dynamic pointer input and starts the hover event stream with
- * performMouseInput { click() }
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputModifierIncompleteMouse_addsBelowPointerInputUnitKey_triggersBothModifiers() {
+ fun dynamicInputModifier_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -2176,10 +2302,6 @@
rule.runOnIdle {
assertTrue(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- // click() as of today triggers only two MotionEvents (Press and Release) where hardware
- // would trigger enter with Press and exit with Release. Either way, Compose recognizes
- // this is a mouse input, so it creates an Enter before the press. (It won't create
- // an exit until the mouse hovers outside the area OR a press happens outside the area.)
assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
assertEquals(0, dynamicPressCounter)
assertEquals(0, dynamicReleaseCounter)
@@ -2201,14 +2323,21 @@
}
}
- /* This is the same as the test above, but with nested boxes:
- * 1. Using pointer input
- * 2. It enables the dynamic pointer input and starts the hover event stream with
- * performMouseInput { click() }
+ /* The next set of tests uses two nested boxes inside a box. The two nested boxes each contain
+ * their own pointer input modifier (vs. the tests above that apply two pointer input modifiers
+ * to the same box).
+ */
+
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input.
+ * The Dynamic Pointer is disabled to start and then enabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputNestedBoxIncompleteMouse_addsBelowPointerInputUnitKey_triggersBothModifiers() {
+ fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -2217,7 +2346,6 @@
var activateDynamicPointerInput by mutableStateOf(false)
rule.setContent {
-
Box(Modifier.size(100.dp).testTag("myClickable")) {
Box(Modifier
.fillMaxSize()
@@ -2259,11 +2387,7 @@
rule.runOnIdle {
assertTrue(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- // click() as of today triggers only two MotionEvents (Press and Release) where hardware
- // would trigger enter with Press and exit with Release. Either way, Compose recognizes
- // this is a mouse input, so it creates an Enter before the press. (It won't create
- // an exit until the mouse hovers outside the area OR a press happens outside the area.)
- assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+ assertEquals(3, originalPointerInputEventCounter)
assertEquals(0, dynamicPressCounter)
assertEquals(0, dynamicReleaseCounter)
}
@@ -2275,23 +2399,22 @@
rule.runOnIdle {
assertTrue(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- // Because the mouse is still within the box area (at first), Compose doesn't need to
- // trigger Exit. However, because after the release event is done, the event is passed
- // on to a separate box (new dynamic one), Compose triggers exit. Thus, the total is 6.
- assertEquals(6, originalPointerInputEventCounter) // Press, Release, Exit
+ assertEquals(3, originalPointerInputEventCounter)
assertEquals(1, dynamicPressCounter)
assertEquals(1, dynamicReleaseCounter)
}
}
- /* This is the same as the test above, but with nested boxes and toggles pointer input on/off:
- * 1. Using pointer input
- * 2. It enables the dynamic pointer input and starts the hover event stream with
- * performMouseInput { click() }
+ /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+ * non-dynamic pointer input and the dynamic pointer input.
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputToggleNestedBoxIncompleteMouse_addsBelowPointerInputUnitKey_triggersProperly() {
+ fun dynamicInputNestedBox_togglesBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -2300,7 +2423,6 @@
var activateDynamicPointerInput by mutableStateOf(false)
rule.setContent {
-
Box(Modifier.size(100.dp).testTag("myClickable")) {
Box(Modifier
.fillMaxSize()
@@ -2333,6 +2455,7 @@
)
Box(Modifier
.fillMaxSize()
+ .background(Color.Cyan)
.dynamicPointerInputModifier(
enabled = activateDynamicPointerInput,
onPress = {
@@ -2358,11 +2481,7 @@
rule.runOnIdle {
assertTrue(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- // click() as of today triggers only two MotionEvents (Press and Release) where hardware
- // would trigger enter with Press and exit with Release. Either way, Compose recognizes
- // this is a mouse input, so it creates an Enter before the press. (It won't create
- // an exit until the mouse hovers outside the area OR a press happens outside the area.)
- assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+ assertEquals(3, originalPointerInputEventCounter)
assertEquals(0, dynamicPressCounter)
assertEquals(0, dynamicReleaseCounter)
}
@@ -2374,24 +2493,22 @@
rule.runOnIdle {
assertFalse(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- // Because the mouse is still within the box area (at first), Compose doesn't need to
- // trigger Exit. However, because after the release event is done, the event is passed
- // on to a separate box (new dynamic one), Compose triggers exit. Thus, the total is 6.
- assertEquals(6, originalPointerInputEventCounter) // Press, Release, Exit
+ assertEquals(3, originalPointerInputEventCounter)
assertEquals(1, dynamicPressCounter)
assertEquals(1, dynamicReleaseCounter)
}
}
- /* This is the same as the test above, but it uses Foundation's detectTapGestures{} for the
- * the first pointer input and lower level pointer input commands for the second.
- * 1. Using pointer input
- * 2. It enables the dynamic pointer input and starts the hover event stream with
- * performMouseInput { click() }
+ /* Uses Foundation's detectTapGestures{} for the non-dynamic pointer input. The dynamic pointer
+ * input uses the lower level pointer input commands.
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
*/
@OptIn(ExperimentalTestApi::class)
@Test
- fun dynamicInputToggleNestedBoxFoundationWithMouse_addsBelowWithUnitKey_triggersProperly() {
+ fun dynamicInputNestedBoxGesture_togglesBelowWithUnitKeyIncompleteMouseEvents_correctEvents() {
var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
var originalPointerInputEventCounter by mutableStateOf(0)
@@ -2451,12 +2568,1314 @@
rule.runOnIdle {
assertFalse(activateDynamicPointerInput)
assertEquals(1, originalPointerInputLambdaExecutionCount)
- assertEquals(2, originalPointerInputEventCounter)
+ assertEquals(1, originalPointerInputEventCounter)
assertEquals(1, dynamicPressCounter)
assertEquals(1, dynamicReleaseCounter)
}
}
+ /* Uses Foundation's detectTapGestures{} for both the non-dynamic pointer input and the
+ * dynamic pointer input (vs. the lower level pointer input commands).
+ * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowWithKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ // This command is the same as
+ // rule.onNodeWithTag("myClickable").performTouchInput { click() }
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertEquals(0, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /*
+ * The next four tests are based on the test above (nested boxes using a pointer input
+ * modifier blocks with the Foundation Gesture detectTapGestures{}).
+ *
+ * The difference is the dynamic pointer input modifier is enabled to start (while in the
+ * other tests it is disabled to start).
+ *
+ * The tests below tests out variations (mouse vs. touch and Unit keys vs. unique keys).
+ */
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIT for key)
+ * Event sequences:
+ * 1. Touch "click" (down/move/up)
+ * 2. Assert
+ */
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithUnitKeyTouchEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = Unit,
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performClick()
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("myUniqueKey1") {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = "myUniqueKey2",
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+
+ /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses Unit for key)
+ * Event sequences:
+ * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+ * 2. Assert
+ */
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBoxGesture_togglesBelowOffUnitKeyIncompleteMouseEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+ var dynamicTapGestureCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(true)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+
+ detectTapGestures {
+ originalPointerInputTapGestureCounter++
+ activateDynamicPointerInput = true
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifierWithDetectTapGestures(
+ enabled = activateDynamicPointerInput,
+ key = Unit,
+ onTap = {
+ dynamicTapGestureCounter++
+ activateDynamicPointerInput = false
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ assertEquals(0, originalPointerInputLambdaExecutionCount)
+ assertEquals(0, originalPointerInputTapGestureCounter)
+ // Since second box is created following first box, it will get the event
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputTapGestureCounter)
+ assertTrue(activateDynamicPointerInput)
+ assertEquals(1, dynamicTapGestureCounter)
+ }
+ }
+ // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
+ // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+ /* These tests dynamically add a pointer input BEFORE or AFTER an existing pointer input DURING
+ * an event stream (specifically, Hover). Some tests use unique keys while others use UNIT as
+ * the key. Finally, some of the tests apply the modifiers to the same Box while others use
+ * sibling blocks (read the test name for details).
+ *
+ * Test name explains the test.
+ * All tests start with the dynamic pointer disabled and enable it on the first hover enter
+ *
+ * Event sequences:
+ * 1. Hover enter
+ * 2. Assert
+ * 3. Move
+ * 4. Assert
+ * 5. Move
+ * 6. Assert
+ * 7. Hover exit
+ * 8. Assert
+ */
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Hover Exit event then a Hover Enter event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(2, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsAbovePointerInputWithKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .dynamicPointerInputModifier(
+ key = "unique_key_1234",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ .background(Color.Green)
+ .pointerInput("unique_key_5678") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Hover Exit event then a Hover Enter event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(2, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(2, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ .background(Color.Green)
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Move event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputModifier_addsBelowPointerInputWithKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier
+ .size(200.dp)
+ .testTag("myClickable")
+ .pointerInput("unique_key_5678") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ .background(Color.Green)
+ .dynamicPointerInputModifier(
+ key = "unique_key_1234",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+ // same modifier chain), it will receive events as well now.
+ // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+ // event stream for the original modifier.)
+ // Original pointer input: Move event
+ // Dynamic pointer input: Hover enter event
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsBelowPointerInputKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("unique_key_1234") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ key = "unique_key_5678",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(1, dynamicEnterCounter)
+ assertEquals(1, dynamicMoveCounter)
+ assertEquals(1, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsAbovePointerInputUnitKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput(Unit) {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+ }
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun dynamicInputNestedBox_addsAbovePointerInputKeyHoverEvents_correctEvents() {
+ var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+ var originalPointerInputEnterEventCounter by mutableStateOf(0)
+ var originalPointerInputMoveEventCounter by mutableStateOf(0)
+ var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+ var dynamicEnterCounter by mutableStateOf(0)
+ var dynamicMoveCounter by mutableStateOf(0)
+ var dynamicExitCounter by mutableStateOf(0)
+ var activateDynamicPointerInput by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.size(100.dp).testTag("myClickable")) {
+ Box(Modifier
+ .fillMaxSize()
+ .dynamicPointerInputModifier(
+ key = "unique_key_5678",
+ enabled = activateDynamicPointerInput,
+ onEnter = {
+ dynamicEnterCounter++
+ },
+ onMove = {
+ dynamicMoveCounter++
+ },
+ onExit = {
+ dynamicExitCounter++
+ }
+ )
+ )
+ Box(Modifier
+ .fillMaxSize()
+ .background(Color.Green)
+ .pointerInput("unique_key_1234") {
+ originalPointerInputLambdaExecutionCount++
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ when (event.type) {
+ PointerEventType.Enter -> {
+ originalPointerInputEnterEventCounter++
+ activateDynamicPointerInput = true
+ }
+ PointerEventType.Move -> {
+ originalPointerInputMoveEventCounter++
+ }
+ PointerEventType.Exit -> {
+ originalPointerInputExitEventCounter++
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertFalse(activateDynamicPointerInput)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ enter()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(0, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(1, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ moveBy(Offset(1.0f, 1.0f))
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(0, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+
+ rule.onNodeWithTag("myClickable").performMouseInput {
+ exit()
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, originalPointerInputLambdaExecutionCount)
+ assertEquals(1, originalPointerInputEnterEventCounter)
+ assertEquals(2, originalPointerInputMoveEventCounter)
+ assertEquals(1, originalPointerInputExitEventCounter)
+ assertEquals(0, dynamicEnterCounter)
+ assertEquals(0, dynamicMoveCounter)
+ assertEquals(0, dynamicExitCounter)
+ }
+ }
+ // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
@OptIn(ExperimentalTestApi::class)
@Test
@LargeTest
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index c63e2a9..7af8753 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -252,236 +252,6 @@
assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
}
- // Inserts a new Node at the top of an existing branch (tests removal of duplicate Nodes too).
- @Test
- fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithOnePointerId_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId1, listOf(pifNew1, pif1, pif2, pif3, pif4))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- @Test
- fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pif5 = PointerInputNodeMock()
- val pif6 = PointerInputNodeMock()
- val pif7 = PointerInputNodeMock()
- val pif8 = PointerInputNodeMock()
-
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- val pointerId2 = PointerId(2)
-
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
- hitPathTracker.addHitPath(pointerId2, listOf(pifNew1, pif5, pif6, pif7, pif8))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
-
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif5).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif6).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif7).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif8).apply {
- pointerIds.add(pointerId2)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- // Inserts a new Node inside an existing branch (tests removal of duplicate Nodes too).
- @Test
- fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithOnePointerId_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pifNew1, pif2, pif3, pif4))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
- @Test
- fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithTwoPointerIds_correctResult() {
- val pif1 = PointerInputNodeMock()
- val pif2 = PointerInputNodeMock()
- val pif3 = PointerInputNodeMock()
- val pif4 = PointerInputNodeMock()
- val pif5 = PointerInputNodeMock()
- val pif6 = PointerInputNodeMock()
- val pif7 = PointerInputNodeMock()
- val pif8 = PointerInputNodeMock()
-
- val pifNew1 = PointerInputNodeMock()
-
- val pointerId1 = PointerId(1)
- val pointerId2 = PointerId(2)
-
- hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
- hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pifNew1, pif7, pif8))
-
- val expectedRoot = NodeParent().apply {
- children.add(
- Node(pif1).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif2).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif3).apply {
- pointerIds.add(pointerId1)
- children.add(
- Node(pif4).apply {
- pointerIds.add(pointerId1)
- }
- )
- }
- )
- }
- )
- }
- )
-
- children.add(
- Node(pif5).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif6).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pifNew1).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif7).apply {
- pointerIds.add(pointerId2)
- children.add(
- Node(pif8).apply {
- pointerIds.add(pointerId2)
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
- }
-
// Inserts a Node in the bottom of an existing branch (tests removal of duplicate Nodes too).
@Test
fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithOnePointerId_correctResult() {
@@ -492,8 +262,16 @@
val pifNew1 = PointerInputNodeMock()
val pointerId1 = PointerId(1)
+ // Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+ // Clear any old hits from previous calls (does not really apply here since it's the first
+ // call)
+ hitPathTracker.removeDetachedPointerInputNodes()
+
+ // Modifier.Node(s) hit by the second pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
+ // Clear any old hits from previous calls
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -541,10 +319,18 @@
val pointerId1 = PointerId(1)
val pointerId2 = PointerId(2)
+ // Modifier.Node(s) hit by the first pointer input event
hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+ // Clear any old hits from previous calls (does not really apply here since it's the first
+ // call)
+ hitPathTracker.removeDetachedPointerInputNodes()
+ // Modifier.Node(s) hit by the second pointer input event
+ hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
+ // Clear any old hits from previous calls
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1335,7 +1121,7 @@
@Test
fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() {
val throwable = catchThrowable {
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
}
assertThat(throwable).isNull()
@@ -1373,7 +1159,7 @@
// Act.
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
// Assert.
@@ -1451,7 +1237,7 @@
hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue()
@@ -1478,7 +1264,7 @@
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1511,7 +1297,7 @@
val pointerId = PointerId(0)
hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1570,7 +1356,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1648,7 +1434,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1727,7 +1513,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1825,7 +1611,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1897,7 +1683,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -1971,7 +1757,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2070,7 +1856,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent()
@@ -2135,7 +1921,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2204,7 +1990,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2294,7 +2080,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2371,7 +2157,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent()
@@ -2444,7 +2230,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2524,7 +2310,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2602,7 +2388,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2654,7 +2440,7 @@
hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2))
hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2721,7 +2507,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
@@ -2787,7 +2573,7 @@
hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
val expectedRoot = NodeParent().apply {
children.add(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index f442482..d84b8d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -17,6 +17,9 @@
package androidx.compose.ui.input.pointer
import androidx.collection.LongSparseArray
+import androidx.collection.MutableLongObjectMap
+import androidx.collection.MutableObjectList
+import androidx.collection.mutableObjectListOf
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,8 +45,7 @@
/*@VisibleForTesting*/
internal val root: NodeParent = NodeParent()
- // Only used when removing duplicate Nodes from the Node tree ([removeDuplicateNode]).
- private val vectorForHandlingDuplicateNodes: MutableVector<NodeParent> = mutableVectorOf()
+ private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10)
/**
* Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
@@ -56,21 +58,34 @@
* @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s
* @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId]. Must be
* ordered from ancestor to descendant.
+ * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes)
+ * that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node.
*/
- fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {
+ fun addHitPath(
+ pointerId: PointerId,
+ pointerInputNodes: List<Modifier.Node>,
+ prunePointerIdsAndChangesNotInNodesList: Boolean = false
+ ) {
var parent: NodeParent = root
+ hitPointerIdsAndNodes.clear()
var merging = true
- var nodeBranchPathToSkipDuringDuplicateNodeRemoval: Node? = null
eachPin@ for (i in pointerInputNodes.indices) {
val pointerInputNode = pointerInputNodes[i]
+
if (merging) {
val node = parent.children.firstOrNull {
it.modifierNode == pointerInputNode
}
+
if (node != null) {
node.markIsIn()
node.pointerIds.add(pointerId)
+
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+ mutableObjectList.add(node)
parent = node
continue@eachPin
} else {
@@ -82,52 +97,30 @@
pointerIds.add(pointerId)
}
- if (nodeBranchPathToSkipDuringDuplicateNodeRemoval == null) {
- // Null means this is the first new Node created that will need a new branch path
- // (possibly from a pre-existing cached version of the node chain).
- // If that is the case, we need to skip this path when looking for duplicate
- // nodes to remove (that may have previously existed somewhere else in the tree).
- nodeBranchPathToSkipDuringDuplicateNodeRemoval = node
- } else {
- // Every node after the top new node (that is, the top Node in the new path)
- // could have potentially existed somewhere else in the cached node tree, and
- // we need to remove it if we are adding it to this new branch.
- removeDuplicateNode(node, nodeBranchPathToSkipDuringDuplicateNodeRemoval)
- }
+ val mutableObjectList =
+ hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+ mutableObjectList.add(node)
parent.children.add(node)
parent = node
}
- }
- /*
- * Removes duplicate nodes when using a cached version of the node tree. Uses breadth-first
- * search for simplicity (and because the tree will be very small).
- */
- private fun removeDuplicateNode(
- duplicateNodeToRemove: Node,
- headOfPathToSkip: Node
- ) {
- vectorForHandlingDuplicateNodes.clear()
- vectorForHandlingDuplicateNodes.add(root)
-
- while (vectorForHandlingDuplicateNodes.isNotEmpty()) {
- val parent = vectorForHandlingDuplicateNodes.removeAt(0)
-
- for (index in parent.children.indices) {
- val child = parent.children[index]
- if (child == headOfPathToSkip) continue
- if (child.modifierNode == duplicateNodeToRemove.modifierNode) {
- // Assumes there is only one unique Node in the tree (not copies).
- // This also removes all children attached below the node.
- parent.children.remove(child)
- return
- }
- vectorForHandlingDuplicateNodes.add(child)
+ if (prunePointerIdsAndChangesNotInNodesList) {
+ hitPointerIdsAndNodes.forEach { key, value ->
+ removeInvalidPointerIdsAndChanges(key, value)
}
}
}
+ // Removes pointers/changes that are not in the latest hit test
+ private fun removeInvalidPointerIdsAndChanges(
+ pointerId: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes)
+ }
+
/**
* Dispatches [internalPointerEvent] through the hierarchy.
*
@@ -175,13 +168,13 @@
}
/**
- * Removes [PointerInputFilter]s that have been removed from the component tree.
+ * Removes detached Pointer Input Modifier Nodes.
*/
// TODO(shepshapard): Ideally, we can process the detaching of PointerInputFilters at the time
// that either their associated LayoutNode is removed from the three, or their
// associated PointerInputModifier is removed from a LayoutNode.
- fun removeDetachedPointerInputFilters() {
- root.removeDetachedPointerInputFilters()
+ fun removeDetachedPointerInputNodes() {
+ root.removeDetachedPointerInputModifierNodes()
}
}
@@ -272,19 +265,29 @@
children.clear()
}
+ open fun removeInvalidPointerIdsAndChanges(
+ pointerIdValue: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ children.forEach {
+ it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+ }
+ }
+
/**
* Removes all child [Node]s that are no longer attached to the compose tree.
*/
- fun removeDetachedPointerInputFilters() {
+ fun removeDetachedPointerInputModifierNodes() {
var index = 0
while (index < children.size) {
val child = children[index]
+
if (!child.modifierNode.isAttached) {
- children.removeAt(index)
child.dispatchCancel()
+ children.removeAt(index)
} else {
index++
- child.removeDetachedPointerInputFilters()
+ child.removeDetachedPointerInputModifierNodes()
}
}
}
@@ -327,6 +330,22 @@
private var isIn = true
private var hasExited = true
+ override fun removeInvalidPointerIdsAndChanges(
+ pointerIdValue: Long,
+ hitNodes: MutableObjectList<Node>
+ ) {
+ if (this.pointerIds.contains(pointerIdValue)) {
+ if (!hitNodes.contains(this)) {
+ this.pointerIds.remove(pointerIdValue)
+ this.relevantChanges.remove(pointerIdValue)
+ }
+ }
+
+ children.forEach {
+ it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+ }
+ }
+
override fun dispatchMainEventPass(
changes: LongSparseArray<PointerInputChange>,
parentCoordinates: LayoutCoordinates,
@@ -342,6 +361,7 @@
return dispatchIfNeeded {
val event = pointerEvent!!
val size = coordinates!!.size
+
// Dispatch on the tunneling pass.
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Initial, size)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index f2b624c..61e45fa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -168,24 +168,37 @@
if (pass == Main) {
// Cursor within the surface area of this node's bounds
if (pointerEvent.type == PointerEventType.Enter) {
- cursorInBoundsOfNode = true
- displayIconIfDescendantsDoNotHavePriority()
+ onEnter()
} else if (pointerEvent.type == PointerEventType.Exit) {
- cursorInBoundsOfNode = false
+ onExit()
+ }
+ }
+ }
+
+ private fun onEnter() {
+ cursorInBoundsOfNode = true
+ displayIconIfDescendantsDoNotHavePriority()
+ }
+
+ private fun onExit() {
+ if (cursorInBoundsOfNode) {
+ cursorInBoundsOfNode = false
+
+ if (isAttached) {
displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
}
}
}
override fun onCancelPointerInput() {
- // We aren't processing the event (only listening for enter/exit), so we don't need to
- // do anything.
+ // While pointer icon only really cares about enter/exit, there are some cases (dynamically
+ // adding Modifier Nodes) where a modifier might be cancelled but hasn't been detached or
+ // exited, so we need to cover that case.
+ onExit()
}
override fun onDetach() {
- cursorInBoundsOfNode = false
- displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
-
+ onExit()
super.onDetach()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 4230b9c..e514e78 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -98,15 +98,22 @@
val isTouchEvent = pointerInputChange.type == PointerType.Touch
root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
if (hitResult.isNotEmpty()) {
- hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
+ hitPathTracker.addHitPath(
+ pointerId = pointerInputChange.id,
+ pointerInputNodes = hitResult,
+ // Prunes PointerIds (and changes) to support dynamically
+ // adding/removing pointer input modifier nodes.
+ // Note: We do not do this for hover because hover relies on those
+ // non hit PointerIds to trigger hover exit events.
+ prunePointerIdsAndChangesNotInNodesList =
+ pointerInputChange.changedToDownIgnoreConsumed()
+ )
hitResult.clear()
}
}
}
- // Remove [PointerInputFilter]s that are no longer valid and refresh the offset information
- // for those that are.
- hitPathTracker.removeDetachedPointerInputFilters()
+ hitPathTracker.removeDetachedPointerInputNodes()
// Dispatch to PointerInputFilters
val dispatchedToSomething =