Skip to content

Commit 679e24d

Browse files
committed
Add blockPointerInputOutside flag
1 parent 077d3a5 commit 679e24d

18 files changed

Lines changed: 448 additions & 57 deletions

File tree

compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/components/popup/ConfigurablePopup.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,15 @@ private fun EditPopupProperties(): PopupProperties {
148148
val clippingEnabled = EditBooleanSetting("clippingEnabled", true)
149149
val usePlatformDefaultWidth = EditBooleanSetting("usePlatformDefaultWidth", false)
150150
val usePlatformInsets = EditBooleanSetting("usePlatformInsets", true)
151+
val blockPointerInputOutside = EditBooleanSetting("blockPointerInputOutside", false)
151152
popupProperties = PopupProperties(
152153
focusable = focusable,
153154
dismissOnBackPress = dismissOnBackPress,
154155
dismissOnClickOutside = dismissOnClickOutside,
155156
clippingEnabled = clippingEnabled,
156157
usePlatformDefaultWidth = usePlatformDefaultWidth,
157-
usePlatformInsets = usePlatformInsets
158+
usePlatformInsets = usePlatformInsets,
159+
blockPointerInputOutside = blockPointerInputOutside,
158160
)
159161
}
160162
return popupProperties

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4611,7 +4611,6 @@ public abstract interface class androidx/compose/ui/window/PopupPositionProvider
46114611

46124612
public final class androidx/compose/ui/window/PopupProperties {
46134613
public static final field $stable I
4614-
public fun <init> ()V
46154614
public synthetic fun <init> (ZZZZ)V
46164615
public synthetic fun <init> (ZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
46174616
public fun <init> (ZZZZZ)V

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

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import androidx.compose.ui.scene.skia.SkiaLayerComponent
3232
import androidx.compose.ui.skiko.OverlayRenderDecorator
3333
import androidx.compose.ui.unit.Density
3434
import androidx.compose.ui.unit.LayoutDirection
35-
import androidx.compose.ui.util.fastAll
3635
import androidx.compose.ui.util.fastForEach
3736
import androidx.compose.ui.util.fastForEachReversed
3837
import androidx.compose.ui.window.Dialog
@@ -138,7 +137,7 @@ internal class ComposeContainer(
138137
},
139138
eventListener = AwtEventListeners(
140139
DetectEventOutsideLayer(),
141-
FocusableLayerEventFilter()
140+
BlockingInputLayerEventFilter()
142141
),
143142
architectureComponentsOwner = architectureComponentsOwner,
144143
coroutineContext = coroutineContext + MainUIDispatcher + DesktopCoroutineExceptionHandler(),
@@ -402,29 +401,32 @@ internal class ComposeContainer(
402401
}
403402

404403
private fun createPlatformLayer(
404+
compositionContext: CompositionContext,
405405
density: Density,
406406
layoutDirection: LayoutDirection,
407407
focusable: Boolean,
408-
compositionContext: CompositionContext
408+
blockPointerInputOutside: Boolean,
409409
): ComposeSceneLayer {
410410
return when (layerType) {
411411
LayerType.OnWindow -> WindowComposeSceneLayer(
412412
composeContainer = this,
413413
skiaLayerAnalytics = skiaLayerAnalytics,
414414
transparent = true, // TODO: Consider allowing opaque window layers
415+
compositionContext = compositionContext,
415416
density = density,
416417
layoutDirection = layoutDirection,
417418
focusable = focusable,
418-
compositionContext = compositionContext,
419-
renderSettings = renderSettings
419+
blockPointerInputOutside = blockPointerInputOutside,
420+
renderSettings = renderSettings,
420421
)
421422
LayerType.OnComponent -> SwingComposeSceneLayer(
422423
composeContainer = this,
423424
skiaLayerAnalytics = skiaLayerAnalytics,
425+
compositionContext = compositionContext,
424426
density = density,
425427
layoutDirection = layoutDirection,
426428
focusable = focusable,
427-
compositionContext = compositionContext
429+
blockPointerInputOutside = blockPointerInputOutside,
428430
)
429431
else -> error("Unexpected LayerType")
430432
}
@@ -447,6 +449,23 @@ internal class ComposeContainer(
447449
}
448450
}
449451

452+
/**
453+
* Generates a sequence of layers that are positioned below the given layer in the layers list.
454+
*
455+
* @param layer the layer to find layers below
456+
* @return a sequence of layers positioned below the given layer
457+
*/
458+
fun layersBelow(layer: DesktopComposeSceneLayer) = sequence {
459+
var isBelow = true
460+
layers.fastForEach { i ->
461+
if (i == layer) {
462+
isBelow = false
463+
} else if (isBelow) {
464+
yield(i)
465+
}
466+
}
467+
}
468+
450469
/**
451470
* Notify layers about change in layers list. Required for additional invalidation and
452471
* re-drawing if needed.
@@ -499,15 +518,17 @@ internal class ComposeContainer(
499518
override val platformContext: PlatformContext,
500519
) : ComposeSceneContext {
501520
override fun createLayer(
521+
compositionContext: CompositionContext,
502522
density: Density,
503523
layoutDirection: LayoutDirection,
504524
focusable: Boolean,
505-
compositionContext: CompositionContext
525+
blockPointerInputOutside: Boolean,
506526
): ComposeSceneLayer = createPlatformLayer(
527+
compositionContext = compositionContext,
507528
density = density,
508529
layoutDirection = layoutDirection,
509530
focusable = focusable,
510-
compositionContext = compositionContext
531+
blockPointerInputOutside = blockPointerInputOutside,
511532
)
512533
}
513534

@@ -520,24 +541,24 @@ internal class ComposeContainer(
520541

521542
/**
522543
* Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below
523-
* focused layer.
544+
* a layer that blocks pointer input outside of its bounds.
524545
*/
525546
private inner class DetectEventOutsideLayer : AwtEventListener {
526547
override fun onMouseEvent(event: AwtMouseEvent): Boolean {
527548
layers.fastForEachReversed {
528549
it.onMouseEventOutside(event)
529-
if (it.focusable) {
550+
if (it.blockPointerInputOutside) {
530551
return false
531552
}
532553
}
533554
return false
534555
}
535556
}
536557

537-
private inner class FocusableLayerEventFilter : AwtEventFilter() {
538-
private val noFocusableLayers get() = layers.fastAll { !it.focusable }
558+
private inner class BlockingInputLayerEventFilter : AwtEventFilter() {
559+
private val noBlockingInputLayers get() = layers.all { !it.blockPointerInputOutside }
539560

540-
override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noFocusableLayers
541-
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noFocusableLayers
561+
override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noBlockingInputLayers
562+
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noBlockingInputLayers
542563
}
543564
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ internal class ComposeSceneMediator(
504504
val processingResult = scene.onMouseWheelEvent(event.position, event)
505505
if (!processingResult.anyChangeConsumed) {
506506
if (redispatchUnconsumedMouseWheelEvents) {
507-
redispatchUnconsumedMouseEvent(event)
507+
redispatchMouseEvent(event)
508508
}
509509
}
510510
}
@@ -531,7 +531,7 @@ internal class ComposeSceneMediator(
531531
* AWT/Swing, however, interprets listening to mouse events as "interest" in them and sends them
532532
* only to the "interested" component "under" the mouse pointer.
533533
*/
534-
private fun redispatchUnconsumedMouseEvent(event: MouseEvent) {
534+
fun redispatchMouseEvent(event: MouseEvent, target: Component? = null) {
535535
// Redispatch the event to the heavyweight ancestor, which in turn will try to find the
536536
// correct target component and send the event to it. Unregistering from mouse events
537537
// during this call allows the event to be sent to the component it would've been sent to
@@ -543,11 +543,11 @@ internal class ComposeSceneMediator(
543543
// With that approach, there would probably also be a need to add a flag (or multiple flags)
544544
// to ComposePanel that would control which types of events should be listened to.
545545
val source = event.component ?: return // Should be contentComponent
546-
val target = source.heavyWeightAncestorOrNull() ?: return
546+
val resolvedTarget = target ?: source.heavyWeightAncestorOrNull() ?: return
547547
try {
548548
unsubscribeFromInputEvents()
549-
val retargetedEvent = SwingUtilities.convertMouseEvent(source, event, target)
550-
target.dispatchEvent(retargetedEvent)
549+
val retargetedEvent = SwingUtilities.convertMouseEvent(source, event, resolvedTarget)
550+
resolvedTarget.dispatchEvent(retargetedEvent)
551551
} finally {
552552
subscribeToInputEvents()
553553
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ internal abstract class DesktopComposeSceneLayer(
5656
protected val eventListener get() = AwtEventListeners(
5757
DetectEventOutsideLayer(),
5858
boundsEventFilter,
59-
FocusableLayerEventFilter()
59+
BlockingInputLayerEventFilter()
6060
)
6161
private val boundsEventFilter = BoundsEventFilter(
6262
bounds = Rectangle(windowContainer.size)
@@ -217,6 +217,10 @@ internal abstract class DesktopComposeSceneLayer(
217217
outsidePointerCallback?.invoke(eventType, event.composePointerButton)
218218
}
219219

220+
protected open fun redispatchMouseEvent(event: MouseEvent) {
221+
mediator?.redispatchMouseEvent(event)
222+
}
223+
220224
private fun inBounds(event: MouseEvent): Boolean {
221225
val point = if (event.component != windowContainer) {
222226
SwingUtilities.convertPoint(event.component, event.point, windowContainer)
@@ -228,28 +232,28 @@ internal abstract class DesktopComposeSceneLayer(
228232

229233
/**
230234
* Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below
231-
* focused layer.
235+
* a layer that blocks pointer input outside of its bounds.
232236
*/
233237
private inner class DetectEventOutsideLayer : AwtEventListener {
234238
override fun onMouseEvent(event: MouseEvent): Boolean {
235239
layersAbove.toList().fastForEachReversed {
236240
if (!inBounds(event)) {
237241
it.onMouseEventOutside(event)
238242
}
239-
if (it.focusable) {
243+
if (it.blockPointerInputOutside) {
240244
return false
241245
}
242246
}
243247
return false
244248
}
245249
}
246250

247-
private inner class FocusableLayerEventFilter : AwtEventFilter() {
248-
private val noFocusableLayersAbove: Boolean
249-
get() = layersAbove.all { !it.focusable }
251+
private inner class BlockingInputLayerEventFilter : AwtEventFilter() {
252+
private val noBlockingInputLayers: Boolean
253+
get() = layersAbove.all { !it.blockPointerInputOutside }
250254

251-
override fun shouldSendMouseEvent(event: MouseEvent): Boolean = noFocusableLayersAbove
252-
override fun shouldSendKeyEvent(event: KeyEvent): Boolean = focusable && noFocusableLayersAbove
255+
override fun shouldSendMouseEvent(event: MouseEvent): Boolean = noBlockingInputLayers
256+
override fun shouldSendKeyEvent(event: KeyEvent): Boolean = focusable && noBlockingInputLayers
253257
}
254258

255259
private inner class BoundsEventFilter(
@@ -277,6 +281,9 @@ internal abstract class DesktopComposeSceneLayer(
277281
true
278282
} else {
279283
onMouseEventOutside(event)
284+
if (!blockPointerInputOutside) {
285+
redispatchMouseEvent(event)
286+
}
280287
false
281288
}
282289
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,25 @@ import org.jetbrains.skiko.SkiaLayerAnalytics
4040
internal class SwingComposeSceneLayer(
4141
composeContainer: ComposeContainer,
4242
private val skiaLayerAnalytics: SkiaLayerAnalytics,
43+
compositionContext: CompositionContext,
4344
density: Density,
4445
layoutDirection: LayoutDirection,
4546
focusable: Boolean,
46-
compositionContext: CompositionContext
47+
blockPointerInputOutside: Boolean = focusable,
4748
) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) {
49+
private fun onBackgroundMouseEvent(event: MouseEvent) {
50+
onMouseEventOutside(event)
51+
if (!blockPointerInputOutside) {
52+
// [redispatchMouseEvent] requires a hack with unregistering from mouse events
53+
container.removeMouseListener(backgroundMouseListener)
54+
mediator?.redispatchMouseEvent(event)
55+
container.addMouseListener(backgroundMouseListener)
56+
}
57+
}
58+
4859
private val backgroundMouseListener = object : MouseAdapter() {
49-
override fun mousePressed(event: MouseEvent) = onMouseEventOutside(event)
50-
override fun mouseReleased(event: MouseEvent) = onMouseEventOutside(event)
60+
override fun mousePressed(event: MouseEvent) = onBackgroundMouseEvent(event)
61+
override fun mouseReleased(event: MouseEvent) = onBackgroundMouseEvent(event)
5162
}
5263

5364
private val container = object : JLayeredPane() {
@@ -94,6 +105,11 @@ internal class SwingComposeSceneLayer(
94105
updateBounds()
95106
}
96107

108+
override var blockPointerInputOutside: Boolean = blockPointerInputOutside
109+
set(value) {
110+
field = value
111+
}
112+
97113
override var scrimColor: Color? = null
98114

99115
init {

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ import androidx.compose.ui.window.density
3737
import androidx.compose.ui.window.getDialogScrimBlendMode
3838
import androidx.compose.ui.window.layoutDirectionFor
3939
import androidx.compose.ui.window.sizeInPx
40+
import java.awt.Component
4041
import java.awt.Point
4142
import java.awt.event.ComponentAdapter
4243
import java.awt.event.ComponentEvent
44+
import java.awt.event.MouseEvent
4345
import javax.swing.JDialog
46+
import javax.swing.SwingUtilities
4447
import org.jetbrains.skia.Canvas
4548
import org.jetbrains.skiko.DelicateSkikoApi
4649
import org.jetbrains.skiko.SkiaLayerAnalytics
@@ -50,10 +53,11 @@ internal class WindowComposeSceneLayer(
5053
composeContainer: ComposeContainer,
5154
private val skiaLayerAnalytics: SkiaLayerAnalytics,
5255
private val transparent: Boolean,
56+
compositionContext: CompositionContext,
5357
density: Density,
5458
layoutDirection: LayoutDirection,
5559
focusable: Boolean,
56-
compositionContext: CompositionContext,
60+
override var blockPointerInputOutside: Boolean,
5761
private val renderSettings: RenderSettings
5862
) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) {
5963
// WindowComposeSceneLayer is tied to the window it was created with
@@ -198,6 +202,12 @@ internal class WindowComposeSceneLayer(
198202
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
199203
}
200204

205+
override fun redispatchMouseEvent(event: MouseEvent) {
206+
val source = event.component ?: return
207+
val target = findRedispatchTarget(source, event.point) ?: return
208+
mediator?.redispatchMouseEvent(event = event, target = target)
209+
}
210+
201211
private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent {
202212
val renderDelegate = OverlayRenderDecorator(
203213
recordDrawBounds(mediator)
@@ -239,4 +249,27 @@ internal class WindowComposeSceneLayer(
239249
locationOnScreen.y + y
240250
)
241251
}
252+
253+
private fun findRedispatchTarget(source: Component, point: Point): Component? {
254+
val screenPoint = Point(point)
255+
val sourceLocation = source.locationOnScreen
256+
screenPoint.translate(sourceLocation.x, sourceLocation.y)
257+
258+
composeContainer.layersBelow(this).toList().asReversed().forEach { layer ->
259+
val targetWindow = (layer as? WindowComposeSceneLayer)?.layerWindow ?: return@forEach
260+
val pointInTargetWindow = Point(screenPoint)
261+
SwingUtilities.convertPointFromScreen(pointInTargetWindow, targetWindow)
262+
if (targetWindow.isShowing && targetWindow.contains(pointInTargetWindow)) {
263+
return targetWindow.findComponentAt(pointInTargetWindow) ?: targetWindow
264+
}
265+
}
266+
267+
val pointInParentWindow = Point(screenPoint)
268+
SwingUtilities.convertPointFromScreen(pointInParentWindow, parentWindow)
269+
if (parentWindow.isShowing && parentWindow.contains(pointInParentWindow)) {
270+
return parentWindow.findComponentAt(pointInParentWindow) ?: parentWindow
271+
}
272+
273+
return null
274+
}
242275
}

0 commit comments

Comments
 (0)