New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor synthetic move event sending #456
Conversation
eventType == PointerEventType.Enter || | ||
eventType == PointerEventType.Exit | ||
|
||
private fun PointerInputEvent.isSamePosition(previousEvent: PointerInputEvent?): Boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the difference compared to the previous code. Now we support multitouch for this function - compare by ids
@@ -479,8 +480,8 @@ class ComposeScene internal constructor( | |||
) | |||
needLayout = false | |||
forEachOwner { it.measureAndLayout() } | |||
pointerPositionUpdater.beforeEvent(event) | |||
processPointerInput(event) | |||
pointerPositionUpdater.update() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the difference with the previous code.
In case of relayout, we send always one additional move that is based on the last Move event, not on the current. Even if after that we send one more Move (that was passed to this function).
It was a missing event without which we can have some inconsistent behavior. Imagine a game that has a square that jumps to random position after we hover it:
move event 1 (300, 300) -> processPointerInput -> hover the square -> square receives Enter, and we set squarePosition = (750, 100)
move event 2 (750, 100) -> measureAndLayout, layout square to (750, 100) -> needUpdate() -> update -> send Move (300, 300) -> square recieves Exit -> ... processPointerInput -> 750, 100 -> square receives Enter.
The example is very synthtic thought, and in reality we don't need this. But besides that, I remember that Compose nodes don't work well if we send a different Move after the node changes its position (it is probably a bug of Compose node), so I would keep it just as a precaution.
cfc54c2
to
6436b1d
Compare
1567999
to
e7c18eb
Compare
1. Create SyntheticEventSender, purpose of which to generate synthetic events based on the previous one. Extract necessary functionality from PointerPositionUpdater. 2. PointerPositionUpdater main purpose is update cursor position for the scene via reusing SyntheticEventSender 3. Remove deprecated functions It is needed for my next PR with multitouch implementation where we need to send synthetic presses/releases API Changes: 1. Removed deprecated SkiaRootForTest.processPointerInput
e7c18eb
to
e1eb6b5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this just a refactoring (keeping the existing behavior) to prepare for multi-touch, or are there behavior changes here too?
Because I remember that we realized the synthetic events we generate and the event processing in HitPathTracker were conflicting with each other, and we said that maybe it all should be done in one place.
@@ -65,6 +65,7 @@ class ComposeSceneInputTest { | |||
overlappedPopup.Content() | |||
independentPopup.Content() | |||
} | |||
scene.render() // Popup has 2-frame layout passes. Call it to avoid synthetic events |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My solution (below in pressed popup should own received moves outside popup
) was a bit different. Maybe make them the same, whether yours or mine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your solution is better, rewrote to it.
) | ||
|
||
internal infix fun List<PointerInputEvent>.shouldEqual(expected: List<PointerInputEvent>) { | ||
assertContentEquals(expected.toList().map { it.formatEssential() }, toList().map { it.formatEssential() }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the toList()
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed it. It is added by mistake.
assertContentEquals(expected.toList().map { it.formatEssential() }, toList().map { it.formatEssential() }) | ||
} | ||
|
||
internal fun PointerInputEvent.formatEssential(): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of the fields are not used here (buttons, keyboardModifiers etc.). If that's intentional, it should be documented in shouldEqual
, or just call it something like positionAndDownShouldEqual
.
Someone could use it without reading it fully, expecting it to do a full equality check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
previousEvent.copySynthetic(PointerEventType.Move) { | ||
copySynthetic(position = idToPosition[id] ?: position) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's confusing to use a trailing lambda like this - it's not clear what that lambda is operating on.
I'd call it with an explicit argument (and rename the argument):
previousEvent.copySynthetic(
type = PointerEventType.Move,
pointerMapper = { ... }
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
// we don't use copy here to not forget to nullify properties that shouldn't be in a synthetic event | ||
private fun PointerInputEvent.copySynthetic( | ||
type: PointerEventType, | ||
pointer: PointerInputEventData.() -> PointerInputEventData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like it PointerInputEventData
should be a regular argument, not a receiver.
sendSyntheticMove(previousEvent) | ||
} | ||
|
||
private fun sendSyntheticMove(pointerSourceEvent: PointerInputEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just sourceEvent
is enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pointerSource here tells us that we take this event as "a source of pointers", not that it is "source pointer event". Renamed it to pointersSourceEvent
and added a doc.
) | ||
} | ||
|
||
private fun sendMissingMove(currentEvent: PointerInputEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe sendMoveIfMissing
, or just inline it into send
. It's just two lines of code...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add sendMissingPresses
, sendMissingReleases
in the next PR, where is a lot of code. For consistency, better to keep this code in a separate function.
Also, if we rename to sendMoveIfMissing
, we have to rename the other to sendPressesIfMissing
, which is not a right sentence (it tells us that we either send 3 presses if missing, or 0 presses, not in between).
* | ||
* For example, it can be needed when we scroll content without moving the pointer, and we need | ||
* to highlight the items under the pointer. | ||
*/ | ||
internal class PointerPositionUpdater( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider folding PointerPositionUpdater
into SyntheticEventSender
. They seem to be dealing with the same problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was extracted because in reality they dealing with different problems.
PointerPositionUpdater informs Compose content about the updated position after relayout. Now it informs via sending synthetic events, but that just one of the options.
SyntheticEventSender encapsulates generation of synthetic events based on the previous events. Besides, in the next PR I add generation of Press/Release, which doesn't fit PointerPositionUpdater
) | ||
} | ||
|
||
private fun syntheticEvents( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe call this eventsSentBy
or something like that, because it returns all the events, not just the synthetic ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is better. Renamed it.
/** | ||
* The original raw native event from AWT. | ||
* | ||
* Null if: | ||
* - the native event is sent by another framework (when Compose UI is embed into it) | ||
* - there no native event (in tests, for example) | ||
* - there is no native event (in tests, for example) | ||
* - there was a synthetic move event sent by compose on relayout |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"re-layout"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
It is refactoring + one side effect of refactoring (see the second comment) + one additional change for multitouch (see my first comment. I should put this in the next PR, but it is late to move it there).
Regarding this nothing changed, just a refactoring of the old code, to prepare it for multitouch |
…r-synthetic-events # Conflicts: # compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
Create SyntheticEventSender, purpose of which to generate synthetic events based on the previous one. Extract necessary functionality from PointerPositionUpdater.
PointerPositionUpdater main purpose is update cursor position for the scene via reusing SyntheticEventSender
Remove deprecated functions
It is needed for my next PR with multitouch implementation where we need to send synthetic presses/releases
API Changes: