Skip to content

Commit e2b48aa

Browse files
authored
Allow drawing outside of platform layers (#1190)
## Proposed Changes - Use skia's BBHFactory to track real drawing bounds - requires JetBrains/skiko#889 (skiko `0.7.98`) - Adding additional click filtering to handle clicks on "shadow" area as click outside ## Testing Test: Try to use new platform layers with shadows <img width="695" alt="image" src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/80dce85f-814d-4d51-ab5e-6535ed976d48"> ## Issues Fixed Fixes: JetBrains/compose-multiplatform#4460
1 parent f6772a7 commit e2b48aa

File tree

16 files changed

+449
-151
lines changed

16 files changed

+449
-151
lines changed

compose/ui/ui/api/desktop/ui.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3134,6 +3134,7 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext {
31343134
public fun calculateLocalPosition-MK-Hz9U (J)J
31353135
public fun calculatePositionInWindow-MK-Hz9U (J)J
31363136
public abstract fun getInputModeManager ()Landroidx/compose/ui/input/InputModeManager;
3137+
public fun getMeasureDrawLayerBounds ()Z
31373138
public fun getParentFocusManager ()Landroidx/compose/ui/focus/FocusManager;
31383139
public fun getRootForTestListener ()Landroidx/compose/ui/platform/PlatformContext$RootForTestListener;
31393140
public fun getSemanticsOwnerListener ()Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener;

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ import androidx.compose.ui.window.Popup
2222
internal enum class LayerType {
2323
OnSameCanvas,
2424
OnComponent,
25+
26+
/**
27+
* TODO known issues:
28+
* - [Rendering issues on Linux](https://github.com/JetBrains/compose-multiplatform/issues/4437)
29+
* - [Blinking when showing](https://github.com/JetBrains/compose-multiplatform/issues/4475)
30+
* - [Resizing the parent window clips the dialog](https://github.com/JetBrains/compose-multiplatform/issues/4484)
31+
*/
2532
OnWindow;
2633

2734
companion object {

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ internal class ComposeSceneMediator(
101101
private var exceptionHandler: WindowExceptionHandler?,
102102
private val eventListener: AwtEventListener = OnlyValidPrimaryMouseButtonFilter,
103103

104+
/**
105+
* @see PlatformContext.measureDrawLayerBounds
106+
*/
107+
private val measureDrawLayerBounds: Boolean = false,
108+
104109
val coroutineContext: CoroutineContext,
105110

106111
skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent,
@@ -625,6 +630,7 @@ internal class ComposeSceneMediator(
625630
override fun calculateLocalPosition(positionInWindow: Offset): Offset =
626631
windowContext.calculateLocalPosition(container, positionInWindow)
627632

633+
override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds
628634
override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration()
629635
override val textInputService: PlatformTextInputService = this@ComposeSceneMediator.textInputService
630636

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/DesktopComposeSceneLayer.desktop.kt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,21 @@ import androidx.compose.ui.awt.AwtEventListeners
2424
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
2525
import androidx.compose.ui.awt.toAwtRectangle
2626
import androidx.compose.ui.input.pointer.PointerEventType
27+
import androidx.compose.ui.skiko.RecordDrawRectSkikoViewDecorator
2728
import androidx.compose.ui.unit.Density
2829
import androidx.compose.ui.unit.IntOffset
30+
import androidx.compose.ui.unit.IntRect
2931
import androidx.compose.ui.unit.LayoutDirection
32+
import androidx.compose.ui.unit.roundToIntRect
3033
import androidx.compose.ui.util.fastForEachReversed
34+
import java.awt.Rectangle
3135
import java.awt.event.KeyEvent
3236
import java.awt.event.MouseEvent
3337
import javax.swing.SwingUtilities
38+
import kotlin.math.max
39+
import kotlin.math.min
3440
import org.jetbrains.skia.Canvas
41+
import org.jetbrains.skiko.SkikoView
3542

3643
/**
3744
* Represents an abstract class for a desktop Compose scene layer.
@@ -49,11 +56,25 @@ internal abstract class DesktopComposeSceneLayer(
4956
protected val eventListener get() = AwtEventListeners(
5057
OnlyValidPrimaryMouseButtonFilter,
5158
DetectEventOutsideLayer(),
59+
boundsEventFilter,
5260
FocusableLayerEventFilter()
5361
)
62+
private val boundsEventFilter = BoundsEventFilter(
63+
bounds = Rectangle(windowContainer.size)
64+
)
5465

5566
protected abstract val mediator: ComposeSceneMediator?
5667

68+
/**
69+
* Bounds of real drawings based on previous renders.
70+
*/
71+
protected var drawBounds = IntRect.Zero
72+
73+
/**
74+
* The maximum amount to inflate the [drawBounds] comparing to [boundsInWindow].
75+
*/
76+
private var maxDrawInflate = IntRect.Zero
77+
5778
private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null
5879
private var isClosed = false
5980

@@ -69,6 +90,13 @@ internal abstract class DesktopComposeSceneLayer(
6990
mediator?.onChangeLayoutDirection(value)
7091
}
7192

93+
// It shouldn't be used for setting canvas size - it will crop drawings outside
94+
override var boundsInWindow: IntRect = IntRect.Zero
95+
set(value) {
96+
field = value
97+
boundsEventFilter.bounds = value.toAwtRectangle(density)
98+
}
99+
72100
final override var compositionLocalContext: CompositionLocalContext?
73101
get() = mediator?.compositionLocalContext
74102
set(value) { mediator?.compositionLocalContext = value }
@@ -101,6 +129,20 @@ internal abstract class DesktopComposeSceneLayer(
101129
override fun calculateLocalPosition(positionInWindow: IntOffset) =
102130
positionInWindow // [ComposeScene] is equal to [windowContainer] for the layer.
103131

132+
protected fun recordDrawBounds(skikoView: SkikoView) =
133+
RecordDrawRectSkikoViewDecorator(skikoView) { canvasBoundsInPx ->
134+
val currentCanvasOffset = drawBounds.topLeft
135+
val drawBoundsInWindow = canvasBoundsInPx.roundToIntRect().translate(currentCanvasOffset)
136+
maxDrawInflate = maxInflate(boundsInWindow, drawBoundsInWindow, maxDrawInflate)
137+
drawBounds = IntRect(
138+
left = boundsInWindow.left - maxDrawInflate.left,
139+
top = boundsInWindow.top - maxDrawInflate.top,
140+
right = boundsInWindow.right + maxDrawInflate.right,
141+
bottom = boundsInWindow.bottom + maxDrawInflate.bottom
142+
)
143+
onUpdateBounds()
144+
}
145+
104146
/**
105147
* Called when the focus of the window containing main Compose view has changed.
106148
*/
@@ -125,6 +167,12 @@ internal abstract class DesktopComposeSceneLayer(
125167
open fun onLayersChange() {
126168
}
127169

170+
/**
171+
* Called when bounds of the layer has been updated.
172+
*/
173+
open fun onUpdateBounds() {
174+
}
175+
128176
/**
129177
* Renders an overlay on the canvas.
130178
*
@@ -185,7 +233,44 @@ internal abstract class DesktopComposeSceneLayer(
185233
override fun onMouseEvent(event: MouseEvent): Boolean = !noFocusableLayersAbove
186234
override fun onKeyEvent(event: KeyEvent): Boolean = !focusable || !noFocusableLayersAbove
187235
}
236+
237+
private inner class BoundsEventFilter(
238+
var bounds: Rectangle,
239+
) : AwtEventListener {
240+
private val MouseEvent.isInBounds: Boolean
241+
get() {
242+
val localPoint = if (component != windowContainer) {
243+
SwingUtilities.convertPoint(component, point, windowContainer)
244+
} else {
245+
point
246+
}
247+
return bounds.contains(localPoint)
248+
}
249+
250+
override fun onMouseEvent(event: MouseEvent): Boolean {
251+
when (event.id) {
252+
// Do not filter motion events
253+
MouseEvent.MOUSE_MOVED,
254+
MouseEvent.MOUSE_ENTERED,
255+
MouseEvent.MOUSE_EXITED,
256+
MouseEvent.MOUSE_DRAGGED -> return false
257+
}
258+
return if (event.isInBounds) {
259+
false
260+
} else {
261+
onMouseEventOutside(event)
262+
true
263+
}
264+
}
265+
}
188266
}
189267

190268
private fun MouseEvent.isMainAction() =
191269
button == MouseEvent.BUTTON1
270+
271+
private fun maxInflate(baseBounds: IntRect, currentBounds: IntRect, maxInflate: IntRect) = IntRect(
272+
left = max(baseBounds.left - currentBounds.left, maxInflate.left),
273+
top = max(baseBounds.top - currentBounds.top, maxInflate.top),
274+
right = max(currentBounds.right - baseBounds.right, maxInflate.right),
275+
bottom = max(currentBounds.bottom - baseBounds.bottom, maxInflate.bottom)
276+
)

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ package androidx.compose.ui.scene
1919
import androidx.compose.runtime.CompositionContext
2020
import androidx.compose.ui.awt.toAwtColor
2121
import androidx.compose.ui.awt.toAwtRectangle
22+
import androidx.compose.ui.geometry.toRect
2223
import androidx.compose.ui.graphics.Color
2324
import androidx.compose.ui.scene.skia.SkiaLayerComponent
2425
import androidx.compose.ui.scene.skia.SwingSkiaLayerComponent
2526
import androidx.compose.ui.unit.Density
26-
import androidx.compose.ui.unit.IntRect
2727
import androidx.compose.ui.unit.IntSize
2828
import androidx.compose.ui.unit.LayoutDirection
29+
import androidx.compose.ui.unit.roundToIntRect
2930
import androidx.compose.ui.window.density
31+
import androidx.compose.ui.window.sizeInPx
3032
import java.awt.Dimension
3133
import java.awt.Graphics
3234
import java.awt.event.MouseAdapter
@@ -52,9 +54,7 @@ internal class SwingComposeSceneLayer(
5254
override fun addNotify() {
5355
super.addNotify()
5456
mediator?.onComponentAttached()
55-
_boundsInWindow?.let {
56-
mediator?.contentComponent?.bounds = it.toAwtRectangle(density)
57-
}
57+
onUpdateBounds()
5858
}
5959

6060
override fun paint(g: Graphics) {
@@ -71,20 +71,14 @@ internal class SwingComposeSceneLayer(
7171
it.background = Color.Transparent.toAwtColor()
7272
it.size = Dimension(windowContainer.width, windowContainer.height)
7373
it.addMouseListener(backgroundMouseListener)
74-
75-
// TODO: Currently it works only with offscreen rendering
76-
// TODO: Do not clip this from main scene if layersContainer == main container
77-
windowContainer.add(it, JLayeredPane.POPUP_LAYER, 0)
7874
}
7975

8076
private var containerSize = IntSize.Zero
8177
set(value) {
8278
if (field.width != value.width || field.height != value.height) {
8379
field = value
8480
container.setBounds(0, 0, value.width, value.height)
85-
if (_boundsInWindow == null) {
86-
mediator?.contentComponent?.size = container.size
87-
}
81+
mediator?.contentComponent?.size = container.size
8882
mediator?.onChangeComponentSize()
8983
}
9084
}
@@ -97,18 +91,6 @@ internal class SwingComposeSceneLayer(
9791
container.isFocusable = value
9892
}
9993

100-
private var _boundsInWindow: IntRect? = null
101-
override var boundsInWindow: IntRect
102-
get() = _boundsInWindow ?: IntRect.Zero
103-
set(value) {
104-
_boundsInWindow = value
105-
val localBounds = SwingUtilities.convertRectangle(
106-
/* source = */ windowContainer,
107-
/* aRectangle = */ value.toAwtRectangle(container.density),
108-
/* destination = */ container)
109-
mediator?.contentComponent?.bounds = localBounds
110-
}
111-
11294
override var scrimColor: Color? = null
11395
set(value) {
11496
field = value
@@ -117,20 +99,28 @@ internal class SwingComposeSceneLayer(
11799
}
118100

119101
init {
102+
val boundsInPx = windowContainer.sizeInPx.toRect()
103+
drawBounds = boundsInPx.roundToIntRect()
120104
mediator = ComposeSceneMediator(
121105
container = container,
122106
windowContext = composeContainer.windowContext,
123107
exceptionHandler = {
124108
composeContainer.exceptionHandler?.onException(it) ?: throw it
125109
},
126110
eventListener = eventListener,
111+
measureDrawLayerBounds = true,
127112
coroutineContext = compositionContext.effectCoroutineContext,
128113
skiaLayerComponentFactory = ::createSkiaLayerComponent,
129114
composeSceneFactory = ::createComposeScene,
130115
).also {
131116
it.onChangeWindowTransparency(true)
132117
it.contentComponent.size = container.size
133118
}
119+
120+
// TODO: Currently it works only with offscreen rendering
121+
// TODO: Do not clip this from main scene if layersContainer == main container
122+
windowContainer.add(container, JLayeredPane.POPUP_LAYER, 0)
123+
134124
composeContainer.attachLayer(this)
135125
}
136126

@@ -149,10 +139,20 @@ internal class SwingComposeSceneLayer(
149139
containerSize = IntSize(windowContainer.width, windowContainer.height)
150140
}
151141

142+
override fun onUpdateBounds() {
143+
val scaledRectangle = drawBounds.toAwtRectangle(density)
144+
val localBounds = SwingUtilities.convertRectangle(
145+
/* source = */ windowContainer,
146+
/* aRectangle = */ scaledRectangle,
147+
/* destination = */ container)
148+
mediator?.contentComponent?.bounds = localBounds
149+
}
150+
152151
private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
152+
val skikoView = recordDrawBounds(mediator)
153153
return SwingSkiaLayerComponent(
154154
mediator = mediator,
155-
skikoView = mediator,
155+
skikoView = skikoView,
156156
skiaLayerAnalytics = skiaLayerAnalytics
157157
)
158158
}

0 commit comments

Comments
 (0)