Skip to content

Commit

Permalink
Merge pull request #178 from JetBrains/feature/fix-mouse-clickable
Browse files Browse the repository at this point in the history
Fix consuming events by mouse clickable
  • Loading branch information
igordmn committed Feb 2, 2022
2 parents 03f42d1 + 6d907bd commit 21f72b7
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerButtons
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import java.awt.event.KeyEvent.VK_ENTER
import kotlinx.coroutines.coroutineScope

Expand Down Expand Up @@ -136,7 +139,7 @@ internal suspend fun PointerInputScope.detectTapWithContext(
it.changes.forEach { it.consumeDownChange() }
}

val up = waitForFirstInboundUp()
val up = waitForFirstInboundUpOrCancellation()
if (up != null) {
up.changes.forEach { it.consumeDownChange() }
onTap?.invoke(down, up)
Expand All @@ -156,16 +159,26 @@ private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
return event
}

private suspend fun AwaitPointerEventScope.waitForFirstInboundUp(): PointerEvent? {
private suspend fun AwaitPointerEventScope.waitForFirstInboundUpOrCancellation(): PointerEvent? {
while (true) {
val event = awaitPointerEvent()
val change = event.changes[0]
if (change.changedToUp()) {
return if (change.isOutOfBounds(size, extendedTouchPadding)) {
null
} else {
event
val event = awaitPointerEvent(PointerEventPass.Main)
if (event.changes.fastAll { it.changedToUp() }) {
// All pointers are up
return event
}

if (event.changes.fastAny {
it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding)
}
) {
return null // Canceled
}

// Check for cancel by position consumption. We can look on the Final pass of the
// existing pointer event because it comes after the Main pass we checked above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
return null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.foundation

import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.ImageComposeScene
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButtons
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.use
import com.google.common.truth.Truth.assertThat
import org.junit.Test

@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
class MouseClickableTest {
@Test
fun click() = ImageComposeScene(
width = 100,
height = 100,
density = Density(1f)
).use { scene ->
val clicks = mutableListOf<MouseClickScope>()

scene.setContent {
Box(
modifier = Modifier
.mouseClickable {
clicks.add(MouseClickScope(buttons, keyboardModifiers))
}
.size(10.dp, 20.dp)
)
}

var downButtons = PointerButtons(isSecondaryPressed = true)
var upButtons = PointerButtons(isSecondaryPressed = false)
val downKeyboardModifiers = PointerKeyboardModifiers(isCtrlPressed = true)
val upKeyboardModifiers = PointerKeyboardModifiers(isCtrlPressed = true, isShiftPressed = true)
scene.sendPointerEvent(PointerEventType.Move, Offset(0f, 0f))
scene.sendPointerEvent(
PointerEventType.Press, Offset(0f, 0f), buttons = downButtons, keyboardModifiers = downKeyboardModifiers
)
scene.sendPointerEvent(
PointerEventType.Release, Offset(0f, 0f), buttons = upButtons, keyboardModifiers = upKeyboardModifiers
)
assertThat(clicks.size).isEqualTo(1)
assertThat(clicks.last().buttons).isEqualTo(downButtons)
assertThat(clicks.last().keyboardModifiers).isEqualTo(downKeyboardModifiers)

downButtons = PointerButtons(isPrimaryPressed = true)
upButtons = PointerButtons(isPrimaryPressed = false)
scene.sendPointerEvent(PointerEventType.Move, Offset(5f, 5f))
scene.sendPointerEvent(
PointerEventType.Press, Offset(5f, 5f), buttons = downButtons, keyboardModifiers = downKeyboardModifiers
)
scene.sendPointerEvent(
PointerEventType.Release, Offset(5f, 5f), buttons = upButtons, keyboardModifiers = upKeyboardModifiers
)
assertThat(clicks.size).isEqualTo(2)
assertThat(clicks.last().buttons).isEqualTo(downButtons)
assertThat(clicks.last().keyboardModifiers).isEqualTo(downKeyboardModifiers)
}

@Test
fun `consume click`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(1f)
).use { scene ->
var outerBoxClicks = 0
var innerBoxClicks = 0

scene.setContent {
Box(
modifier = Modifier
.clickable {
outerBoxClicks++
}
.size(40.dp, 40.dp)
) {
Box(
modifier = Modifier
.mouseClickable {
innerBoxClicks++
}
.size(10.dp, 20.dp)
)
}
}

val downButtons = PointerButtons(isPrimaryPressed = true)
val upButtons = PointerButtons(isPrimaryPressed = false)
scene.sendPointerEvent(PointerEventType.Move, Offset(0f, 0f))
scene.sendPointerEvent(PointerEventType.Press, Offset(0f, 0f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Release, Offset(0f, 0f), buttons = upButtons)
assertThat(outerBoxClicks).isEqualTo(0)
assertThat(innerBoxClicks).isEqualTo(1)

scene.sendPointerEvent(PointerEventType.Move, Offset(30f, 30f))
scene.sendPointerEvent(PointerEventType.Press, Offset(30f, 30f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Release, Offset(30f, 30f), buttons = upButtons)
assertThat(outerBoxClicks).isEqualTo(1)
assertThat(innerBoxClicks).isEqualTo(1)
}

@Test
fun `don't handle consumed click by another click`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(1f)
).use { scene ->
var outerBoxClicks = 0
var innerBoxClicks = 0

scene.setContent {
Box(
modifier = Modifier
.mouseClickable {
outerBoxClicks++
}
.size(40.dp, 40.dp)
) {
Box(
modifier = Modifier
.clickable {
innerBoxClicks++
}
.size(10.dp, 20.dp)
)
}
}

val downButtons = PointerButtons(isPrimaryPressed = true)
val upButtons = PointerButtons(isPrimaryPressed = false)
scene.sendPointerEvent(PointerEventType.Move, Offset(0f, 0f))
scene.sendPointerEvent(PointerEventType.Press, Offset(0f, 0f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Release, Offset(0f, 0f), buttons = upButtons)
assertThat(outerBoxClicks).isEqualTo(0)
assertThat(innerBoxClicks).isEqualTo(1)

scene.sendPointerEvent(PointerEventType.Move, Offset(30f, 30f))
scene.sendPointerEvent(PointerEventType.Press, Offset(30f, 30f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Release, Offset(30f, 30f), buttons = upButtons)
assertThat(outerBoxClicks).isEqualTo(1)
assertThat(innerBoxClicks).isEqualTo(1)
}

@Test
fun `don't handle consumed click by pan`() = ImageComposeScene(
width = 100,
height = 100,
density = Density(1f)
).use { scene ->
var outerBoxTotalPan = Offset.Zero
var innerBoxClicks = 0

scene.setContent {
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTransformGestures { _, pan, _, _ ->
outerBoxTotalPan += pan
}
}
.size(80.dp, 80.dp)
) {
Box(
modifier = Modifier
.mouseClickable {
innerBoxClicks++
}
.size(50.dp, 50.dp)
)
}
}

val downButtons = PointerButtons(isPrimaryPressed = true)
val upButtons = PointerButtons(isPrimaryPressed = false)
scene.sendPointerEvent(PointerEventType.Move, Offset(0f, 0f))
scene.sendPointerEvent(PointerEventType.Press, Offset(0f, 0f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Move, Offset(20f, 0f))
scene.sendPointerEvent(PointerEventType.Release, Offset(0f, 0f), buttons = upButtons)
assertThat(outerBoxTotalPan).isEqualTo(Offset(20f, 0f))
assertThat(innerBoxClicks).isEqualTo(0)

scene.sendPointerEvent(PointerEventType.Press, Offset(20f, 0f), buttons = downButtons)
scene.sendPointerEvent(PointerEventType.Release, Offset(20f, 0f), buttons = upButtons)
assertThat(outerBoxTotalPan).isEqualTo(Offset(20f, 0f))
assertThat(innerBoxClicks).isEqualTo(1)
}
}

0 comments on commit 21f72b7

Please sign in to comment.