From 0191ae1e9f00043ffe6284fcc2a34050bd7d10bd Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Tue, 11 Nov 2025 18:46:04 +0100 Subject: [PATCH] CMP-9264 Expose showLayoutBounds API on ComposePanel This exposes a showLayoutBounds property on ComposePanel (desktop only) that allows users to turn on or off the showLayoutBounds property on the root layout node owner(s). The change mostly pipes the property up to the ComposePanel, but has no public API impact. The new property is also annotated as experimental. The Skiko RootNodeOwner's showLayoutBounds is now also a state, to cause a full recomposition when the value changes and see it reflected in the UI. --- .../compose/ui/awt/ComposeDialog.desktop.kt | 13 ++++++++++++ .../compose/ui/awt/ComposePanel.desktop.kt | 20 ++++++++++++++++++- .../compose/ui/awt/ComposeWindow.desktop.kt | 13 ++++++++++++ .../ui/awt/ComposeWindowPanel.desktop.kt | 8 ++++++++ .../ui/scene/ComposeContainer.desktop.kt | 2 ++ .../ui/scene/ComposeSceneMediator.desktop.kt | 6 ++++++ .../compose/ui/node/RootNodeOwner.skiko.kt | 2 +- .../scene/CanvasLayersComposeScene.skiko.kt | 10 ++++++++++ .../compose/ui/scene/ComposeScene.skiko.kt | 5 +++++ .../scene/PlatformLayersComposeScene.skiko.kt | 8 ++++++++ 10 files changed, 85 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt index 15ede8f14ee8a..979cfe9dcfa32 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeDialog.desktop.kt @@ -18,6 +18,7 @@ package androidx.compose.ui.awt import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.layout.layoutId @@ -349,4 +350,16 @@ class ComposeDialog : JDialog { override fun removeMouseWheelListener(listener: MouseWheelListener) = composePanel.removeMouseWheelListener(listener) + + /** + * Set the visual debug option that shows bounds for all nodes in the hierarchy. + */ + @InternalComposeUiApi + var showLayoutBounds: Boolean + get() { + return composePanel.showLayoutBounds + } + set(value) { + composePanel.showLayoutBounds = value + } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 54a036e616d94..9bff2cc65ab02 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -20,11 +20,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.ComposeFeatureFlags import androidx.compose.ui.ComposeUiFlags import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.LayerType import androidx.compose.ui.awt.RenderSettings.SkiaSurface import androidx.compose.ui.awt.RenderSettings.SwingGraphics import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.isClearFocusOnMouseDownEnabled +import androidx.compose.ui.node.InternalCoreApi import androidx.compose.ui.scene.ComposeContainer import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.window.WindowExceptionHandler @@ -183,7 +185,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( override fun getPreferredSize(): Dimension? = if (isPreferredSizeSet) { super.getPreferredSize() - } else { + } else { _composeContainer?.preferredSize ?: Dimension(0, 0) } @@ -252,6 +254,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // content. val composeContainer = _composeContainer ?: createComposeContainer().also { _composeContainer = it + @OptIn(InternalCoreApi::class) + it.showLayoutBounds = showLayoutBounds val composeContent = _composeContent if (composeContent != null) { it.setContent(composeContent) @@ -284,6 +288,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( focusManager.takeFocus(FocusDirection.Next) } } + else -> Unit } } @@ -392,4 +397,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor( fun renderImmediately() { _composeContainer?.renderImmediately() } + + /** + * Set the visual debug option that shows bounds for all nodes in the hierarchy. + */ + @InternalComposeUiApi + var showLayoutBounds: Boolean = _composeContainer?.showLayoutBounds ?: false + set(value) { + // We're assuming we own the scene and thus this value, and nobody + // else will change it from under us, so we never get out of sync. + field = value + @OptIn(InternalCoreApi::class) + _composeContainer?.showLayoutBounds = value + } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt index 5db0898066bc7..0f5327d5bc8f8 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindow.desktop.kt @@ -18,6 +18,7 @@ package androidx.compose.ui.awt import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.layout.layoutId @@ -333,4 +334,16 @@ class ComposeWindow @ExperimentalComposeUiApi constructor( override fun removeMouseWheelListener(listener: MouseWheelListener) = composePanel.removeMouseWheelListener(listener) + + /** + * Set the visual debug option that shows bounds for all nodes in the hierarchy. + */ + @InternalComposeUiApi + var showLayoutBounds: Boolean + get() { + return composePanel.showLayoutBounds + } + set(value) { + composePanel.showLayoutBounds = value + } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt index b48caed9f06b6..2002f6cce1e91 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt @@ -192,4 +192,12 @@ internal class ComposeWindowPanel( override fun removeMouseMotionListener(listener: MouseMotionListener) { contentComponent.removeMouseMotionListener(listener) } + + var showLayoutBounds: Boolean + get() { + return _composeContainer?.showLayoutBounds ?: false + } + set(value) { + _composeContainer?.showLayoutBounds = value + } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt index d503cc6ffddaf..750bff973943d 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt @@ -176,6 +176,8 @@ internal class ComposeContainer( val preferredSize by mediator::preferredSize val semanticsOwners by mediator::semanticsOwners + var showLayoutBounds by mediator::showLayoutBounds + private var isDisposed = false private var isDetached = true private var isMinimized = false diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index d30b4a0d3ee77..3d9f18f1680e9 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -325,6 +325,12 @@ internal class ComposeSceneMediator( var compositionLocalContext: CompositionLocalContext? get() = scene.compositionLocalContext set(value) { scene.compositionLocalContext = value } + var showLayoutBounds: Boolean + get() = scene.showLayoutBounds + set(value) { + scene.showLayoutBounds = value + } + /** * Provides the size of ComposeScene content inside infinity constraints diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index 3518e713ad5ee..57abf548f3fea 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -498,7 +498,7 @@ internal class RootNodeOwner( override val fontLoader = androidx.compose.ui.text.platform.FontLoader() override val fontFamilyResolver = createFontFamilyResolver() override val layoutDirection get() = _layoutDirection - override var showLayoutBounds = false + override var showLayoutBounds by mutableStateOf(false) @InternalCoreApi set diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt index 2c03bc11163a9..5430da677de7b 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerInputEvent import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.rotary.RotaryScrollEvent +import androidx.compose.ui.node.InternalCoreApi import androidx.compose.ui.node.RootNodeOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.setContent @@ -273,6 +274,13 @@ private class CanvasLayersComposeSceneImpl( forEachOwner { it.draw(canvas) } } + override var showLayoutBounds: Boolean = false + @OptIn(InternalCoreApi::class) + set(value) { + field = value + forEachOwner { it.owner.showLayoutBounds = value } + } + /** * Find hovered owner for position of first pointer. */ @@ -560,6 +568,8 @@ private class CanvasLayersComposeSceneImpl( private var onKeyEvent: ((KeyEvent) -> Boolean)? = null init { + @OptIn(InternalCoreApi::class) + owner.owner.showLayoutBounds = showLayoutBounds attachLayer(this) } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt index b044c6ce2b455..af5a5bc92917a 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt @@ -284,4 +284,9 @@ sealed interface ComposeScene : AutoCloseable { * provided by the [androidx.compose.runtime.Recomposer] of the current scene. */ suspend fun withMonotonicFrameClock(block: suspend () -> Unit) + + /** + * Set the visual debug option that shows bounds for all nodes in the hierarchy. + */ + var showLayoutBounds: Boolean } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt index 78cc24a760d3e..8bf620cfd6529 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.pointer.PointerInputEvent import androidx.compose.ui.input.rotary.RotaryScrollEvent +import androidx.compose.ui.node.InternalCoreApi import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.node.RootNodeOwner import androidx.compose.ui.platform.setContent @@ -192,6 +193,13 @@ private class PlatformLayersComposeSceneImpl( mainOwner.draw(canvas) } + override var showLayoutBounds: Boolean + get() = mainOwner.owner.showLayoutBounds + set(value) { + @OptIn(InternalCoreApi::class) + mainOwner.owner.showLayoutBounds = value + } + private fun onOwnerAppended(owner: RootNodeOwner) { semanticsOwnerListener?.onSemanticsOwnerAppended(owner.semanticsOwner) }