Skip to content

Commit

Permalink
Fix render order of interop views (#1145)
Browse files Browse the repository at this point in the history
## Proposed Changes

Currently on both Desktop and iOS interop views are added to the view
hierarchy in order to add nodes to Compose. It works only if all
intersecting interop views were added at the same time (frame). So it's
basically last-added - above-displayed. This PR changes this behavior in
a way that it will respect the order inside Compose like regular compose
elements.

**It does NOT make any changes in the ability to display Compose content
above interop view on Desktop**, this fix was made in #915

Main changes:
- Unify a way to work with interop on Desktop (`SwingPanel`) and iOS
(`UIKitView`)
- `LocalInteropContainer` -> `LocalUIKitInteropContainer` on iOS
- `LocalLayerContainer` -> `LocalSwingInteropContainer` on Desktop
- Reduce copy-pasting by moving `OverlayLayout` and `EmptyLayout`
- Remove overriding `add` method on `ComposePanel` and
`ComposeWindowPanel` - it was required to redirect interop, but now it's
not required and it's better to avoid changing default AWT hierarchy
behaviour
- Do not use `JLayeredPane`'s layers anymore - it brings a lot of
transparency issues (faced with it on Windows too after unrelated
change). Sorting via indexes is used instead
- Add `InteropOrder` page to mpp demo

### How it works

It utilizes `TraversableNode` to traverse the tree in the right order
and calculate the index based on interop views count that placed before
the current node in the hierarchy. All interop nodes are marked via
`Modifier.trackSwingInterop`/`Modifier.trackUIKitInterop` modifier to
filter them from the `LayoutNode`s tree.

## Testing

Test: run reproducers from the issues or look at "InteropOrder" page in
mpp demo

Desktop | iOS
--- | ---
<img width="400" alt="Screenshot 2024-02-27 at 12 51 06"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/534cbdc8-9671-4ab7-bd6d-b577d2004d1b">
| <img width="300" alt="Simulator Screenshot - iPhone 15 Pro -
2024-02-27 at 12 49 50"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/ac7553db-c2a4-4c4a-a270-5d6dbf82fb79">


## Issues Fixed

### Desktop

Fixes JetBrains/compose-multiplatform#2926
Fixes
JetBrains/compose-multiplatform#1521 (comment)

### iOS

Fixes JetBrains/compose-multiplatform#4004
Fixes JetBrains/compose-multiplatform#3848
  • Loading branch information
MatkovIvan committed Feb 29, 2024
1 parent d680c84 commit 339dbd3
Show file tree
Hide file tree
Showing 28 changed files with 716 additions and 309 deletions.
14 changes: 7 additions & 7 deletions compose/mpp/demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ kotlin {
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-graphics"))
implementation(project(":compose:ui:ui-text"))
implementation(libs.kotlinStdlib)
implementation(libs.kotlinCoroutinesCore)
api(libs.kotlinSerializationCore)
}
Expand All @@ -181,19 +182,18 @@ kotlin {
}
}

val jsMain by getting {
val webMain by creating {
dependsOn(skikoMain)
resources.setSrcDirs(resources.srcDirs)
resources.srcDirs(unzipTask.map { it.destinationDir })
}

val jsMain by getting {
dependsOn(webMain)
}

val wasmJsMain by getting {
dependsOn(skikoMain)
resources.setSrcDirs(resources.srcDirs)
resources.srcDirs(unzipTask.map { it.destinationDir })
dependencies {
implementation(libs.kotlinStdlib)
}
dependsOn(webMain)
}

val nativeMain by creating { dependsOn(skikoMain) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ val MainScreen = Screen.Selection(
Screen.Example("GraphicsLayerSettings") { GraphicsLayerSettings() },
Screen.Example("Blending") { Blending() },
Screen.Example("FontRasterization") { FontRasterization() },
Screen.Example("InteropOrder") { InteropOrder() },
AndroidTextFieldSamples,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 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.mpp.demo

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun InteropOrder() {
var red by remember { mutableStateOf(true) }
var green by remember { mutableStateOf(true) }
var blue by remember { mutableStateOf(true) }
Column(
modifier = Modifier.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(onClick = { red = !red }) {
Text("Red")
}
Button(onClick = { green = !green }) {
Text("Green")
}
Button(onClick = { blue = !blue }) {
Text("Blue")
}

Box {
if (red) {
TestInteropView(Modifier.size(150.dp).offset(0.dp, 0.dp), Color.Red)
}
if (green) {
TestInteropView(Modifier.size(150.dp).offset(75.dp, 75.dp), Color.Green)
}
if (blue) {
TestInteropView(Modifier.size(150.dp).offset(150.dp, 150.dp), Color.Blue)
}
}
}
}

@Composable
internal expect fun TestInteropView(modifier: Modifier, color: Color)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.mpp.demo

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color
import javax.swing.JPanel

@Composable
internal actual fun TestInteropView(modifier: Modifier, color: Color) {
SwingPanel(
background = color,
factory = { JPanel().apply { background = color.toAwtColor() } },
modifier = modifier
)
}

private fun Color.toAwtColor() = java.awt.Color(red, green, blue, alpha)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 The Android Open Source Project
* Copyright 2024 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.
Expand All @@ -14,11 +14,15 @@
* limitations under the License.
*/

package androidx.compose.ui.awt
package androidx.compose.mpp.demo

import androidx.compose.runtime.staticCompositionLocalOf
import java.awt.Container
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

internal val LocalLayerContainer = staticCompositionLocalOf<Container> {
error("CompositionLocal LayerContainer not provided")
@Composable
internal actual fun TestInteropView(modifier: Modifier, color: Color) {
Box(modifier.background(color)) // TODO
}
52 changes: 0 additions & 52 deletions compose/mpp/demo/src/uikitMain/kotlin/UIKitViewOrder.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2024 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.mpp.demo

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.interop.UIKitView
import platform.UIKit.UIColor
import platform.UIKit.UIView

@Composable
internal actual fun TestInteropView(modifier: Modifier, color: Color) {
UIKitView(
factory = { UIView().apply { backgroundColor = color.toUIColor() } },
modifier = modifier,
)
}

private fun Color.toUIColor() = UIColor(
red = red.toDouble(),
green = green.toDouble(),
blue = blue.toDouble(),
alpha = alpha.toDouble(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@ package androidx.compose.mpp.demo

import NativeModalWithNaviationExample
import SwiftUIInteropExample
import UIKitViewOrder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.main.defaultUIKitMain
import androidx.compose.ui.platform.AccessibilityDebugLogger
import androidx.compose.ui.platform.AccessibilitySyncOptions
import androidx.compose.ui.window.ComposeUIViewController
import bugs.IosBugs
import bugs.ProperContainmentDisposal
import bugs.ComposeAndNativeScroll
import bugs.StartRecompositionCheck
import platform.UIKit.UIViewController


@OptIn(ExperimentalComposeApi::class, ExperimentalComposeUiApi::class)
fun main(vararg args: String) {
androidx.compose.ui.util.enableTraceOSLog()
Expand Down Expand Up @@ -47,9 +42,6 @@ fun IosDemo(arg: String, makeHostingController: ((Int) -> UIViewController)? = n
extraScreens = listOf(
IosBugs,
NativeModalWithNaviationExample,
UIKitViewOrder,
ProperContainmentDisposal,
ComposeAndNativeScroll
) + listOf(makeHostingController).mapNotNull {
it?.let {
SwiftUIInteropExample(it)
Expand Down
2 changes: 2 additions & 0 deletions compose/mpp/demo/src/uikitMain/kotlin/bugs/IosBugs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ val IosBugs = Screen.Selection(
BackspaceIssue,
DropdownMenuIssue,
KeyboardIMEActionPopup,
ProperContainmentDisposal,
ComposeAndNativeScroll
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Android Open Source Project
* Copyright 2024 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.
Expand All @@ -14,11 +14,15 @@
* limitations under the License.
*/

package androidx.compose.ui.interop
package androidx.compose.mpp.demo

import androidx.compose.runtime.staticCompositionLocalOf
import platform.UIKit.UIView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

internal val LocalInteropContainer = staticCompositionLocalOf<UIView> {
error("CompositionLocal LayerContainer not provided")
@Composable
internal actual fun TestInteropView(modifier: Modifier, color: Color) {
Box(modifier.background(color)) // TODO
}
7 changes: 0 additions & 7 deletions compose/ui/ui/api/desktop/ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -414,13 +414,6 @@ public final class androidx/compose/ui/awt/AwtEvents_desktopKt {
public static final fun getAwtEventOrNull-ZmokQxo (Ljava/lang/Object;)Ljava/awt/event/KeyEvent;
}

public final class androidx/compose/ui/awt/ComposableSingletons$SwingPanel_desktopKt {
public static final field INSTANCE Landroidx/compose/ui/awt/ComposableSingletons$SwingPanel_desktopKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$ui ()Lkotlin/jvm/functions/Function2;
}

public final class androidx/compose/ui/awt/ComposeDialog : javax/swing/JDialog {
public static final field $stable I
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ internal object ComposeFeatureFlags {
* Indicates whether interop blending is enabled.
* It allows drawing compose elements above interop and apply clip/shape modifiers to it.
*
* Note that it currently works only with Metal, DirectX and offscreen rendering.
* Known limitations:
* - Works only with Metal, DirectX and offscreen rendering
* - On DirectX, it cannot overlay another DirectX component (due to OS blending limitation)
* - On macOS, render and event dispatching order differs. It means that interop view might
* catch the mouse event even if visually it renders below Compose content
*/
val useInteropBlending: Boolean
get() = System.getProperty("compose.interop.blending").toBoolean()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
}

override fun add(component: Component): Component {
_composeContainer?.addToComponentLayer(component)
return component
return super.add(component)
}

override fun remove(component: Component) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,6 @@ internal class ComposeWindowPanel(
composeContainer.setBounds(0, 0, width, height)
}

override fun add(component: Component): Component {
composeContainer.addToComponentLayer(component)
return component
}

override fun getPreferredSize(): Dimension? = if (isPreferredSizeSet) {
super.getPreferredSize()
} else {
Expand Down

0 comments on commit 339dbd3

Please sign in to comment.