diff --git a/compose/ui/ui/api/ui.klib.api b/compose/ui/ui/api/ui.klib.api index 8bd7deddca291..06b72e3d56185 100644 --- a/compose/ui/ui/api/ui.klib.api +++ b/compose/ui/ui/api/ui.klib.api @@ -5177,6 +5177,9 @@ final val androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitI // Targets: [ios] final val androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop // androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop|#static{}androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop[0] +// Targets: [ios] +final val androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop // androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop|#static{}androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop[0] + // Targets: [ios] final val androidx.compose.ui.window/androidx_compose_ui_window_ComposeSceneKeyboardOffsetManager$stableprop // androidx.compose.ui.window/androidx_compose_ui_window_ComposeSceneKeyboardOffsetManager$stableprop|#static{}androidx_compose_ui_window_ComposeSceneKeyboardOffsetManager$stableprop[0] @@ -5372,6 +5375,9 @@ final fun androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitI // Targets: [ios] final fun androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop_getter(): kotlin/Int // androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop_getter|androidx_compose_ui_viewinterop_UIKitInteropViewHolder$stableprop_getter(){}[0] +// Targets: [ios] +final fun androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop_getter(): kotlin/Int // androidx.compose.ui.viewinterop/androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop_getter|androidx_compose_ui_viewinterop_UIKitInteropViewMeasurePolicy$stableprop_getter(){}[0] + // Targets: [ios] final fun androidx.compose.ui.window/ComposeUIViewController(kotlin/Function1 = ..., kotlin/Function2): platform.UIKit/UIViewController // androidx.compose.ui.window/ComposeUIViewController|ComposeUIViewController(kotlin.Function1;kotlin.Function2){}[0] diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/viewinterop/SwingInteropViewHolder.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/viewinterop/SwingInteropViewHolder.desktop.kt index c2cf0a1bfed17..d6891c45fd597 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/viewinterop/SwingInteropViewHolder.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/viewinterop/SwingInteropViewHolder.desktop.kt @@ -48,13 +48,12 @@ internal class SwingInteropViewHolder( override val group: SwingInteropViewGroup, focusSwitcher: InteropFocusSwitcher, compositeKeyHashCode: CompositeKeyHashCode, - measurePolicy: MeasurePolicy + override val measurePolicy: MeasurePolicy, ) : TypedInteropViewHolder( factory = factory, interopContainer = container, group = group, compositeKeyHashCode = compositeKeyHashCode, - measurePolicy = measurePolicy ), ClipRectangle { private var clipBounds: IntRect? = null diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropView.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropView.skiko.kt index c923fd8053b17..bba85eeedf97c 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropView.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropView.skiko.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentCompositeKeyHashCode import androidx.compose.ui.Modifier import androidx.compose.ui.UiComposable -import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.materialize import androidx.compose.ui.node.ComposeUiNode.Companion.SetCompositeKeyHash import androidx.compose.ui.node.ComposeUiNode.Companion.SetResolvedCompositionLocals @@ -47,12 +46,10 @@ internal abstract class TypedInteropViewHolder( interopContainer: InteropContainer, group: InteropViewGroup, compositeKeyHashCode: CompositeKeyHashCode, - measurePolicy: MeasurePolicy ) : InteropViewHolder( interopContainer, group, compositeKeyHashCode, - measurePolicy ) { @Suppress("INAPPLICABLE_JVM_NAME") @get:JvmName("interopView") diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropViewHolder.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropViewHolder.skiko.kt index 701aaadb60654..8c79119368d7b 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropViewHolder.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/viewinterop/InteropViewHolder.skiko.kt @@ -37,7 +37,6 @@ internal abstract class InteropViewHolder( val container: InteropContainer, open val group: InteropViewGroup, private val compositeKeyHashCode: CompositeKeyHashCode, - measurePolicy: MeasurePolicy ) : InteropViewFactoryHolder() { private var onModifierChanged: (() -> Unit)? = null @@ -63,6 +62,8 @@ internal abstract class InteropViewHolder( } } + protected abstract val measurePolicy: MeasurePolicy + private var hasUpdateBlock = false var update: () -> Unit = {} diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropInteractionModeTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropInteractionModeTest.kt new file mode 100644 index 0000000000000..550ad7ee5d697 --- /dev/null +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropInteractionModeTest.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2025 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.ui.interop + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.findNodeWithTag +import androidx.compose.ui.test.runUIKitInstrumentedTest +import androidx.compose.ui.test.utils.up +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.readValue +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UIButton +import platform.UIKit.UIEvent + +internal class InteropInteractionModeTest { + @Test + fun testUIButtonTapCooperativeDefault() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { + TouchReactingView( + onTouchBegin = { beganCount++ }, + onTouchEnd = { endedCount++ }) + }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties( + UIKitInteropInteractionMode.Cooperative() + ) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + + delay(UIKitInteropInteractionMode.Cooperative.DefaultDelayMillis + 50L) + + assertEquals(1, beganCount) + assertEquals(0, endedCount) + + touch.up() + + assertEquals(1, beganCount) + assertEquals(1, endedCount) + } + + @Test + fun testUIButtonCooperativeTouchUpBeforeDelay() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(delayMillis = 1000) + ) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + + delay(500) + + touch.up() + + assertEquals(1, beganCount) + assertEquals(1, endedCount) + } + + @Test + fun testUIButtonCooperativeTouchUpAfterDelay() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.Cooperative(delayMillis = 800) + ) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + + delay(500) + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + + delay(350) + + assertEquals(1, beganCount) + assertEquals(0, endedCount) + + touch.up() + + assertEquals(1, beganCount) + assertEquals(1, endedCount) + } + + @Test + fun testUIButtonNonInteractive() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties(null) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + + touch.up() + + assertEquals(0, beganCount) + assertEquals(0, endedCount) + } + + @Test + fun testUIButtonTapNonCooperative() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties(UIKitInteropInteractionMode.NonCooperative) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(1, beganCount) + assertEquals(0, endedCount) + + touch.up() + + assertEquals(1,beganCount) + assertEquals(1, endedCount) + } + + @Test + fun testUIButtonLongTapNonCooperative() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties(UIKitInteropInteractionMode.NonCooperative) + ) + } + + val touch = findNodeWithTag("Button") + .touchDown() + + assertEquals(1, beganCount) + assertEquals(0, endedCount) + + delay(500) + + touch.up() + + assertEquals(1,beganCount) + assertEquals(1, endedCount) + } + + @Test + fun testUIButtonDoubleTapNonCooperative() = runUIKitInstrumentedTest { + var beganCount = 0 + var endedCount = 0 + + setContent { + UIKitView( + factory = { TouchReactingView(onTouchBegin = { beganCount++ }, onTouchEnd = { endedCount++ }) }, + modifier = Modifier.fillMaxWidth().height(50.dp).testTag("Button"), + properties = UIKitInteropProperties(UIKitInteropInteractionMode.NonCooperative) + ) + } + + findNodeWithTag("Button") + .doubleTap() + + assertEquals(2, beganCount) + assertEquals(2, endedCount) + } +} + +@OptIn(ExperimentalForeignApi::class) +private class TouchReactingView( + val onTouchBegin: () -> Unit, + val onTouchEnd: () -> Unit, +): UIButton(frame = CGRectZero.readValue()) { + override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) { + super.touchesBegan(touches, withEvent) + onTouchBegin() + } + + override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) { + super.touchesEnded(touches, withEvent) + onTouchEnd() + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithConstraintsTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithConstraintsTest.kt new file mode 100644 index 0000000000000..ab8aaec339f4c --- /dev/null +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithConstraintsTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2025 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.ui.interop + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.test.runUIKitInstrumentedTest +import androidx.compose.ui.test.utils.DpRectZero +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toDpRect +import androidx.compose.ui.viewinterop.UIKitView +import kotlin.test.Test +import kotlin.test.assertEquals +import platform.UIKit.NSLayoutConstraint +import platform.UIKit.UIView + +class InteropUIKitViewSizingWithConstraintsTest { + @Test + fun testFixedSizeConstraints() = runUIKitInstrumentedTest { + var rect = DpRectZero() + + setContent { + UIKitView( + factory = { + UIView().apply { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activateConstraints( + listOf( + widthAnchor.constraintEqualToConstant(80.0), + heightAnchor.constraintEqualToConstant(100.0) + ) + ) + } + }, + modifier = Modifier.onGloballyPositioned { rect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(DpRect(0.dp, 0.dp, 80.dp, 100.dp), rect) + } + + @Test + fun testFixedSizeConstraintsOverrideByCompose() = runUIKitInstrumentedTest { + var rect = DpRectZero() + + setContent { + UIKitView( + factory = { + UIView().apply { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activateConstraints( + listOf( + widthAnchor.constraintEqualToConstant(80.0), + heightAnchor.constraintEqualToConstant(100.0) + ) + ) + } + }, + modifier = Modifier + .size(width = 100.dp, height = 200.dp) + .onGloballyPositioned { rect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(DpRect(0.dp, 0.dp, 100.dp, 200.dp), rect) + } + + @Test + fun testUnconstrained() = runUIKitInstrumentedTest { + var composeRect = DpRectZero() + var uiKitRect = DpRectZero() + + val showCompose = mutableStateOf(false) + + setContent { + if (showCompose.value) { + Box( + modifier = Modifier + .onGloballyPositioned { composeRect = it.boundsInRoot().toDpRect(density) } + ) + } else { + UIKitView( + factory = { + UIView() + }, + modifier = Modifier + .onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + showCompose.value = true + + waitForIdle() + + assertEquals(composeRect, uiKitRect) + assertEquals(DpRectZero(), uiKitRect) + } + + @Test + fun testUnconstrainedFill() = runUIKitInstrumentedTest { + var composeRect = DpRectZero() + var uiKitRect = DpRectZero() + + val showCompose = mutableStateOf(false) + + setContent { + if (showCompose.value) { + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { composeRect = it.boundsInRoot().toDpRect(density) } + ) + } else { + UIKitView( + factory = { + UIView() + }, + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + showCompose.value = true + + waitForIdle() + + assertEquals(composeRect, uiKitRect) + assertEquals(DpRect(origin = DpOffset(0.dp, 0.dp), size = screenSize), uiKitRect) + } + + @Test + fun testUnconstrainedSize() = runUIKitInstrumentedTest { + var rect = DpRectZero() + + setContent { + UIKitView( + factory = { + UIView() + }, + modifier = Modifier + .size(100.dp, 200.dp) + .onGloballyPositioned { rect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(DpRect(origin = DpOffset(0.dp, 0.dp), size = DpSize(width = 100.dp, height = 200.dp)), rect) + } + + @Test + fun testUIKitViewHeightLargerThanScreenHeight() = runUIKitInstrumentedTest { + var uiKitRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UIView().apply { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activateConstraints( + listOf( + widthAnchor.constraintEqualToConstant(100.0), + heightAnchor.constraintEqualToConstant(screenSize.height.value.toDouble() + 100.0) + ) + ) + } + }, + modifier = Modifier.onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(DpRect(top = 0.dp, left = 0.dp, right = 100.dp, bottom = screenSize.height), uiKitRect) + } + + @Test + fun testUIKitViewWidthLargerThanScreenWidth() = runUIKitInstrumentedTest { + var uiKitRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UIView().apply { + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activateConstraints( + listOf( + widthAnchor.constraintEqualToConstant(screenSize.width.value.toDouble() + 100.0), + heightAnchor.constraintEqualToConstant(100.0) + ) + ) + } + }, + modifier = Modifier.onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(DpRect(top = 0.dp, left = 0.dp, right = screenSize.width, bottom = 100.dp), uiKitRect) + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithUILabelTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithUILabelTest.kt new file mode 100644 index 0000000000000..60f63065ef692 --- /dev/null +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIKitViewSizingWithUILabelTest.kt @@ -0,0 +1,435 @@ +/* + * Copyright 2025 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.ui.interop + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.test.runUIKitInstrumentedTest +import androidx.compose.ui.test.utils.DpRectZero +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.asDpRect +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.height +import androidx.compose.ui.unit.size +import androidx.compose.ui.unit.toDpRect +import androidx.compose.ui.unit.width +import androidx.compose.ui.viewinterop.UIKitView +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.UIKit.UILabel + +class InteropUIKitViewSizingWithUILabelTest { + private val SHORT_TEXT: String = "TEXT" + private val LONG_TEXT: String = List(100) { "TEXT" }.joinToString(" ") + + @Test + @OptIn(ExperimentalForeignApi::class) + fun testUILabelFrameMatchesInteropBoundsShortTextSingleLine() = runUIKitInstrumentedTest { + var interopRect = DpRectZero() + var uiLabelRect: () -> DpRect = { DpRectZero() } + + setContent { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + uiLabelRect = { frame.useContents { asDpRect() } } + } + }, + modifier = Modifier.onGloballyPositioned { interopRect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(interopRect, uiLabelRect()) + } + + @Test + @OptIn(ExperimentalForeignApi::class) + fun testUILabelFrameMatchesInteropBoundsShortTextMultiLine() = runUIKitInstrumentedTest { + var interopRect = DpRectZero() + var uiLabelRect: () -> DpRect = { DpRectZero() } + + setContent { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = SHORT_TEXT + uiLabelRect = { frame.useContents { asDpRect() } } + } + }, + modifier = Modifier.onGloballyPositioned { interopRect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(interopRect, uiLabelRect()) + } + + @Test + @OptIn(ExperimentalForeignApi::class) + fun testUILabelFrameMatchesInteropBoundsLongTextSingleLine() = runUIKitInstrumentedTest { + var interopRect = DpRectZero() + var uiLabelRect: () -> DpRect = { DpRectZero() } + + setContent { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = LONG_TEXT + uiLabelRect = { frame.useContents { asDpRect() } } + } + }, + modifier = Modifier.onGloballyPositioned { interopRect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(interopRect, uiLabelRect()) + } + + @Test + @OptIn(ExperimentalForeignApi::class) + fun testUILabelFrameMatchesInteropBoundsLongTextMultiLine() = runUIKitInstrumentedTest { + var interopRect = DpRectZero() + var uiLabelRect: () -> DpRect = { DpRectZero() } + + setContent { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + uiLabelRect = { frame.useContents { asDpRect() } } + } + }, + modifier = Modifier.onGloballyPositioned { interopRect = it.boundsInRoot().toDpRect(density) } + ) + } + + assertEquals(interopRect, uiLabelRect()) + } + + @Test + fun testUILabelShortText() = runUIKitInstrumentedTest { + var singleLineLabel = DpRectZero() + var multiLineLabel = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { singleLineLabel = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = SHORT_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { multiLineLabel = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertTrue(multiLineLabel.width > 0.dp && multiLineLabel.width < screenSize.width) + assertTrue(multiLineLabel.height > 0.dp) + assertEquals(singleLineLabel.height, multiLineLabel.height) + assertEquals(singleLineLabel.width, multiLineLabel.width) + assertEquals(singleLineLabel.left, multiLineLabel.left) + assertEquals(singleLineLabel.bottom, multiLineLabel.top) + } + + @Test + fun testUILabelLongText() = runUIKitInstrumentedTest { + var singleLineText = DpRectZero() + var multiLineText = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { singleLineText = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { multiLineText = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(screenSize.width, singleLineText.width) + assertTrue(multiLineText.width <= singleLineText.width) + assertTrue(singleLineText.height < multiLineText.height) + } + + @Test + fun testUILabelSingleLineShortTextFixedWidth() = runUIKitInstrumentedTest { + var referenceViewRect = DpRectZero() + var fixedWidthViewRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { referenceViewRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier + .width(200.dp) + .onGloballyPositioned { fixedWidthViewRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(200.dp, fixedWidthViewRect.width) + assertEquals(referenceViewRect.height, fixedWidthViewRect.height) + } + + @Test + fun testUILabelSingleLineShortTextFixedHeight() = runUIKitInstrumentedTest { + var referenceViewRect = DpRectZero() + var fixedWidthViewRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { referenceViewRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier + .height(200.dp) + .onGloballyPositioned { fixedWidthViewRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(200.dp, fixedWidthViewRect.height) + assertEquals(referenceViewRect.width, fixedWidthViewRect.width) + } + + @Test + fun testUILabelSingleLineShortTextFixedSize() = runUIKitInstrumentedTest { + var referenceViewRect = DpRectZero() + var fixedSizeViewRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { referenceViewRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 1 + text = SHORT_TEXT + } + }, + modifier = Modifier + .size(width = 200.dp, height = 400.dp) + .onGloballyPositioned { fixedSizeViewRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(DpSize(width = 200.dp, height = 400.dp), fixedSizeViewRect.size) + assertNotEquals(referenceViewRect.size, fixedSizeViewRect.size) + } + + @Test + fun testUILabelMultiLineLongTextFixedWidth() = runUIKitInstrumentedTest { + var unboundedTextRect = DpRectZero() + var boundedWidthTextRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { unboundedTextRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + } + }, + modifier = Modifier + .width(width = 200.dp) + .onGloballyPositioned { boundedWidthTextRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(200.dp, boundedWidthTextRect.width) + assertTrue(boundedWidthTextRect.width <= unboundedTextRect.width) + assertTrue(boundedWidthTextRect.height > unboundedTextRect.height) + } + + @Test + fun testUILabelMultiLineLongTextFixedHeight() = runUIKitInstrumentedTest { + var unboundedTextRect = DpRectZero() + var boundedHeightTextRect = DpRectZero() + + setContent { + Column { + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { unboundedTextRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + numberOfLines = 0 + text = LONG_TEXT + } + }, + modifier = Modifier + .height(100.dp) + .onGloballyPositioned { boundedHeightTextRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(100.dp, boundedHeightTextRect.height) + assertEquals(unboundedTextRect.width, boundedHeightTextRect.width, ) + assertTrue(boundedHeightTextRect.height < unboundedTextRect.height) + } + + @Test + fun testUILabelAndTextInRow() = runUIKitInstrumentedTest { + var composeRect = DpRectZero() + var uiKitRect = DpRectZero() + + setContent { + Row { + Text( + SHORT_TEXT, + modifier = Modifier.onGloballyPositioned { composeRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(composeRect.right, uiKitRect.left) + assertEquals(composeRect.top, uiKitRect.top) + assertEquals(uiKitRect.right, screenSize.width) + assertTrue(uiKitRect.height > 0.dp) + } + + @Test + fun testUILabelAndTextInColumn() = runUIKitInstrumentedTest { + var composeRect = DpRectZero() + var uiKitRect = DpRectZero() + + setContent { + Column { + Text( + SHORT_TEXT, + modifier = Modifier.onGloballyPositioned { composeRect = it.boundsInRoot().toDpRect(density) } + ) + UIKitView( + factory = { + UILabel().apply { + text = LONG_TEXT + } + }, + modifier = Modifier.onGloballyPositioned { uiKitRect = it.boundsInRoot().toDpRect(density) } + ) + } + } + + assertEquals(composeRect.left, uiKitRect.left) + assertEquals(composeRect.bottom, uiKitRect.top) + assertEquals(uiKitRect.right, screenSize.width) + assertTrue(uiKitRect.bottom > composeRect.bottom) + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIMenuTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIMenuTest.kt new file mode 100644 index 0000000000000..3cf4d1897f5ca --- /dev/null +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interop/InteropUIMenuTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2025 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.ui.interop + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.findNodeWithLabel +import androidx.compose.ui.test.findNodeWithTag +import androidx.compose.ui.test.runUIKitInstrumentedTest +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitView +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.readValue +import platform.CoreGraphics.CGPoint +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UIAction +import platform.UIKit.UIButton +import platform.UIKit.UIColor +import platform.UIKit.UIContextMenuConfiguration +import platform.UIKit.UIContextMenuInteraction +import platform.UIKit.UIContextMenuInteractionAnimatingProtocol +import platform.UIKit.UIContextMenuInteractionDelegateProtocol +import platform.UIKit.UIMenu + +internal class InteropUIMenuTest { + + @Test + fun testUIMenuDismissByTapOnComposeView() = runUIKitInstrumentedTest { + var isMenuOpen: () -> Boolean = { false } + + setContent { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .background(Color.Green) + .testTag("Box") + ) + UIKitView( + factory = { + val button = ContextMenuButton() + isMenuOpen = { button.isMenuOpen } + button + }, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .testTag("MenuButton"), + ) + } + } + + findNodeWithTag("MenuButton") + .tap() + + delay(800) + + assertTrue(isMenuOpen()) + + findNodeWithTag("Box") + .tap() + + delay(800) + + assertFalse(isMenuOpen()) + } + + @Test + fun testUIMenuEmbeddedInComposeViewDismissByTapOnOtherComposeView() = runUIKitInstrumentedTest { + var isMenuOpen: () -> Boolean = { false } + + setContent { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .background(Color.Green) + .testTag("Box") + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + contentAlignment = Alignment.Center + ) { + UIKitView( + factory = { + val button = ContextMenuButton() + isMenuOpen = { button.isMenuOpen } + button + }, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .testTag("MenuButton"), + ) + } + } + } + + findNodeWithTag("MenuButton") + .tap() + + delay(800) + + assertTrue(isMenuOpen()) + + findNodeWithTag("Box") + .tap() + + delay(800) + + assertFalse(isMenuOpen()) + } + + @Test + fun testUIMenuDismissByTapOnUIButton() = runUIKitInstrumentedTest { + var isMenuOpen: () -> Boolean = { false } + + setContent { + Column { + UIKitView( + factory = { + val button = UIButton() + button.backgroundColor = UIColor.yellowColor + button + }, + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .testTag("UIButton") + ) + UIKitView( + factory = { + val button = ContextMenuButton() + isMenuOpen = { button.isMenuOpen } + button + }, + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .testTag("MenuButton"), + ) + } + } + + findNodeWithTag("MenuButton") + .tap() + + delay(800) + + assertTrue(isMenuOpen()) + + findNodeWithTag("UIButton") + .tap() + + delay(800) + + assertFalse(isMenuOpen()) + } + + @Test + fun testUIMenuDismissByTapOnUIAction() = runUIKitInstrumentedTest { + var isMenuOpen: () -> Boolean = { false } + + setContent { + UIKitView( + factory = { + val button = ContextMenuButton() + isMenuOpen = { button.isMenuOpen } + button + }, + modifier = Modifier.fillMaxWidth().height(400.dp).testTag("MenuButton"), + ) + } + + findNodeWithTag("MenuButton") + .tap() + + delay(800) + + assertTrue(isMenuOpen()) + + findNodeWithLabel("MenuItem2") + .tap() + + delay(800) + + assertFalse(isMenuOpen()) + } +} + +@OptIn(ExperimentalForeignApi::class) +private class ContextMenuButton( + var isMenuOpen: Boolean = false, +): UIButton(frame = CGRectZero.readValue()), UIContextMenuInteractionDelegateProtocol { + init { + backgroundColor = UIColor.redColor + showsMenuAsPrimaryAction = true + menu = UIMenu() + } + + override fun contextMenuInteraction( + interaction: UIContextMenuInteraction, + configurationForMenuAtLocation: CValue + ): UIContextMenuConfiguration? { + isMenuOpen = true + return UIContextMenuConfiguration.configurationWithIdentifier( + identifier = null, + previewProvider = null, + actionProvider = { + UIMenu.menuWithChildren( + listOf( + UIAction.actionWithTitle("MenuItem1", null, null) {}, + UIAction.actionWithTitle("MenuItem2", null, null) {}, + UIAction.actionWithTitle("MenuItem3", null, null) {}, + ) + ) + } + ) + } + + override fun contextMenuInteraction( + interaction: UIContextMenuInteraction, + willEndForConfiguration: UIContextMenuConfiguration, + animator: UIContextMenuInteractionAnimatingProtocol? + ) { + isMenuOpen = false + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropElementHolder.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropElementHolder.uikit.kt index f9733d3334ab8..2a467769ead2f 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropElementHolder.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropElementHolder.uikit.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.asCGRect @@ -42,20 +41,12 @@ internal abstract class UIKitInteropElementHolder( interopContainer: InteropContainer, private val interopWrappingView: InteropWrappingView, properties: UIKitInteropProperties, - compositeKeyHashCode: CompositeKeyHashCode + compositeKeyHashCode: CompositeKeyHashCode, ) : TypedInteropViewHolder( factory = factory, interopContainer = interopContainer, group = interopWrappingView, compositeKeyHashCode = compositeKeyHashCode, - measurePolicy = MeasurePolicy { _, constraints -> - layout(constraints.minWidth, constraints.minHeight) { - // No-op, no children are expected - // TODO: attempt to calculate the size of the wrapped view using constraints - // and autolayout system if possible - // https://youtrack.jetbrains.com/issue/CMP-5873/iOS-investigate-intrinsic-sizing-of-interop-elements - } - } ) { constructor( factory: () -> T, diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewControllerHolder.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewControllerHolder.uikit.kt index 31b697656c96b..1a05114a52e2d 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewControllerHolder.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewControllerHolder.uikit.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.viewinterop import androidx.compose.runtime.CompositeKeyHashCode +import androidx.compose.ui.layout.MeasurePolicy import kotlinx.cinterop.CValue import platform.CoreGraphics.CGRect import platform.UIKit.UIViewController @@ -35,13 +36,15 @@ internal class UIKitInteropViewControllerHolder( factory, interopContainer, properties, - compositeKeyHashCode + compositeKeyHashCode, ) { init { // Group will be placed to hierarchy in [InteropContainer.placeInteropView] group.addSubview(interopView.view) } + override val measurePolicy: MeasurePolicy = UIKitInteropViewMeasurePolicy(interopView.view) + override var userComponentCGRect: CValue get() = interopView.view.frame set(value) { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewHolder.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewHolder.uikit.kt index 7ab5deac9d8ce..dcb29f5db7c18 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewHolder.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewHolder.uikit.kt @@ -17,6 +17,7 @@ package androidx.compose.ui.viewinterop import androidx.compose.runtime.CompositeKeyHashCode +import androidx.compose.ui.layout.MeasurePolicy import kotlinx.cinterop.CValue import platform.CoreGraphics.CGRect import platform.UIKit.UIView @@ -30,13 +31,15 @@ internal class UIKitInteropViewHolder( factory, interopContainer, properties, - compositeKeyHashCode + compositeKeyHashCode, ) { init { // Group will be placed to hierarchy in [InteropContainer.placeInteropView] group.addSubview(interopView) } + override val measurePolicy: MeasurePolicy = UIKitInteropViewMeasurePolicy(interopView) + override var userComponentCGRect: CValue get() = interopView.frame set(value) { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewMeasurePolicy.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewMeasurePolicy.uikit.kt new file mode 100644 index 0000000000000..66d4f9a4291b0 --- /dev/null +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropViewMeasurePolicy.uikit.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 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.ui.viewinterop + +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.asCGSize +import androidx.compose.ui.unit.dp +import kotlinx.cinterop.useContents +import platform.UIKit.UILayoutPriorityFittingSizeLevel +import platform.UIKit.UIView + +internal class UIKitInteropViewMeasurePolicy( + val interopView: T +): MeasurePolicy { + override fun MeasureScope.measure( + measurables: List, + constraints: Constraints + ): MeasureResult { + val measuredSize by lazy { + val targetSize = DpSize( + constraints.maxWidth.toDp(), + constraints.maxHeight.toDp() + ).asCGSize() + + interopView.systemLayoutSizeFittingSize( + targetSize, + withHorizontalFittingPriority = UILayoutPriorityFittingSizeLevel, + verticalFittingPriority = UILayoutPriorityFittingSizeLevel + ) + } + + val width = if (constraints.hasFixedWidth) { + constraints.minWidth + } else { + measuredSize.useContents { width.dp.roundToPx() } + .coerceIn(constraints.minWidth, constraints.maxWidth) + } + + val height = if (constraints.hasFixedHeight) { + constraints.minHeight + } else { + measuredSize.useContents { height.dp.roundToPx() } + .coerceIn(constraints.minHeight, constraints.maxHeight) + } + + return layout(width, height) { + // No-op, no children are expected + } + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/viewinterop/WebInteropElementHolder.wasmJs.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/viewinterop/WebInteropElementHolder.wasmJs.kt index ae44208e65168..90a22533f40e7 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/viewinterop/WebInteropElementHolder.wasmJs.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/viewinterop/WebInteropElementHolder.wasmJs.kt @@ -34,17 +34,12 @@ internal abstract class WebInteropElementHolder( factory: () -> T, interopContainer: InteropContainer, private val interopWrapper: HTMLElement, - compositeKeyHashCode: CompositeKeyHashCode + compositeKeyHashCode: CompositeKeyHashCode, ) : TypedInteropViewHolder( factory = factory, interopContainer = interopContainer, group = InteropViewGroup(interopWrapper), compositeKeyHashCode = compositeKeyHashCode, - measurePolicy = MeasurePolicy { _, constraints -> - layout(constraints.minWidth, constraints.minHeight) { - // No-op, no children are expected - } - } ) { constructor( factory: () -> T, @@ -70,6 +65,12 @@ internal abstract class WebInteropElementHolder( protected abstract var userComponentRect: String + override val measurePolicy: MeasurePolicy = MeasurePolicy { _, constraints -> + layout(constraints.minWidth, constraints.minHeight) { + // No-op, no children are expected + } + } + private fun Rect.round(density: Density): IntRect { val left = floor(left / density.density).toInt() val top = floor(top / density.density).toInt()