Skip to content

Commit 7587c0c

Browse files
committed
Add blockPointerInputOutside flag
1 parent f79cd2a commit 7587c0c

17 files changed

Lines changed: 310 additions & 56 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 consumePointerInputOutside = EditBooleanSetting("consumePointerInputOutside", 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+
consumePointerInputOutside = consumePointerInputOutside,
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: 17 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,31 @@ 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+
consumePointerInputOutside: Boolean,
409409
): ComposeSceneLayer {
410410
return when (layerType) {
411411
LayerType.OnWindow -> WindowComposeSceneLayer(
412412
composeContainer = this,
413413
skiaLayerAnalytics = skiaLayerAnalytics,
414+
renderSettings = renderSettings,
414415
transparent = true, // TODO: Consider allowing opaque window layers
416+
compositionContext = compositionContext,
415417
density = density,
416418
layoutDirection = layoutDirection,
417419
focusable = focusable,
418-
compositionContext = compositionContext,
419-
renderSettings = renderSettings
420420
)
421421
LayerType.OnComponent -> SwingComposeSceneLayer(
422422
composeContainer = this,
423423
skiaLayerAnalytics = skiaLayerAnalytics,
424+
compositionContext = compositionContext,
424425
density = density,
425426
layoutDirection = layoutDirection,
426427
focusable = focusable,
427-
compositionContext = compositionContext
428+
consumePointerInputOutside = consumePointerInputOutside,
428429
)
429430
else -> error("Unexpected LayerType")
430431
}
@@ -499,15 +500,17 @@ internal class ComposeContainer(
499500
override val platformContext: PlatformContext,
500501
) : ComposeSceneContext {
501502
override fun createLayer(
503+
compositionContext: CompositionContext,
502504
density: Density,
503505
layoutDirection: LayoutDirection,
504506
focusable: Boolean,
505-
compositionContext: CompositionContext
507+
consumePointerInputOutside: Boolean,
506508
): ComposeSceneLayer = createPlatformLayer(
509+
compositionContext = compositionContext,
507510
density = density,
508511
layoutDirection = layoutDirection,
509512
focusable = focusable,
510-
compositionContext = compositionContext
513+
consumePointerInputOutside = consumePointerInputOutside,
511514
)
512515
}
513516

@@ -520,24 +523,24 @@ internal class ComposeContainer(
520523

521524
/**
522525
* Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below
523-
* focused layer.
526+
* a layer that blocks pointer input outside of its bounds.
524527
*/
525528
private inner class DetectEventOutsideLayer : AwtEventListener {
526529
override fun onMouseEvent(event: AwtMouseEvent): Boolean {
527530
layers.fastForEachReversed {
528531
it.onMouseEventOutside(event)
529-
if (it.focusable) {
532+
if (it.consumePointerInputOutside) {
530533
return false
531534
}
532535
}
533536
return false
534537
}
535538
}
536539

537-
private inner class FocusableLayerEventFilter : AwtEventFilter() {
538-
private val noFocusableLayers get() = layers.fastAll { !it.focusable }
540+
private inner class BlockingInputLayerEventFilter : AwtEventFilter() {
541+
private val noBlockingInputLayers get() = layers.all { !it.consumePointerInputOutside }
539542

540-
override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noFocusableLayers
541-
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noFocusableLayers
543+
override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noBlockingInputLayers
544+
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noBlockingInputLayers
542545
}
543546
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
private fun redispatchUnconsumedMouseEvent(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: 13 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)
@@ -108,6 +108,11 @@ internal abstract class DesktopComposeSceneLayer(
108108
get() = mediator?.compositionLocalContext
109109
set(value) { mediator?.compositionLocalContext = value }
110110

111+
// Blocking pointer input outside is not supported
112+
override var consumePointerInputOutside: Boolean
113+
get() = false
114+
set(_) {}
115+
111116
@CallSuper
112117
override fun close() {
113118
isClosed = true
@@ -228,28 +233,28 @@ internal abstract class DesktopComposeSceneLayer(
228233

229234
/**
230235
* Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below
231-
* focused layer.
236+
* a layer that blocks pointer input outside of its bounds.
232237
*/
233238
private inner class DetectEventOutsideLayer : AwtEventListener {
234239
override fun onMouseEvent(event: MouseEvent): Boolean {
235240
layersAbove.toList().fastForEachReversed {
236241
if (!inBounds(event)) {
237242
it.onMouseEventOutside(event)
238243
}
239-
if (it.focusable) {
244+
if (it.consumePointerInputOutside) {
240245
return false
241246
}
242247
}
243248
return false
244249
}
245250
}
246251

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

251-
override fun shouldSendMouseEvent(event: MouseEvent): Boolean = noFocusableLayersAbove
252-
override fun shouldSendKeyEvent(event: KeyEvent): Boolean = focusable && noFocusableLayersAbove
256+
override fun shouldSendMouseEvent(event: MouseEvent): Boolean = noBlockingInputLayers
257+
override fun shouldSendKeyEvent(event: KeyEvent): Boolean = focusable && noBlockingInputLayers
253258
}
254259

255260
private inner class BoundsEventFilter(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ 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+
override var consumePointerInputOutside: Boolean = focusable,
4748
) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) {
4849
private val backgroundMouseListener = object : MouseAdapter() {
4950
override fun mousePressed(event: MouseEvent) = onMouseEventOutside(event)
@@ -156,7 +157,7 @@ internal class SwingComposeSceneLayer(
156157
val contentComponent = mediator?.contentComponent ?: return
157158
val localDrawBounds = drawBounds.toAwtRectangle(density)
158159

159-
if (focusable) {
160+
if (consumePointerInputOutside) {
160161
container.setBounds(0, 0, windowContainer.width, windowContainer.height)
161162
contentComponent.bounds = localDrawBounds
162163
mediator?.sceneBoundsInPx = null

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ import org.jetbrains.skiko.transparentWindowBackgroundHack
4949
internal class WindowComposeSceneLayer(
5050
composeContainer: ComposeContainer,
5151
private val skiaLayerAnalytics: SkiaLayerAnalytics,
52+
private val renderSettings: RenderSettings,
5253
private val transparent: Boolean,
54+
compositionContext: CompositionContext,
5355
density: Density,
5456
layoutDirection: LayoutDirection,
5557
focusable: Boolean,
56-
compositionContext: CompositionContext,
57-
private val renderSettings: RenderSettings
5858
) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) {
5959
// WindowComposeSceneLayer is tied to the window it was created with
6060
private val parentWindow = requireNotNull(composeContainer.window)

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package androidx.compose.ui.awt
1717

1818
import androidx.compose.foundation.ScrollState
19+
import androidx.compose.foundation.clickable
1920
import androidx.compose.foundation.focusable
2021
import androidx.compose.foundation.layout.Box
2122
import androidx.compose.foundation.layout.Column
@@ -620,6 +621,66 @@ class ComposePanelTest {
620621
}
621622
}
622623

624+
@Test
625+
fun focusableNonBlockingPopup_withComponentLayerType_passesClicksThrough() {
626+
ComposeFeatureFlags.layerType.withOverride(LayerType.OnComponent) {
627+
ComposeFeatureFlags.useSwingGraphicsInComposePanel.withOverride(true) {
628+
val window = JFrame()
629+
try {
630+
runApplicationTest {
631+
var showPopup by mutableStateOf(false)
632+
var backgroundClickCount = 0
633+
634+
val composePanel = ComposePanel()
635+
composePanel.setContent {
636+
Box(
637+
Modifier
638+
.size(200.dp)
639+
.background(Color.Yellow)
640+
.clickable { backgroundClickCount++ }
641+
)
642+
643+
if (showPopup) {
644+
Popup(
645+
properties = PopupProperties(
646+
focusable = true,
647+
dismissOnClickOutside = false,
648+
consumePointerInputOutside = false,
649+
),
650+
) {
651+
Box(
652+
Modifier
653+
.size(50.dp)
654+
.background(Color.Blue)
655+
)
656+
}
657+
}
658+
}
659+
660+
composePanel.windowContainer = window.layeredPane
661+
662+
window.contentPane.add(composePanel, BorderLayout.CENTER)
663+
window.pack()
664+
window.isVisible = true
665+
666+
awaitIdle()
667+
668+
showPopup = true
669+
awaitIdle()
670+
671+
window.layeredPane.sendMousePress(x = 100, y = 100)
672+
window.layeredPane.sendMouseRelease(x = 100, y = 100)
673+
awaitIdle()
674+
675+
assertEquals(1, backgroundClickCount)
676+
}
677+
} finally {
678+
window.dispose()
679+
}
680+
}
681+
}
682+
}
683+
623684
@Test
624685
fun unfocusablePopupLayer_withComponentLayerType_inComposePanel_isSizedCorrectly() {
625686
ComposeFeatureFlags.layerType.withOverride(LayerType.OnComponent) {

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,11 @@ internal class ComposeContainer(
297297
override val platformContext: PlatformContext = platformContext
298298

299299
override fun createLayer(
300+
compositionContext: CompositionContext,
300301
density: Density,
301302
layoutDirection: LayoutDirection,
302303
focusable: Boolean,
303-
compositionContext: CompositionContext
304+
consumePointerInputOutside: Boolean,
304305
): ComposeSceneLayer {
305306
val layer = UIKitComposeSceneLayer(
306307
onClosed = {
@@ -314,6 +315,7 @@ internal class ComposeContainer(
314315
configuration = configuration,
315316
onAccessibilityChanged = ::onAccessibilityChanged,
316317
focusedViewsList = if (focusable) focusedViewsList.childFocusedViewsList() else null,
318+
consumePointerInputOutside = consumePointerInputOutside,
317319
parentCoroutineContext = compositionContext.effectCoroutineContext,
318320
ownerProvider = architectureComponentsOwner,
319321
interfaceOrientationState = interfaceOrientationState,

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/UIKitComposeSceneLayer.ios.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ internal class UIKitComposeSceneLayer(
5858
private val onAccessibilityChanged: () -> Unit,
5959
configuration: ComposeContainerConfiguration,
6060
private var focusedViewsList: FocusedViewsList?,
61+
consumePointerInputOutside: Boolean = focusedViewsList != null,
6162
parentCoroutineContext: CoroutineContext,
6263
private val ownerProvider: PlatformArchitectureComponentsOwner,
6364
private val interfaceOrientationState: State<InterfaceOrientation>,
@@ -66,6 +67,14 @@ internal class UIKitComposeSceneLayer(
6667
private val layerCoroutineContext = parentCoroutineContext + layerJob
6768

6869
override var focusable: Boolean = focusedViewsList != null
70+
set(value) {
71+
if (field != value) {
72+
field = value
73+
onAccessibilityChanged()
74+
}
75+
}
76+
77+
override var consumePointerInputOutside: Boolean = consumePointerInputOutside
6978
set(value) {
7079
if (field != value) {
7180
field = value
@@ -102,7 +111,7 @@ internal class UIKitComposeSceneLayer(
102111
interfaceOrientationState = interfaceOrientationState
103112
).also {
104113
interactionView.embedSubview(it.backgroundView)
105-
it.isInterceptingOutsideEvents = focusable
114+
it.isInterceptingOutsideEvents = consumePointerInputOutside
106115
}
107116

108117
private fun createComposeScene(
@@ -228,4 +237,4 @@ internal class UIKitComposeSceneLayer(
228237
fun sceneWillDisappear() {
229238
mediator.sceneWillDisappear()
230239
}
231-
}
240+
}

0 commit comments

Comments
 (0)