Skip to content

Commit

Permalink
Refactor synthetic events, fix losing Exit event on scroll
Browse files Browse the repository at this point in the history
1. Don't send native event when we send synthetic events

2. Fix losing Exit event on scroll (JetBrains/compose-multiplatform#1324)

The current HitPathTracker algorithm doesn't work well if we miss Move event before non-Move event with a different position (see example in PointerPositionUpdater)

Change-Id: I3bba1a9878626a3a8bd52d86671e3260229425fa
Test: ./gradlew jvmTest desktopTest -Pandroidx.compose.multiplatformEnabled=true
  • Loading branch information
igordmn authored and Oleksandr Karpovich committed Apr 13, 2022
1 parent f070197 commit 0911513
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ val KeyEvent.awtEvent: java.awt.event.KeyEvent get() {
*/
@Suppress("DEPRECATION")
val PointerEvent.awtEventOrNull: java.awt.event.MouseEvent? get() {
if (nativeEvent is SyntheticMouseEvent) return null
return nativeEvent as? java.awt.event.MouseEvent
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@ package androidx.compose.ui.awt
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.platform.PlatformComponent
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.input.pointer.PointerButtons
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.platform.DesktopPlatform
import androidx.compose.ui.platform.AccessibilityControllerImpl
import androidx.compose.ui.platform.PlatformComponent
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.window.density
Expand Down Expand Up @@ -55,6 +52,9 @@ import java.awt.im.InputMethodRequests
import javax.accessibility.Accessible
import javax.accessibility.AccessibleContext
import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
import androidx.compose.ui.ComposeScene
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerType

internal class ComposeLayer {
private var isDisposed = false
Expand All @@ -66,7 +66,8 @@ internal class ComposeLayer {
Dispatchers.Swing,
_component,
Density(1f),
_component::needRedraw
_component::needRedraw,
createSyntheticNativeMoveEvent = _component::createSyntheticMouseEvent,
)

private val density get() = _component.density.density
Expand Down Expand Up @@ -147,6 +148,21 @@ internal class ComposeLayer {
updateSceneSize()
}
}

@Suppress("DEPRECATION")
fun createSyntheticMouseEvent(sourceEvent: Any?, positionSourceEvent: Any?): Any {
sourceEvent as MouseEvent
positionSourceEvent as MouseEvent

return SyntheticMouseEvent(
sourceEvent.source as Component,
MouseEvent.MOUSE_MOVED,
sourceEvent.`when`,
sourceEvent.modifiersEx,
positionSourceEvent.x,
positionSourceEvent.y
)
}
}

init {
Expand Down Expand Up @@ -336,4 +352,14 @@ private val MouseEvent.isMacOsCtrlClick
DesktopPlatform.Current == DesktopPlatform.MacOS &&
((modifiersEx and InputEvent.BUTTON1_DOWN_MASK) != 0) &&
((modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0)
)
)

@Deprecated("Will be removed in Compose 1.3")
internal class SyntheticMouseEvent(
source: Component,
id: Int,
`when`: Long,
modifiers: Int,
x: Int,
y: Int
) : MouseEvent(source, id, `when`, modifiers, x, y, 0, false)
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ private fun PopupLayout(
val owner = SkiaBasedOwner(
platformInputService = scene.platformInputService,
component = scene.component,
pointerPositionUpdater = scene.pointerPositionUpdater,
density = density,
isPopup = true,
isFocusable = focusable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ class ComposeSceneTest {
screenshotRule.snap(surface, "frame1_initial")
assertFalse(hasRenders())

scene.sendPointerEvent(PointerEventType.Enter, Offset(2f, 2f))
scene.sendPointerEvent(PointerEventType.Move, Offset(2f, 2f))
// TODO(demin): why we need extra frame when we send hover + press?
// maybe a race between hoverable and clickable?
awaitNextRender()

scene.sendPointerEvent(PointerEventType.Press, Offset(2f, 2f))
awaitNextRender()
screenshotRule.snap(surface, "frame2_onMousePressed")
Expand All @@ -291,10 +297,23 @@ class ComposeSceneTest {
assertFalse(hasRenders())

scene.sendPointerEvent(PointerEventType.Release, Offset(1f, 1f))

// TODO(demin): why we need extra frame when we send hover + press?
// maybe a race between hoverable and clickable?
awaitNextRender()

scene.sendPointerEvent(PointerEventType.Move, Offset(-1f, -1f))
scene.sendPointerEvent(PointerEventType.Exit, Offset(-1f, -1f))
awaitNextRender()
screenshotRule.snap(surface, "frame3_onMouseReleased")

scene.sendPointerEvent(PointerEventType.Enter, Offset(1f, 1f))
scene.sendPointerEvent(PointerEventType.Move, Offset(1f, 1f))

// TODO(demin): why we need extra frame when we send hover + press?
// maybe a race between hoverable and clickable?
awaitNextRender()

scene.sendPointerEvent(PointerEventType.Press, Offset(3f, 3f))
awaitNextRender()
screenshotRule.snap(surface, "frame4_onMouseMoved_onMousePressed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,27 +95,29 @@ fun JFrame.sendMouseEvent(
}

fun JFrame.sendMouseWheelEvent(
id: Int,
x: Int,
y: Int,
scrollType: Int,
wheelRotation: Int,
scrollType: Int = MouseWheelEvent.WHEEL_UNIT_SCROLL,
wheelRotation: Double = 0.0,
modifiers: Int = 0,
): Boolean {
// we use width and height instead of x and y because we can send (-1, -1), but still need
// the component inside window
val component = findComponentAt(width / 2, height / 2)
val event = MouseWheelEvent(
component,
id,
MouseWheelEvent.MOUSE_WHEEL,
0,
modifiers,
x,
y,
x,
y,
1,
false,
scrollType,
1,
wheelRotation.toInt(),
wheelRotation
)
component.dispatchEvent(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package androidx.compose.ui.window.window

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
Expand Down Expand Up @@ -47,6 +49,7 @@ import androidx.compose.ui.window.launchApplication
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.window.runApplicationTest
import com.google.common.truth.Truth.assertThat
import java.awt.Dimension
import java.awt.Toolkit
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
Expand All @@ -56,7 +59,6 @@ import java.awt.event.MouseEvent.CTRL_DOWN_MASK
import java.awt.event.MouseEvent.MOUSE_DRAGGED
import java.awt.event.MouseEvent.MOUSE_PRESSED
import java.awt.event.MouseEvent.MOUSE_RELEASED
import java.awt.event.MouseEvent.MOUSE_WHEEL
import java.awt.event.MouseEvent.SHIFT_DOWN_MASK
import java.awt.event.MouseWheelEvent.WHEEL_UNIT_SCROLL
import org.junit.Test
Expand Down Expand Up @@ -255,10 +257,14 @@ class WindowInputEventTest {

window.sendMouseEvent(MOUSE_RELEASED, 80, 30)
awaitIdle()
assertThat(events.size).isEqualTo(4)
assertThat(events.last().type).isEqualTo(PointerEventType.Release)
assertThat(events.last().pressed).isEqualTo(false)
assertThat(events.last().position).isEqualTo(Offset(80 * density, 30 * density))
// Synthetic move, because position of the Release isn't the same as in the previous event
assertThat(events.size).isEqualTo(5)
assertThat(events[3].type).isEqualTo(PointerEventType.Move)
assertThat(events[3].pressed).isEqualTo(true)
assertThat(events[3].position).isEqualTo(Offset(80 * density, 30 * density))
assertThat(events[4].type).isEqualTo(PointerEventType.Release)
assertThat(events[4].pressed).isEqualTo(false)
assertThat(events[4].position).isEqualTo(Offset(80 * density, 30 * density))

exitApplication()
}
Expand Down Expand Up @@ -354,24 +360,12 @@ class WindowInputEventTest {
awaitIdle()
assertThat(deltas.size).isEqualTo(0)

window.sendMouseWheelEvent(
MOUSE_WHEEL,
x = 100,
y = 50,
scrollType = WHEEL_UNIT_SCROLL,
wheelRotation = 1
)
window.sendMouseWheelEvent(100, 50, WHEEL_UNIT_SCROLL, wheelRotation = 1.0)
awaitIdle()
assertThat(deltas.size).isEqualTo(1)
assertThat(deltas.last()).isEqualTo(Offset(0f, 1f))

window.sendMouseWheelEvent(
MOUSE_WHEEL,
x = 100,
y = 50,
scrollType = WHEEL_UNIT_SCROLL,
wheelRotation = -1
)
window.sendMouseWheelEvent(100, 50, WHEEL_UNIT_SCROLL, wheelRotation = -1.0)
awaitIdle()
assertThat(deltas.size).isEqualTo(2)
assertThat(deltas.last()).isEqualTo(Offset(0f, -1f))
Expand Down Expand Up @@ -408,13 +402,7 @@ class WindowInputEventTest {
val eventCount = 500

repeat(eventCount) {
window.sendMouseWheelEvent(
MOUSE_WHEEL,
x = 100,
y = 50,
scrollType = WHEEL_UNIT_SCROLL,
wheelRotation = 1
)
window.sendMouseWheelEvent(100, 50, WHEEL_UNIT_SCROLL, wheelRotation = 1.0)
}
awaitIdle()
assertThat(deltas.size).isEqualTo(eventCount)
Expand Down Expand Up @@ -452,13 +440,7 @@ class WindowInputEventTest {
val eventCount = 500

repeat(eventCount) {
window.sendMouseWheelEvent(
MOUSE_WHEEL,
x = 100,
y = 50,
scrollType = WHEEL_UNIT_SCROLL,
wheelRotation = 1
)
window.sendMouseWheelEvent(100, 50, WHEEL_UNIT_SCROLL, wheelRotation = 1.0)
}
awaitIdle()
assertThat(deltas.size).isEqualTo(1)
Expand Down Expand Up @@ -526,11 +508,8 @@ class WindowInputEventTest {
)

window.sendMouseWheelEvent(
MOUSE_WHEEL,
x = 100,
y = 50,
scrollType = WHEEL_UNIT_SCROLL,
wheelRotation = 1,
100, 50, WHEEL_UNIT_SCROLL,
wheelRotation = 1.0,
modifiers = SHIFT_DOWN_MASK or CTRL_DOWN_MASK or
BUTTON1_DOWN_MASK or BUTTON3_DOWN_MASK
)
Expand All @@ -557,6 +536,84 @@ class WindowInputEventTest {
exitApplication()
}

@Test
fun `send scroll into two boxes without intermediate move`() = runApplicationTest {
var box1ScrollCount = 0
var box2ScrollCount = 0

val windowSizeAwt = 100
val window = ComposeWindow().apply {
isUndecorated = true
isResizable = false
size = Dimension(windowSizeAwt, windowSizeAwt)
}

window.isVisible = true

window.setContent {
Row {
Box(
Modifier.size(20.dp).onPointerEvent(PointerEventType.Scroll) {
box1ScrollCount++
}
)
Box(
Modifier.size(20.dp).onPointerEvent(PointerEventType.Scroll) {
box2ScrollCount++
}
)
}
}
awaitIdle()

window.sendMouseWheelEvent(x = 1, y = 1)
window.sendMouseWheelEvent(x = 21, y = 1)

assertThat(box1ScrollCount).isEqualTo(1)
assertThat(box2ScrollCount).isEqualTo(1)

window.dispose()
}

@Test
fun `send release into two boxes without intermediate move`() = runApplicationTest {
var box1ReleaseCount = 0
var box2ReleaseCount = 0

val windowSizeAwt = 100
val window = ComposeWindow().apply {
isUndecorated = true
isResizable = false
size = Dimension(windowSizeAwt, windowSizeAwt)
}

window.isVisible = true

window.setContent {
Row {
Box(
Modifier.size(20.dp).onPointerEvent(PointerEventType.Release) {
box1ReleaseCount++
}
)
Box(
Modifier.size(20.dp).onPointerEvent(PointerEventType.Release) {
box2ReleaseCount++
}
)
}
}
awaitIdle()

window.sendMouseEvent(id = MouseEvent.MOUSE_PRESSED, x = 1, y = 1)
window.sendMouseEvent(id = MouseEvent.MOUSE_RELEASED, x = 21, y = 1)

assertThat(box1ReleaseCount).isEqualTo(0)
assertThat(box2ReleaseCount).isEqualTo(1)

window.dispose()
}

private fun getLockingKeyStateSafe(
mask: Int
): Boolean = try {
Expand Down

0 comments on commit 0911513

Please sign in to comment.