Skip to content

Commit

Permalink
Add native interop views only after placement
Browse files Browse the repository at this point in the history
  • Loading branch information
MatkovIvan committed May 22, 2024
1 parent 7b0b9cb commit 64d8cee
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import androidx.compose.ui.scene.ComposeSceneMediator
import androidx.compose.ui.unit.IntRect
import java.awt.Component
import java.awt.Container
import java.awt.event.ContainerAdapter
import java.awt.event.ContainerEvent
import java.awt.event.ContainerListener
import javax.swing.SwingUtilities
import org.jetbrains.skiko.ClipRectangle

/**
Expand All @@ -54,7 +58,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,12 +67,13 @@ internal class SwingInteropContainer(
override val interopViews: Set<InteropComponent>
get() = interopComponents.values.toSet()

override fun addInteropView(nativeView: InteropComponent) {
override fun placeInteropView(nativeView: InteropComponent) = SwingUtilities.invokeLater {
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.remove(component)
container.add(component, if (placeInteropAbove) {
index
} else {
Expand All @@ -81,7 +86,7 @@ internal class SwingInteropContainer(
container.repaint()
}

override fun removeInteropView(nativeView: InteropComponent) {
override fun removeInteropView(nativeView: InteropComponent) = SwingUtilities.invokeLater {
val component = nativeView.container
container.remove(component)
interopComponents.remove(component)
Expand All @@ -92,6 +97,11 @@ internal class SwingInteropContainer(
container.repaint()
}

fun validateComponentsOrder() = SwingUtilities.invokeLater {
container.validate()
container.repaint()
}

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

Expand All @@ -113,8 +123,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 +138,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.setComponent(factory())
onDispose {
componentInfo.container.remove(componentInfo.component)
componentInfo.updater.dispose()
interopComponent.dispose()
}
}

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 @@ -244,11 +223,13 @@ private class FocusSwitcher<T : Component>(
EmptyLayout(
Modifier
.focusRequester(backwardRequester)
.onFocusEvent {
.onFocusEvent { it ->
if (it.isFocused && !isRequesting) {
focusManager.clearFocus(force = true)

val component = info.container.focusTraversalPolicy.getFirstComponent(info.container)
val component = interopComponent.container.let { container ->
container.focusTraversalPolicy.getFirstComponent(container)
}
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)
} else {
Expand All @@ -265,7 +246,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 +259,54 @@ 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 component: T? = null
private var updater: Updater<T>? = null

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

fun setComponent(component: T) {
this.component = component
container.add(component)
updater = Updater(component, update)
}

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
component?.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 (component == null) return null
val point = SwingUtilities.convertPoint(event.component, event.point, component)
return SwingUtilities.getDeepestComponentAt(component, 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 64d8cee

Please sign in to comment.