Skip to content

Commit

Permalink
Use z-sorted traverse in InteropContainer (#1340)
Browse files Browse the repository at this point in the history
Fixes JetBrains/compose-multiplatform#4485

## Testing

Use reproduction from the issue
This should be tested by QA

## Release Notes
### Fixes - Multiple Platforms
- Fix order of interop elements in some cases
  • Loading branch information
MatkovIvan committed May 27, 2024
1 parent d1f3898 commit 8a9057e
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.TrackInteropContainer
import androidx.compose.ui.node.TrackInteropModifierElement
import androidx.compose.ui.node.TrackInteropModifierNode
import androidx.compose.ui.node.countInteropComponentsBefore
import androidx.compose.ui.node.countInteropComponentsBelow
import androidx.compose.ui.scene.ComposeSceneMediator
import androidx.compose.ui.unit.IntRect
import java.awt.Component
Expand Down Expand Up @@ -54,7 +54,7 @@ internal class SwingInteropContainer(
private val placeInteropAbove: Boolean
): InteropContainer<InteropComponent> {
/**
* @see SwingInteropContainer.addInteropView
* @see SwingInteropContainer.placeInteropView
* @see SwingInteropContainer.removeInteropView
*/
private var interopComponents = mutableMapOf<Component, InteropComponent>()
Expand All @@ -63,17 +63,46 @@ internal class SwingInteropContainer(
override val interopViews: Set<InteropComponent>
get() = interopComponents.values.toSet()

override fun addInteropView(nativeView: InteropComponent) {
/**
* Index of last interop component in [container].
*
* [ComposeSceneMediator] might keep extra components in the same container.
* So based on [placeInteropAbove] they should go below or under all interop views.
*
* @see ComposeSceneMediator.contentComponent
* @see ComposeSceneMediator.invisibleComponent
*/
private val lastInteropIndex: Int
get() {
var lastInteropIndex = interopComponents.size - 1
if (!placeInteropAbove) {
val nonInteropComponents = container.componentCount - interopComponents.size
lastInteropIndex += nonInteropComponents
}
return lastInteropIndex
}

override fun placeInteropView(nativeView: InteropComponent) {
val component = nativeView.container
val nonInteropComponents = container.componentCount - interopComponents.size
// AWT uses the reverse order for drawing and events, so index = size - count
val index = interopComponents.size - countInteropComponentsBefore(nativeView)
interopComponents[component] = nativeView
container.add(component, if (placeInteropAbove) {
index

// Add this component to [interopComponents] to track count and clip rects
val alreadyAdded = component in interopComponents
if (!alreadyAdded) {
interopComponents[component] = nativeView
}

// Iterate through a Compose layout tree in draw order and count interop view below this one
val countBelow = countInteropComponentsBelow(nativeView)

// AWT/Swing uses the **REVERSE ORDER** for drawing and events
val awtIndex = lastInteropIndex - countBelow

// Update AWT/Swing hierarchy
if (alreadyAdded) {
container.setComponentZOrder(component, awtIndex)
} else {
index + nonInteropComponents
})
container.add(component, awtIndex)
}

// Sometimes Swing displays the rest of interop views in incorrect order after adding,
// so we need to force re-validate it.
Expand All @@ -92,6 +121,11 @@ internal class SwingInteropContainer(
container.repaint()
}

fun validateComponentsOrder() {
container.validate()
container.repaint()
}

fun getClipRectForComponent(component: Component): ClipRectangle =
requireNotNull(interopComponents[component])

Expand All @@ -113,8 +147,10 @@ internal class SwingInteropContainer(
* @param component The Swing component that matches the current node.
*/
internal fun Modifier.trackSwingInterop(
container: SwingInteropContainer,
component: InteropComponent
): Modifier = this then TrackInteropModifierElement(
container = container,
nativeView = component
)

Expand All @@ -126,7 +162,7 @@ internal fun Modifier.trackSwingInterop(
*/
internal open class InteropComponent(
val container: Container,
var clipBounds: IntRect? = null
protected var clipBounds: IntRect? = null
) : ClipRectangle {
override val x: Float
get() = (clipBounds?.left ?: container.x).toFloat()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,60 +89,43 @@ public fun <T : Component> SwingPanel(
) {
val interopContainer = LocalSwingInteropContainer.current
val compositeKey = currentCompositeKeyHash
val componentInfo = remember {
ComponentInfo<T>(
val interopComponent = remember {
SwingInteropComponent(
container = SwingPanelContainer(
key = compositeKey,
focusComponent = interopContainer.container,
)
),
update = update,
)
}

val density = LocalDensity.current
val focusManager = LocalFocusManager.current
val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) }
val focusSwitcher = remember { FocusSwitcher(interopComponent, focusManager) }

OverlayLayout(
modifier = modifier.onGloballyPositioned { coordinates ->
val rootCoordinates = coordinates.findRootCoordinates()
val clipedBounds = rootCoordinates
val clippedBounds = rootCoordinates
.localBoundingBoxOf(coordinates, clipBounds = true).round(density)
val bounds = rootCoordinates
.localBoundingBoxOf(coordinates, clipBounds = false).round(density)

// Take care about clipped bounds
componentInfo.clipBounds = clipedBounds // Clipping area for skia canvas
componentInfo.container.isVisible = !clipedBounds.isEmpty // Hide if it's fully clipped
// Swing clips children based on parent's bounds, so use our container for clipping
componentInfo.container.setBounds(
/* x = */ clipedBounds.left,
/* y = */ clipedBounds.top,
/* width = */ clipedBounds.width,
/* height = */ clipedBounds.height
)

// The real size and position should be based on not-clipped bounds
componentInfo.component.setBounds(
/* x = */ bounds.left - clipedBounds.left, // Local position relative to container
/* y = */ bounds.top - clipedBounds.top,
/* width = */ bounds.width,
/* height = */ bounds.height
)
componentInfo.container.validate()
componentInfo.container.repaint()
interopComponent.setBounds(bounds, clippedBounds)
interopContainer.validateComponentsOrder()
}.drawBehind {
// Clear interop area to make visible the component under our canvas.
drawRect(Color.Transparent, blendMode = BlendMode.Clear)
}.trackSwingInterop(componentInfo)
.then(InteropPointerInputModifier(componentInfo))
}.trackSwingInterop(interopContainer, interopComponent)
.then(InteropPointerInputModifier(interopComponent))
) {
focusSwitcher.Content()
}

DisposableEffect(Unit) {
val focusListener = object : FocusListener {
override fun focusGained(e: FocusEvent) {
if (componentInfo.container.isParentOf(e.oppositeComponent)) {
if (interopComponent.container.isParentOf(e.oppositeComponent)) {
when (e.cause) {
FocusEvent.Cause.TRAVERSAL_FORWARD -> focusSwitcher.moveForward()
FocusEvent.Cause.TRAVERSAL_BACKWARD -> focusSwitcher.moveBackward()
Expand All @@ -154,26 +137,22 @@ public fun <T : Component> SwingPanel(
override fun focusLost(e: FocusEvent) = Unit
}
interopContainer.container.addFocusListener(focusListener)
interopContainer.addInteropView(componentInfo)
onDispose {
interopContainer.removeInteropView(componentInfo)
interopContainer.removeInteropView(interopComponent)
interopContainer.container.removeFocusListener(focusListener)
}
}

DisposableEffect(factory) {
componentInfo.component = factory()
componentInfo.container.add(componentInfo.component)
componentInfo.updater = Updater(componentInfo.component, update)
interopComponent.setupUserComponent(factory())
onDispose {
componentInfo.container.remove(componentInfo.component)
componentInfo.updater.dispose()
interopComponent.cleanUserComponent()
}
}

SideEffect {
componentInfo.container.background = background.toAwtColor()
componentInfo.updater.update = update
interopComponent.container.background = background.toAwtColor()
interopComponent.update = update
}
}

Expand Down Expand Up @@ -212,7 +191,7 @@ private class SwingPanelContainer(
}

private class FocusSwitcher<T : Component>(
private val info: ComponentInfo<T>,
private val interopComponent: SwingInteropComponent<T>,
private val focusManager: FocusManager,
) {
private val backwardRequester = FocusRequester()
Expand Down Expand Up @@ -248,7 +227,8 @@ private class FocusSwitcher<T : Component>(
if (it.isFocused && !isRequesting) {
focusManager.clearFocus(force = true)

val component = info.container.focusTraversalPolicy.getFirstComponent(info.container)
val container = interopComponent.container
val component = container.focusTraversalPolicy.getFirstComponent(container)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)
} else {
Expand All @@ -265,7 +245,7 @@ private class FocusSwitcher<T : Component>(
if (it.isFocused && !isRequesting) {
focusManager.clearFocus(force = true)

val component = info.container.focusTraversalPolicy.getLastComponent(info.container)
val component = interopComponent.container.focusTraversalPolicy.getLastComponent(interopComponent.container)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_BACKWARD)
} else {
Expand All @@ -278,11 +258,55 @@ private class FocusSwitcher<T : Component>(
}
}

private class ComponentInfo<T : Component>(
container: SwingPanelContainer
private class SwingInteropComponent<T : Component>(
container: SwingPanelContainer,
var update: (T) -> Unit
): InteropComponent(container) {
lateinit var component: T
lateinit var updater: Updater<T>
private var userComponent: T? = null
private var updater: Updater<T>? = null

fun setupUserComponent(component: T) {
check(userComponent == null)
userComponent = component
container.add(component)
updater = Updater(component, update)
}

fun cleanUserComponent() {
container.remove(userComponent)
updater?.dispose()
userComponent = null
updater = null
}

fun setBounds(
bounds: IntRect,
clippedBounds: IntRect = bounds
) {
clipBounds = clippedBounds // Clipping area for skia canvas
container.isVisible = !clippedBounds.isEmpty // Hide if it's fully clipped
// Swing clips children based on parent's bounds, so use our container for clipping
container.setBounds(
/* x = */ clippedBounds.left,
/* y = */ clippedBounds.top,
/* width = */ clippedBounds.width,
/* height = */ clippedBounds.height
)

// The real size and position should be based on not-clipped bounds
userComponent?.setBounds(
/* x = */ bounds.left - clippedBounds.left, // Local position relative to container
/* y = */ bounds.top - clippedBounds.top,
/* width = */ bounds.width,
/* height = */ bounds.height
)
}

fun getDeepestComponentForEvent(event: MouseEvent): Component? {
if (userComponent == null) return null
val point = SwingUtilities.convertPoint(event.component, event.point, userComponent)
return SwingUtilities.getDeepestComponentAt(userComponent, point.x, point.y)
}
}

private class Updater<T : Component>(
Expand Down Expand Up @@ -343,7 +367,7 @@ private fun Rect.round(density: Density): IntRect {
}

private class InteropPointerInputModifier<T : Component>(
private val componentInfo: ComponentInfo<T>,
private val interopComponent: SwingInteropComponent<T>,
) : PointerInputFilter(), PointerInputModifier {
override val pointerInputFilter: PointerInputFilter = this

Expand Down Expand Up @@ -380,23 +404,18 @@ private class InteropPointerInputModifier<T : Component>(
// to original component.
MouseEvent.MOUSE_ENTERED, MouseEvent.MOUSE_EXITED -> return
}
if (SwingUtilities.isDescendingFrom(e.component, componentInfo.container)) {
if (SwingUtilities.isDescendingFrom(e.component, interopComponent.container)) {
// Do not redispatch the event if it originally from this interop view.
return
}
val component = getDeepestComponentForEvent(componentInfo.component, e)
val component = interopComponent.getDeepestComponentForEvent(e)
if (component != null) {
component.dispatchEvent(SwingUtilities.convertMouseEvent(e.component, e, component))
pointerEvent.changes.fastForEach {
it.consume()
}
}
}

private fun getDeepestComponentForEvent(parent: Component, event: MouseEvent): Component? {
val point = SwingUtilities.convertPoint(event.component, event.point, parent)
return SwingUtilities.getDeepestComponentAt(parent, point.x, point.y)
}
}

/**
Expand Down
Loading

0 comments on commit 8a9057e

Please sign in to comment.