Skip to content

Commit fdfc226

Browse files
committed
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.
1 parent 4c05dc6 commit fdfc226

File tree

8 files changed

+118
-64
lines changed

8 files changed

+118
-64
lines changed

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.ui.LayerType
2323
import androidx.compose.ui.awt.RenderSettings.SkiaSurface
2424
import androidx.compose.ui.awt.RenderSettings.SwingGraphics
2525
import androidx.compose.ui.focus.FocusDirection
26+
import androidx.compose.ui.node.InternalCoreApi
2627
import androidx.compose.ui.scene.ComposeContainer
2728
import androidx.compose.ui.semantics.SemanticsOwner
2829
import androidx.compose.ui.window.WindowExceptionHandler
@@ -171,7 +172,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
171172

172173
override fun getPreferredSize(): Dimension? = if (isPreferredSizeSet) {
173174
super.getPreferredSize()
174-
} else {
175+
} else {
175176
_composeContainer?.preferredSize ?: Dimension(0, 0)
176177
}
177178

@@ -378,4 +379,14 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
378379
fun renderImmediately() {
379380
_composeContainer?.renderImmediately()
380381
}
382+
383+
@ExperimentalComposeUiApi
384+
var showLayoutBounds: Boolean
385+
get() {
386+
return _composeContainer?.showLayoutBounds ?: false
387+
}
388+
@InternalCoreApi
389+
set(value) {
390+
_composeContainer?.showLayoutBounds = value
391+
}
381392
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.awt.AwtEventListener
2525
import androidx.compose.ui.awt.AwtEventListeners
2626
import androidx.compose.ui.awt.RenderSettings
2727
import androidx.compose.ui.input.key.KeyEvent
28+
import androidx.compose.ui.node.InternalCoreApi
2829
import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner
2930
import androidx.compose.ui.platform.PlatformContext
3031
import androidx.compose.ui.platform.PlatformWindowContext
@@ -176,6 +177,9 @@ internal class ComposeContainer(
176177
val preferredSize by mediator::preferredSize
177178
val semanticsOwners by mediator::semanticsOwners
178179

180+
@set:InternalCoreApi
181+
var showLayoutBounds by mediator::showLayoutBounds
182+
179183
private var isDisposed = false
180184
private var isDetached = true
181185
private var isMinimized = false

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

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import androidx.compose.ui.input.pointer.PointerIcon
4343
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
4444
import androidx.compose.ui.input.pointer.PointerType
4545
import androidx.compose.ui.navigationevent.BackNavigationEventInput
46+
import androidx.compose.ui.node.InternalCoreApi
4647
import androidx.compose.ui.platform.AwtDragAndDropManager
4748
import androidx.compose.ui.platform.DefaultInputModeManager
4849
import androidx.compose.ui.platform.DelegateRootForTestListener
@@ -322,7 +323,15 @@ internal class ComposeSceneMediator(
322323
val focusManager get() = scene.focusManager
323324
var compositionLocalContext: CompositionLocalContext?
324325
get() = scene.compositionLocalContext
325-
set(value) { scene.compositionLocalContext = value }
326+
set(value) {
327+
scene.compositionLocalContext = value
328+
}
329+
var showLayoutBounds: Boolean
330+
get() = scene.showLayoutBounds
331+
@InternalCoreApi set(value) {
332+
scene.showLayoutBounds = value
333+
}
334+
326335

327336
/**
328337
* Provides the size of ComposeScene content inside infinity constraints
@@ -493,7 +502,7 @@ internal class ComposeSceneMediator(
493502
/**
494503
* Returns the first heavyweight ancestor of the given component.
495504
*/
496-
private fun Component.heavyWeightAncestorOrNull() : Component? {
505+
private fun Component.heavyWeightAncestorOrNull(): Component? {
497506
var parent = parent
498507
while (parent != null) {
499508
if (!parent.isLightweight) return parent
@@ -671,13 +680,14 @@ internal class ComposeSceneMediator(
671680
scene.layoutDirection = layoutDirection
672681
}
673682

674-
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) = catchExceptions {
675-
interopContainer.postponingExecutingScheduledUpdates {
676-
canvas.withSceneOffset {
677-
scene.render(asComposeCanvas(), nanoTime)
683+
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) =
684+
catchExceptions {
685+
interopContainer.postponingExecutingScheduledUpdates {
686+
canvas.withSceneOffset {
687+
scene.render(asComposeCanvas(), nanoTime)
688+
}
678689
}
679690
}
680-
}
681691

682692
private inline fun Canvas.withSceneOffset(block: Canvas.() -> Unit) {
683693
// Offset of scene relative to [container]
@@ -706,7 +716,8 @@ internal class ComposeSceneMediator(
706716
keyboardModifiersRequireUpdate = true
707717
}
708718

709-
private inner class DesktopViewConfiguration : ViewConfiguration by PlatformContext.DefaultViewConfiguration {
719+
private inner class DesktopViewConfiguration :
720+
ViewConfiguration by PlatformContext.DefaultViewConfiguration {
710721
override val touchSlop: Float get() = with(platformComponent.density) { 18.dp.toPx() }
711722
}
712723

@@ -747,7 +758,8 @@ internal class ComposeSceneMediator(
747758
* A new [SemanticsOwner] is always created above existing ones. So, usage of [LinkedHashMap]
748759
* is required here to keep insertion-order (that equal to [SemanticsOwner]s order).
749760
*/
750-
private val _accessibilityControllers = linkedMapOf<SemanticsOwner, AccessibilityController>()
761+
private val _accessibilityControllers =
762+
linkedMapOf<SemanticsOwner, AccessibilityController>()
751763
val accessibilityControllers get() = _accessibilityControllers.values.reversed()
752764

753765
val semanticsOwners = mutableStateSetOf<SemanticsOwner>()
@@ -797,7 +809,8 @@ internal class ComposeSceneMediator(
797809
override fun convertScreenToLocalPosition(positionOnScreen: Offset): Offset =
798810
windowContext.convertScreenToLocalPosition(container, positionOnScreen)
799811

800-
override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds
812+
override val measureDrawLayerBounds: Boolean =
813+
this@ComposeSceneMediator.measureDrawLayerBounds
801814
override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration()
802815
override val inputModeManager: InputModeManager = DefaultInputModeManager()
803816
override val textInputService = this@ComposeSceneMediator.textInputService
@@ -893,19 +906,20 @@ private fun ComposeScene.onMouseEvent(
893906
)
894907
}
895908

896-
internal val MouseEvent.composePointerButton: PointerButton? get() {
897-
if (button == MouseEvent.NOBUTTON) return null
898-
return when (button) {
899-
MouseEvent.BUTTON2 -> PointerButton.Tertiary
900-
MouseEvent.BUTTON3 -> PointerButton.Secondary
901-
else -> PointerButton(button - 1)
909+
internal val MouseEvent.composePointerButton: PointerButton?
910+
get() {
911+
if (button == MouseEvent.NOBUTTON) return null
912+
return when (button) {
913+
MouseEvent.BUTTON2 -> PointerButton.Tertiary
914+
MouseEvent.BUTTON3 -> PointerButton.Secondary
915+
else -> PointerButton(button - 1)
916+
}
902917
}
903-
}
904918

905919
private fun ComposeScene.onMouseWheelEvent(
906920
position: Offset,
907921
event: MouseWheelEvent
908-
) : PointerEventResult {
922+
): PointerEventResult {
909923
return sendPointerEvent(
910924
eventType = PointerEventType.Scroll,
911925
position = position,
@@ -923,37 +937,39 @@ private fun ComposeScene.onMouseWheelEvent(
923937
}
924938

925939

926-
private val MouseEvent.buttons get() = PointerButtons(
927-
// We should check [event.button] because of case where [event.modifiersEx] does not provide
928-
// info about the pressed mouse button when using touchpad on MacOS 12 (AWT only).
929-
// When the [Tap to click] feature is activated on Mac OS 12, half of all clicks are not
930-
// handled because [event.modifiersEx] may not provide info about the pressed mouse button.
931-
isPrimaryPressed = ((modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
932-
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON1))
933-
&& !isMacOsCtrlClick,
934-
isSecondaryPressed = (modifiersEx and MouseEvent.BUTTON3_DOWN_MASK) != 0
935-
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3)
936-
|| isMacOsCtrlClick,
937-
isTertiaryPressed = (modifiersEx and MouseEvent.BUTTON2_DOWN_MASK) != 0
938-
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON2),
939-
isBackPressed = (modifiersEx and MouseEvent.getMaskForButton(4)) != 0
940-
|| (id == MouseEvent.MOUSE_PRESSED && button == 4),
941-
isForwardPressed = (modifiersEx and MouseEvent.getMaskForButton(5)) != 0
942-
|| (id == MouseEvent.MOUSE_PRESSED && button == 5),
943-
)
944-
945-
private val MouseEvent.keyboardModifiers get() = PointerKeyboardModifiers(
946-
isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0,
947-
isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0,
948-
isAltPressed = (modifiersEx and InputEvent.ALT_DOWN_MASK) != 0,
949-
isShiftPressed = (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0,
950-
isAltGraphPressed = (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) != 0,
951-
isSymPressed = false,
952-
isFunctionPressed = false,
953-
isCapsLockOn = getLockingKeyStateSafe(KeyEvent.VK_CAPS_LOCK),
954-
isScrollLockOn = getLockingKeyStateSafe(KeyEvent.VK_SCROLL_LOCK),
955-
isNumLockOn = getLockingKeyStateSafe(KeyEvent.VK_NUM_LOCK),
956-
)
940+
private val MouseEvent.buttons
941+
get() = PointerButtons(
942+
// We should check [event.button] because of case where [event.modifiersEx] does not provide
943+
// info about the pressed mouse button when using touchpad on MacOS 12 (AWT only).
944+
// When the [Tap to click] feature is activated on Mac OS 12, half of all clicks are not
945+
// handled because [event.modifiersEx] may not provide info about the pressed mouse button.
946+
isPrimaryPressed = ((modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
947+
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON1))
948+
&& !isMacOsCtrlClick,
949+
isSecondaryPressed = (modifiersEx and MouseEvent.BUTTON3_DOWN_MASK) != 0
950+
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3)
951+
|| isMacOsCtrlClick,
952+
isTertiaryPressed = (modifiersEx and MouseEvent.BUTTON2_DOWN_MASK) != 0
953+
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON2),
954+
isBackPressed = (modifiersEx and MouseEvent.getMaskForButton(4)) != 0
955+
|| (id == MouseEvent.MOUSE_PRESSED && button == 4),
956+
isForwardPressed = (modifiersEx and MouseEvent.getMaskForButton(5)) != 0
957+
|| (id == MouseEvent.MOUSE_PRESSED && button == 5),
958+
)
959+
960+
private val MouseEvent.keyboardModifiers
961+
get() = PointerKeyboardModifiers(
962+
isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0,
963+
isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0,
964+
isAltPressed = (modifiersEx and InputEvent.ALT_DOWN_MASK) != 0,
965+
isShiftPressed = (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0,
966+
isAltGraphPressed = (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) != 0,
967+
isSymPressed = false,
968+
isFunctionPressed = false,
969+
isCapsLockOn = getLockingKeyStateSafe(KeyEvent.VK_CAPS_LOCK),
970+
isScrollLockOn = getLockingKeyStateSafe(KeyEvent.VK_SCROLL_LOCK),
971+
isNumLockOn = getLockingKeyStateSafe(KeyEvent.VK_NUM_LOCK),
972+
)
957973

958974
private fun Component.subscribeToMouseEvents(mouseAdapter: MouseAdapter) {
959975
addMouseListener(mouseAdapter)

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package androidx.compose.ui.node
1818

1919
import androidx.collection.MutableIntObjectMap
2020
import androidx.collection.mutableIntObjectMapOf
21-
import androidx.compose.runtime.retain.RetainedValuesStore
2221
import androidx.compose.runtime.collection.mutableVectorOf
2322
import androidx.compose.runtime.getValue
2423
import androidx.compose.runtime.mutableStateOf
2524
import androidx.compose.runtime.retain.ForgetfulRetainedValuesStore
25+
import androidx.compose.runtime.retain.RetainedValuesStore
2626
import androidx.compose.runtime.setValue
2727
import androidx.compose.runtime.snapshotFlow
2828
import androidx.compose.runtime.snapshots.Snapshot
@@ -68,13 +68,13 @@ import androidx.compose.ui.platform.DefaultAccessibilityManager
6868
import androidx.compose.ui.platform.DefaultHapticFeedback
6969
import androidx.compose.ui.platform.DelegatingSoftwareKeyboardController
7070
import androidx.compose.ui.platform.GraphicsLayerOwnerLayer
71+
import androidx.compose.ui.platform.LegacyRenderNodeLayer
7172
import androidx.compose.ui.platform.OwnedLayerManager
7273
import androidx.compose.ui.platform.PlatformClipboardManager
7374
import androidx.compose.ui.platform.PlatformContext
7475
import androidx.compose.ui.platform.PlatformRootForTest
7576
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
7677
import androidx.compose.ui.platform.PlatformTextInputSessionScope
77-
import androidx.compose.ui.platform.LegacyRenderNodeLayer
7878
import androidx.compose.ui.platform.createPlatformClipboard
7979
import androidx.compose.ui.platform.setLightingInfo
8080
import androidx.compose.ui.scene.ComposeScene
@@ -135,7 +135,8 @@ internal class RootNodeOwner(
135135
private val rootSemanticsNode = EmptySemanticsModifier()
136136
private val snapshotObserver = snapshotInvalidationTracker.snapshotObserver()
137137
private val graphicsContext = SkiaGraphicsContext(platformContext.measureDrawLayerBounds)
138-
private val coroutineScope = CoroutineScope(coroutineContext + Job(parent = coroutineContext[Job]))
138+
private val coroutineScope =
139+
CoroutineScope(coroutineContext + Job(parent = coroutineContext[Job]))
139140

140141
private val _owner = OwnerImpl(layoutDirection, coroutineContext)
141142
val owner: Owner get() = _owner
@@ -397,10 +398,10 @@ internal class RootNodeOwner(
397398
override val focusOwner: FocusOwner = FocusOwnerImpl(platformFocusOwner, this)
398399

399400
val rootModifier = if (ComposeUiFlags.areWindowInsetsRulersEnabled) {
400-
RulerProviderModifierElement(platformContext.windowInsets)
401-
} else {
402-
Modifier
403-
}
401+
RulerProviderModifierElement(platformContext.windowInsets)
402+
} else {
403+
Modifier
404+
}
404405
.then(EmptySemanticsElement(rootSemanticsNode))
405406
.focusProperties {
406407
onExit = {
@@ -480,7 +481,7 @@ internal class RootNodeOwner(
480481

481482
override suspend fun textInputSession(
482483
session: suspend PlatformTextInputSessionScope.() -> Nothing
483-
) : Nothing {
484+
): Nothing {
484485
textInputSessionMutex.withSessionCancellingPrevious<Nothing>(
485486
sessionInitializer = ::TextInputSession,
486487
session = session
@@ -498,7 +499,7 @@ internal class RootNodeOwner(
498499
override val fontLoader = androidx.compose.ui.text.platform.FontLoader()
499500
override val fontFamilyResolver = createFontFamilyResolver()
500501
override val layoutDirection get() = _layoutDirection
501-
override var showLayoutBounds = false
502+
override var showLayoutBounds by mutableStateOf(false)
502503
@InternalCoreApi
503504
set
504505

@@ -934,7 +935,8 @@ internal class RootNodeOwner(
934935
postponed.clear()
935936
}
936937

937-
val isAnyCurrentFrameRateSet = !currentFrameRate.isNaN() || currentFrameRateCategory != 0f
938+
val isAnyCurrentFrameRateSet =
939+
!currentFrameRate.isNaN() || currentFrameRateCategory != 0f
938940
if (ComposeUiFlags.isAdaptiveRefreshRateEnabled && isAnyCurrentFrameRateSet) {
939941
platformContext.voteFrameRate(currentFrameRate, currentFrameRateCategory)
940942
currentFrameRate = Float.NaN
@@ -994,7 +996,7 @@ private fun MeasureAndLayoutDelegate.updateRootConstraintsWithInfinityCheck(
994996

995997
private fun IntSize.toConstraints() = Constraints(maxWidth = width, maxHeight = height)
996998

997-
private object IdentityPositionCalculator: PositionCalculator {
999+
private object IdentityPositionCalculator : PositionCalculator {
9981000
override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
9991001
override fun localToScreen(localPosition: Offset): Offset = localPosition
10001002
}

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/BaseComposeScene.skiko.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable
2121
import androidx.compose.runtime.Composition
2222
import androidx.compose.runtime.CompositionContext
2323
import androidx.compose.runtime.CompositionLocalContext
24-
import androidx.compose.runtime.CompositionLocalProvider
2524
import androidx.compose.runtime.MonotonicFrameClock
2625
import androidx.compose.runtime.getValue
2726
import androidx.compose.runtime.mutableStateOf
@@ -40,8 +39,6 @@ import androidx.compose.ui.input.pointer.PointerType
4039
import androidx.compose.ui.input.rotary.RotaryScrollEvent
4140
import androidx.compose.ui.node.SnapshotInvalidationTracker
4241
import androidx.compose.ui.platform.GlobalSnapshotManager
43-
import androidx.compose.ui.platform.LocalPlatformScreenReader
44-
import androidx.compose.ui.platform.LocalPlatformWindowInsets
4542
import androidx.compose.ui.platform.ProvidePlatformCompositionLocals
4643
import androidx.compose.ui.util.trace
4744
import kotlin.concurrent.Volatile

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import androidx.compose.ui.input.pointer.PointerEventType
3838
import androidx.compose.ui.input.pointer.PointerInputEvent
3939
import androidx.compose.ui.input.pointer.PointerType
4040
import androidx.compose.ui.input.rotary.RotaryScrollEvent
41+
import androidx.compose.ui.node.InternalCoreApi
4142
import androidx.compose.ui.node.RootNodeOwner
4243
import androidx.compose.ui.platform.PlatformContext
4344
import androidx.compose.ui.platform.setContent
@@ -273,6 +274,13 @@ private class CanvasLayersComposeSceneImpl(
273274
forEachOwner { it.draw(canvas) }
274275
}
275276

277+
override var showLayoutBounds: Boolean = false
278+
@InternalCoreApi
279+
set(value) {
280+
field = value
281+
forEachOwner { it.owner.showLayoutBounds = value }
282+
}
283+
276284
/**
277285
* Find hovered owner for position of first pointer.
278286
*/
@@ -560,6 +568,8 @@ private class CanvasLayersComposeSceneImpl(
560568
private var onKeyEvent: ((KeyEvent) -> Boolean)? = null
561569

562570
init {
571+
@OptIn(InternalCoreApi::class)
572+
owner.owner.showLayoutBounds = showLayoutBounds
563573
attachLayer(this)
564574
}
565575

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,10 @@ sealed interface ComposeScene : AutoCloseable {
284284
* provided by the [androidx.compose.runtime.Recomposer] of the current scene.
285285
*/
286286
suspend fun withMonotonicFrameClock(block: suspend () -> Unit)
287+
288+
/**
289+
* Set the visual debug option that shows bounds for all nodes in the hierarchy.
290+
*/
291+
@set:InternalComposeUiApi
292+
var showLayoutBounds: Boolean
287293
}

0 commit comments

Comments
 (0)