Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix clipping bounds of SwingPanel #1147

Merged
merged 3 commits into from
Feb 29, 2024
Merged
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 @@ -27,8 +27,10 @@ import androidx.compose.ui.node.TrackInteropModifierElement
import androidx.compose.ui.node.TrackInteropModifierNode
import androidx.compose.ui.node.countInteropComponentsBefore
import androidx.compose.ui.scene.ComposeSceneMediator
import androidx.compose.ui.unit.IntRect
import java.awt.Component
import java.awt.Container
import org.jetbrains.skiko.ClipRectangle

/**
* Providing interop container as composition local, so [SwingPanel] can use it to add
Expand All @@ -50,54 +52,55 @@ internal val LocalSwingInteropContainer = staticCompositionLocalOf<SwingInteropC
internal class SwingInteropContainer(
val container: Container,
private val placeInteropAbove: Boolean
): InteropContainer<Component> {
): InteropContainer<InteropComponent> {
/**
* Represents the count of interop components in [container].
*
* This variable is required to add interop components to right indexes independently of
* already existing children of [container].
*
* @see SwingInteropContainer.addInteropView
* @see SwingInteropContainer.removeInteropView
*/
private var interopComponentsCount = 0
private var interopComponents = mutableMapOf<Component, InteropComponent>()

override var rootModifier: TrackInteropModifierNode<Component>? = null
override var rootModifier: TrackInteropModifierNode<InteropComponent>? = null
override val interopViews: Set<InteropComponent>
get() = interopComponents.values.toSet()

override fun addInteropView(nativeView: Component) {
val nonInteropComponents = container.componentCount - interopComponentsCount
override fun addInteropView(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 = interopComponentsCount - countInteropComponentsBefore(nativeView)
container.add(nativeView, if (placeInteropAbove) {
val index = interopComponents.size - countInteropComponentsBefore(nativeView)
interopComponents[component] = nativeView
container.add(component, if (placeInteropAbove) {
index
} else {
index + nonInteropComponents
})
interopComponentsCount++

// Sometimes Swing displays the rest of interop views in incorrect order after removing,
// so we need to force re-validate it.
container.validate()
container.repaint()
}

override fun removeInteropView(nativeView: Component) {
interopComponentsCount--
container.remove(nativeView)
override fun removeInteropView(nativeView: InteropComponent) {
val component = nativeView.container
container.remove(component)
interopComponents.remove(component)

// Sometimes Swing displays the rest of interop views in incorrect order after removing,
// so we need to force re-validate it.
container.validate()
container.repaint()
}

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

@Composable
operator fun invoke(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalSwingInteropContainer provides this,
) {
TrackInteropContainer(
container = container,
content = content
)
}
Expand All @@ -110,7 +113,27 @@ internal class SwingInteropContainer(
* @param component The Swing component that matches the current node.
*/
internal fun Modifier.trackSwingInterop(
component: Component
component: InteropComponent
): Modifier = this then TrackInteropModifierElement(
nativeView = component
)

/**
* Provides clipping bounds for skia canvas.
*
* @param container The container that holds the component.
* @param clipBounds The rectangular region to clip skia canvas. It's relative to Compose root
*/
internal open class InteropComponent(
val container: Container,
var clipBounds: IntRect? = null
) : ClipRectangle {
override val x: Float
get() = (clipBounds?.left ?: container.x).toFloat()
override val y: Float
get() = (clipBounds?.top ?: container.y).toFloat()
override val width: Float
get() = (clipBounds?.width ?: container.width).toFloat()
override val height: Float
get() = (clipBounds?.height ?: container.height).toFloat()
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.layout.EmptyLayout
import androidx.compose.ui.layout.OverlayLayout
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
Expand All @@ -50,7 +50,6 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Container
import java.awt.event.FocusEvent
Expand Down Expand Up @@ -105,14 +104,36 @@ public fun <T : Component> SwingPanel(

OverlayLayout(
modifier = modifier.onGloballyPositioned { coordinates ->
val bounds = coordinates.boundsInRoot().round(density)
componentInfo.container.setBounds(bounds.left, bounds.top, bounds.width, bounds.height)
val rootCoordinates = coordinates.findRootCoordinates()
val clipedBounds = 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()
}.drawBehind {
// Clear interop area to make visible the component under our canvas.
drawRect(Color.Transparent, blendMode = BlendMode.Clear)
}.trackSwingInterop(componentInfo.container)
}.trackSwingInterop(componentInfo)
.then(InteropPointerInputModifier(componentInfo))
) {
focusSwitcher.Content()
Expand All @@ -133,9 +154,9 @@ public fun <T : Component> SwingPanel(
override fun focusLost(e: FocusEvent) = Unit
}
interopContainer.container.addFocusListener(focusListener)
interopContainer.addInteropView(componentInfo.container)
interopContainer.addInteropView(componentInfo)
onDispose {
interopContainer.removeInteropView(componentInfo.container)
interopContainer.removeInteropView(componentInfo)
interopContainer.container.removeFocusListener(focusListener)
}
}
Expand All @@ -156,13 +177,19 @@ public fun <T : Component> SwingPanel(
}
}

/**
* A container for [SwingPanel]'s component. Takes care about focus and clipping.
*
* @param key The unique identifier for the panel container.
* @param focusComponent The component that should receive focus.
*/
private class SwingPanelContainer(
key: Int,
private val focusComponent: Component
): JPanel() {
init {
name = "SwingPanel #$key"
layout = BorderLayout(0, 0)
name = "SwingPanel #${key.toString(MaxSupportedRadix)}"
layout = null
focusTraversalPolicy = object : LayoutFocusTraversalPolicy() {
override fun getComponentAfter(aContainer: Container?, aComponent: Component?): Component? {
return if (aComponent == getLastComponent(aContainer)) {
Expand Down Expand Up @@ -252,8 +279,8 @@ private class FocusSwitcher<T : Component>(
}

private class ComponentInfo<T : Component>(
val container: Container
) {
container: SwingPanelContainer
): InteropComponent(container) {
lateinit var component: T
lateinit var updater: Updater<T>
}
Expand Down Expand Up @@ -371,3 +398,8 @@ private class InteropPointerInputModifier<T : Component>(
return SwingUtilities.getDeepestComponentAt(parent, point.x, point.y)
}
}

/**
* The maximum radix available for conversion to and from strings.
*/
private val MaxSupportedRadix = 36
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.ClipComponent
import org.jetbrains.skiko.ClipRectangle
import org.jetbrains.skiko.ExperimentalSkikoApi
import org.jetbrains.skiko.GraphicsApi
import org.jetbrains.skiko.SkikoInput
Expand Down Expand Up @@ -151,7 +152,7 @@ internal class ComposeSceneMediator(
)

private val containerListener = object : ContainerListener {
private val clipMap = mutableMapOf<Component, ClipComponent>()
private val clipMap = mutableMapOf<Component, ClipRectangle>()

override fun componentAdded(e: ContainerEvent) {
val component = e.child
Expand All @@ -176,7 +177,7 @@ internal class ComposeSceneMediator(
}

private fun addClipComponent(component: Component) {
val clipRectangle = ClipComponent(component)
val clipRectangle = interopContainer.getClipRectForComponent(component)
clipMap[component] = clipRectangle
skiaLayerComponent.clipComponents.add(clipRectangle)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAct
*/
internal interface InteropContainer<T> {
var rootModifier: TrackInteropModifierNode<T>?
val interopViews: Set<T>

fun addInteropView(nativeView: T)
fun removeInteropView(nativeView: T)
Expand All @@ -44,7 +45,11 @@ internal fun <T> InteropContainer<T>.countInteropComponentsBefore(nativeView: T)
var componentsBefore = 0
rootModifier?.traverseDescendants {
if (it.nativeView != nativeView) {
componentsBefore++
// It might be inside Compose tree before adding in InteropContainer in case
// if it was initiated out of scroll visible bounds for example.
if (it.nativeView in interopViews) {
componentsBefore++
}
ContinueTraversal
} else {
CancelTraversal
Expand All @@ -58,11 +63,9 @@ internal fun <T> InteropContainer<T>.countInteropComponentsBefore(nativeView: T)
* that allows to traverse interop views in the tree with the right order.
*/
@Composable
internal fun <T> InteropContainer<T>.TrackInteropContainer(container: T, content: @Composable () -> Unit) {
internal fun <T> InteropContainer<T>.TrackInteropContainer(content: @Composable () -> Unit) {
OverlayLayout(
modifier = TrackInteropModifierElement(
nativeView = container
) { rootModifier = it },
modifier = TrackInteropModifierElement { rootModifier = it },
content = content
)
}
Expand All @@ -79,7 +82,7 @@ internal fun <T> InteropContainer<T>.TrackInteropContainer(container: T, content
* @see ModifierNodeElement
*/
internal data class TrackInteropModifierElement<T>(
var nativeView: T,
var nativeView: T? = null,
val onModifierNodeCreated: ((TrackInteropModifierNode<T>) -> Unit)? = null
) : ModifierNodeElement<TrackInteropModifierNode<T>>() {
override fun create() = TrackInteropModifierNode(
Expand All @@ -105,7 +108,7 @@ private const val TRAVERSAL_NODE_KEY =
* @see TraversableNode
*/
internal class TrackInteropModifierNode<T>(
var nativeView: T
var nativeView: T?
) : Modifier.Node(), TraversableNode {
override val traverseKey = TRAVERSAL_NODE_KEY
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ internal val LocalUIKitInteropContainer = staticCompositionLocalOf<UIKitInteropC
internal class UIKitInteropContainer: InteropContainer<UIView> {
val containerView: UIView = UIKitInteropContainerView()
override var rootModifier: TrackInteropModifierNode<UIView>? = null
override var interopViews = mutableSetOf<UIView>()
private set

override fun addInteropView(nativeView: UIView) {
val index = countInteropComponentsBefore(nativeView)
interopViews.add(nativeView)
containerView.insertSubview(nativeView, index.toLong())
}

override fun removeInteropView(nativeView: UIView) {
nativeView.removeFromSuperview()
interopViews.remove(nativeView)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,6 @@ internal class ComposeSceneMediator(
if (renderingView.isReadyToShowContent.value) {
ProvideComposeSceneMediatorCompositionLocals {
interopViewContainer.TrackInteropContainer(
container = interopViewContainer.containerView,
content = content
)
}
Expand Down
Loading