Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toDpSize
import androidx.compose.ui.unit.toMaxConstraints
import androidx.compose.ui.unit.toSize
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmName
Expand Down Expand Up @@ -231,7 +232,7 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor(
* Constraints used to measure and layout content.
*/
var constraints: Constraints
get() = scene.size?.toConstraints() ?: Constraints()
get() = scene.size.toMaxConstraints()
set(value) { scene.size = value.toIntSize() }

/**
Expand Down Expand Up @@ -402,6 +403,4 @@ private fun Constraints.toIntSize() =
IntSize(width = maxWidth, height = maxHeight)
} else {
null
}

private fun IntSize.toConstraints() = Constraints(maxWidth = width, maxHeight = height)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alexander Maryanovsky (@m-sasha) I have migrated the rest of collections in SyntheticEventSender to use the primitive-based ones. Now the Pointers are stored using their underlying primitive values for better performance

Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
package androidx.compose.ui.input.pointer

import androidx.collection.LongLongMap
import androidx.collection.MutableLongList
import androidx.collection.MutableLongSet
import androidx.collection.buildLongLongMap
import androidx.collection.buildLongSet
import androidx.collection.mutableLongSetOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.scene.PointerEventResult
import androidx.compose.ui.scene.merging
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilteredMap
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
Expand Down Expand Up @@ -139,7 +142,7 @@ internal class SyntheticEventSender(
// modifiers as the previous event.
// Note that missing move events for this event should have already been sent
fun areSameParams(e1: PointerInputEvent, e2: PointerInputEvent): Boolean {
if (e1.pressedIds().toSet() != e2.pressedIds().toSet()) return false
if (e1.pressedIds() != e2.pressedIds()) return false
if (e1.buttons != e2.buttons) return false
if (e1.keyboardModifiers != e2.keyboardModifiers) return false
return true
Expand Down Expand Up @@ -224,8 +227,8 @@ internal class SyntheticEventSender(
val previousEvent = previousEvent ?: return UnconsumedEventResult
val previousPressed = previousEvent.pressedIds()
val currentPressed = currentEvent.pressedIds()
val newReleased = (previousPressed - currentPressed.toSet()).toList()
val sendingAsUp = HashSet<PointerId>(newReleased.size)
val newReleased = previousPressed - currentPressed
val sendingAsUp = PointerIdSet(newReleased.size)

var result = UnconsumedEventResult
val lastIndex = when (currentEvent.eventType) {
Expand Down Expand Up @@ -253,10 +256,10 @@ internal class SyntheticEventSender(
}

private fun sendMissingPresses(currentEvent: PointerInputEvent): PointerEventResult {
val previousPressed = previousEvent?.pressedIds().orEmpty().toSet()
val previousPressed = previousEvent?.pressedIds()?.toSet() ?: mutableLongSetOf()
val currentPressed = currentEvent.pressedIds()
val newPressed = (currentPressed - previousPressed).toList()
val sendingAsDown = HashSet<PointerId>(newPressed.size)
val newPressed = currentPressed - previousPressed
val sendingAsDown = PointerIdSet(newPressed.size)

var result = UnconsumedEventResult
val lastIndex = when (currentEvent.eventType) {
Expand All @@ -283,10 +286,6 @@ internal class SyntheticEventSender(
return result
}

private fun PointerInputEvent.pressedIds(): List<PointerId> =
pointers.fastFilteredMap(PointerInputEventData::down, PointerInputEventData::id)


private fun sendInternal(event: PointerInputEvent): PointerEventResult {
when (event.eventType) {
PointerEventType.ScaleStart -> isScaleGestureInProgress = true
Expand Down Expand Up @@ -414,6 +413,47 @@ internal class SyntheticEventSender(

private typealias PointerToPositionMap = LongLongMap

private typealias PointerIdSet = MutableLongSet

private typealias PointerIdList = MutableLongList

private fun PointerInputEvent.pressedIds(): PointerIdList {
val target = MutableLongList(pointers.size)
pointers.fastForEach {
if (it.down) target += it.id.value
}
return target
}

private operator fun PointerIdList.minus(elements: PointerIdList): PointerIdList =
when {
elements.isEmpty() -> this
elements.size > 16 -> {
//Optimizing for when the element list is large, converting to a Set is cheaper than repeated O(N) contains checks
val set = elements.toSet()
filter { it !in set }
}
else -> filter { it !in elements }
}

private inline fun PointerIdList.filter(predicate: (Long) -> Boolean): PointerIdList {
val target = MutableLongList(size)
forEach { if (predicate(it)) target += it }
return target
}

@Suppress("NOTHING_TO_INLINE")
private inline operator fun PointerIdSet.contains(id: PointerId): Boolean = contains(id.value)

private fun PointerIdList.toSet(): PointerIdSet = buildLongSet(size) {
this@toSet.forEach { add(it) }
} as PointerIdSet //Safe cast

internal operator fun PointerIdList.minus(elements: PointerIdSet): PointerIdList {
if (elements.isEmpty()) return this
return filter { it !in elements }
}

private fun List<PointerInputEventData>.mapPointersToPosition(): PointerToPositionMap =
buildLongLongMap(size) {
this@mapPointersToPosition.fastForEach { ptr ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.contains
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.toMaxConstraints
import androidx.compose.ui.unit.toRect
import androidx.compose.ui.useLegacyRenderNodeLayers
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.trace
import androidx.compose.ui.viewinterop.InteropPointerInputModifier
import androidx.compose.ui.viewinterop.InteropView
Expand Down Expand Up @@ -393,7 +396,7 @@ internal class RootNodeOwner(
}

private fun isInBounds(localPosition: Offset): Boolean =
size?.toRect()?.contains(localPosition) ?: true
size?.contains(localPosition) ?: true

private fun calculateBoundsInWindow(): Rect? {
val rect = size?.toRect() ?: return null
Expand Down Expand Up @@ -972,12 +975,11 @@ internal class RootNodeOwner(
// So, we applying it before drawing to reflect the changes from previous phases.
// Changes that requires another round of invalidation will be scheduled to next frame.
if (dirtyLayers.isNotEmpty()) {
for (i in 0 until dirtyLayers.size) {
val layer = dirtyLayers[i]
dirtyLayers.fastForEach { layer ->
layer.updateDisplayList()
}
dirtyLayers.clear()
}
dirtyLayers.clear()

// Draw root node
owner.root.draw(
Expand All @@ -1002,9 +1004,6 @@ internal class RootNodeOwner(
}
}

private fun IntSize?.toMaxConstraints() =
if (this == null) Constraints() else Constraints(maxWidth = width, maxHeight = height)

private object IdentityPositionCalculator : PositionCalculator {
override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
override fun localToScreen(localPosition: Offset): Offset = localPosition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ internal inline fun DpSize.coerceAtMost(size: DpSize): DpSize =
internal inline fun IntSize.toRect(): Rect =
Rect(0f, 0f, width.toFloat(), height.toFloat())

/**
* Returns true if the given [offset] is contained within this [IntSize] and (0,0)
* This is used to avoid [Rect] object allocations on hot paths
*/
@Stable
internal inline fun IntSize.contains(offset: Offset): Boolean {
val offsetY = offset.y
val offsetX = offset.x
return (offsetX >= 0f) and (offsetX < width) and (offsetY >= 0f) and (offsetY < height)
}

@Stable
internal fun IntSize.toDpSize(density: Density): DpSize {
with(density) {
Expand Down Expand Up @@ -154,6 +165,9 @@ internal fun DpOffset.requireReal(): DpOffset {
}

@Stable
internal inline fun IntSize?.toMaxConstraints() =
if (this == null) Constraints() else Constraints(maxWidth = width, maxHeight = height)

internal fun IntRect.union(other: IntRect): IntRect =
IntRect(
left = min(left, other.left),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ internal abstract class WebTextInputService :
* We need the correct position, so the software keyboard wouldn't overlap the text input.
* See https://youtrack.jetbrains.com/issue/CMP-8611 for details.
*/
internal open val currentTouchOffset: Offset? = null
internal open val currentTouchOffset: Offset = Offset.Unspecified

/**
* This container will host the actual hidden HTML input element.
Expand Down Expand Up @@ -86,12 +86,12 @@ internal abstract class WebTextInputService :
)
backingDomInput?.register()

if (currentTouchOffset != null) {
if (currentTouchOffset != Offset.Unspecified) {
// We don't know the real position yet, but it's reasonable to assume that
// if startInput is caused by a touch event,
// then the TextField is positioned around the touch coordinates.
// See the currentTouchOffset KDoc for the details.
notifyFocusedRect(Rect(currentTouchOffset!!, Size(1f, 1f)))
notifyFocusedRect(Rect(currentTouchOffset, Size(1f, 1f)))
}
showSoftwareKeyboard()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package androidx.compose.ui.window
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import kotlinx.browser.document
import kotlinx.dom.clear
import org.w3c.dom.Element
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLDivElement
Expand Down Expand Up @@ -84,8 +83,8 @@ fun ComposeViewport(
configure: ComposeViewportConfiguration.() -> Unit = {},
content: @Composable () -> Unit = { }
) = onSkikoReady {
viewportContainer.clear()

clearNodeChildren(viewportContainer)
// Create a common positioning container (parent html element) for shadow and the interop containers
// to position at the same place - the interop container is position at 0,0 relative to the shadow.
// It simplifies the positioning of the interop views in the container.
Expand Down Expand Up @@ -159,6 +158,7 @@ fun ComposeViewport(
val canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.setAttribute("tabindex", "0")
canvas.setAttribute("role", "generic")
canvas.setAttribute("draggable", "true")
canvas.style.outline = "none" // Fixes https://youtrack.jetbrains.com/issue/CMP-9040
canvas.style.setProperty("touch-action", "none") //blocks default browser touch handling
appContainer.appendChild(canvas)
Expand Down Expand Up @@ -197,4 +197,14 @@ fun ComposeViewport(
configuration = configuration,
state = DefaultWindowState(viewportContainer)
)
}
}

private fun clearNodeChildren(node: Element): Unit =
//language=JavaScript
js(
"""
{
if (node.hasChildNodes()) node.replaceChildren();
}
"""
)
Loading