Skip to content

Commit 077d3a5

Browse files
authored
Expose ComposeScene.measurableContent to allow querying the scene for its size properties (JetBrains#2986)
1 parent 6fd60ff commit 077d3a5

7 files changed

Lines changed: 123 additions & 32 deletions

File tree

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,11 @@ internal class ComposeSceneMediator(
344344
ComposeFeatureFlags.redispatchUnconsumedMouseWheelEvents.value
345345

346346
/**
347-
* Provides the size of ComposeScene content inside infinity constraints
347+
* Provides the size of the scene content in infinity constraints.
348348
*
349-
* This is needed for the bridge between Compose and Swing since
350-
* in some cases, Swing's LayoutManagers need
351-
* to calculate the preferred size of the content without max/min constraints
352-
* to properly lay it out.
349+
* This is needed for the bridge between Compose and Swing since in some cases, Swing's
350+
* LayoutManagers need to calculate the preferred size of the content without max/min
351+
* constraints to properly lay it out.
353352
*
354353
* Example: Compose content inside Popup without a preferred size.
355354
* Swing will calculate the preferred size of the Compose content and set Popup's side for that.
@@ -358,7 +357,7 @@ internal class ComposeSceneMediator(
358357
*/
359358
val preferredSize: Dimension
360359
get() {
361-
val contentSize = scene.calculateContentSize()
360+
val contentSize = scene.unconstrainedSize()
362361
val scale = scene.density.density
363362
return Dimension(
364363
(contentSize.width / scale).toInt(),

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.ui.platform.WindowInfoImpl
3737
import androidx.compose.ui.scene.CanvasLayersComposeScene
3838
import androidx.compose.ui.scene.ComposeScene
3939
import androidx.compose.ui.scene.ComposeScenePointer
40+
import androidx.compose.ui.scene.unconstrainedSize
4041
import androidx.compose.ui.semantics.SemanticsOwner
4142
import androidx.compose.ui.unit.Constraints
4243
import androidx.compose.ui.unit.Density
@@ -258,7 +259,7 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor(
258259
*/
259260
@Deprecated("Use calculateContentSize() instead", replaceWith = ReplaceWith("calculateContentSize()"))
260261
val contentSize: IntSize
261-
get() = scene.calculateContentSize()
262+
get() = scene.unconstrainedSize()
262263

263264
/**
264265
* Returns the current content size in infinity constraints.
@@ -268,7 +269,7 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor(
268269
*/
269270
@ExperimentalComposeUiApi
270271
fun calculateContentSize(): IntSize {
271-
return scene.calculateContentSize()
272+
return scene.unconstrainedSize()
272273
}
273274

274275
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.ui.layout
18+
19+
import androidx.compose.ui.InternalComposeUiApi
20+
import androidx.compose.ui.unit.Constraints
21+
22+
/**
23+
* The interface through which composable content can be queried for its size preferences, such as
24+
* its intrinsic size.
25+
*/
26+
@InternalComposeUiApi
27+
interface MeasurableRootContent : IntrinsicMeasurable {
28+
/**
29+
* Measures the content with the given constraints and calls [block] on the resulting
30+
* [Measured].
31+
*
32+
* Returns the result of [block].
33+
*
34+
* It is recommended to not hold onto the [Measured] instance beyond the lifetime of the call
35+
* to [block].
36+
*/
37+
fun <T> measuringIn(constraints: Constraints, block: (Measured) -> T): T
38+
}

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

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
6262
import androidx.compose.ui.input.pointer.PointerType
6363
import androidx.compose.ui.input.pointer.PositionCalculator
6464
import androidx.compose.ui.input.rotary.RotaryScrollEvent
65+
import androidx.compose.ui.layout.MeasurableRootContent
66+
import androidx.compose.ui.layout.Measured
6567
import androidx.compose.ui.layout.RootMeasurePolicy
6668
import androidx.compose.ui.layout.RulerProviderModifierElement
6769
import androidx.compose.ui.modifier.ModifierLocalManager
@@ -204,22 +206,58 @@ internal class RootNodeOwner(
204206
}
205207
}
206208

209+
val measurableRootContent: MeasurableRootContent = object : MeasurableRootContent {
210+
override val parentData
211+
get() = null
212+
213+
override fun minIntrinsicWidth(height: Int): Int {
214+
// RootMeasurePolicy has LayoutNode.NoIntrinsicsMeasurePolicy, so we ask the children
215+
return owner.root.children.fastMaxOfOrDefault(0) {
216+
it.outerCoordinator.minIntrinsicWidth(height)
217+
}
218+
}
219+
220+
override fun minIntrinsicHeight(width: Int): Int {
221+
// RootMeasurePolicy has LayoutNode.NoIntrinsicsMeasurePolicy, so we ask the children
222+
return owner.root.children.fastMaxOfOrDefault(0) {
223+
it.outerCoordinator.minIntrinsicHeight(width)
224+
}
225+
}
226+
227+
override fun maxIntrinsicWidth(height: Int): Int {
228+
// RootMeasurePolicy has LayoutNode.NoIntrinsicsMeasurePolicy, so we ask the children
229+
return owner.root.children.fastMaxOfOrDefault(0) {
230+
it.outerCoordinator.maxIntrinsicWidth(height)
231+
}
232+
}
233+
234+
override fun maxIntrinsicHeight(width: Int): Int {
235+
// RootMeasurePolicy has LayoutNode.NoIntrinsicsMeasurePolicy, so we ask the children
236+
return owner.root.children.fastMaxOfOrDefault(0) {
237+
it.outerCoordinator.maxIntrinsicHeight(width)
238+
}
239+
}
240+
241+
override fun <T> measuringIn(constraints: Constraints, block: (Measured) -> T): T {
242+
return measuringRootWithConstraints(constraints) {
243+
block(it.outerCoordinator)
244+
}
245+
}
246+
}
247+
207248
/**
208249
* Provides a way to measure Owner's content in given [constraints]
209250
* Draw/pointer and other callbacks won't be called here like in [measureAndLayout] functions
210251
*/
211-
fun measureInConstraints(constraints: Constraints): IntSize {
212-
try {
252+
private fun <T> measuringRootWithConstraints(
253+
constraints: Constraints,
254+
block: (LayoutNode) -> T
255+
): T {
256+
return try {
213257
// TODO: is it possible to measure without reassigning root constraints?
214258
measureAndLayoutDelegate.updateRootConstraintsWithInfinityCheck(constraints)
215259
measureAndLayoutDelegate.measureOnly()
216-
217-
// Don't use mainOwner.root.width here, as it strictly coerced by [constraints]
218-
val children = owner.root.children
219-
return IntSize(
220-
width = children.fastMaxOfOrDefault(0) { it.outerCoordinator.measuredWidth },
221-
height = children.fastMaxOfOrDefault(0) { it.outerCoordinator.measuredHeight },
222-
)
260+
block(owner.root)
223261
} finally {
224262
measureAndLayoutDelegate.updateRootConstraintsWithInfinityCheck(size?.toConstraints())
225263
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ 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.layout.MeasurableRootContent
4142
import androidx.compose.ui.node.InternalCoreApi
4243
import androidx.compose.ui.node.RootNodeOwner
4344
import androidx.compose.ui.platform.PlatformContext
4445
import androidx.compose.ui.platform.setContent
45-
import androidx.compose.ui.unit.Constraints
4646
import androidx.compose.ui.unit.Density
4747
import androidx.compose.ui.unit.Dp
4848
import androidx.compose.ui.unit.IntOffset
@@ -199,10 +199,11 @@ private class CanvasLayersComposeSceneImpl(
199199
super.close()
200200
}
201201

202-
override fun calculateContentSize(): IntSize {
203-
check(!isClosed) { "calculateContentSize called after ComposeScene is closed" }
204-
return mainOwner.measureInConstraints(Constraints())
205-
}
202+
override val measurableContent: MeasurableRootContent
203+
get() {
204+
check(!isClosed) { "measurableContent requested after ComposeScene is closed" }
205+
return mainOwner.measurableRootContent
206+
}
206207

207208
override fun invalidatePositionInWindow() {
208209
check(!isClosed) { "invalidatePositionInWindow called after ComposeScene is closed" }

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ import androidx.compose.ui.input.pointer.PointerEventType
3737
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
3838
import androidx.compose.ui.input.pointer.PointerType
3939
import androidx.compose.ui.input.rotary.RotaryScrollEvent
40+
import androidx.compose.ui.layout.MeasurableRootContent
4041
import androidx.compose.ui.layout.onGloballyPositioned
4142
import androidx.compose.ui.node.LayoutNode
4243
import androidx.compose.ui.platform.LocalLayoutDirection
4344
import androidx.compose.ui.platform.PlatformContext
4445
import androidx.compose.ui.platform.PlatformDragAndDropManager
46+
import androidx.compose.ui.unit.Constraints
4547
import androidx.compose.ui.unit.Density
4648
import androidx.compose.ui.unit.Dp
4749
import androidx.compose.ui.unit.IntSize
@@ -133,12 +135,10 @@ sealed interface ComposeScene : AutoCloseable {
133135
override fun close()
134136

135137
/**
136-
* Returns the current content size (in pixels) in infinity constraints.
137-
*
138-
* @throws IllegalStateException when [ComposeScene] content has lazy layouts without maximum
139-
* size bounds (e.g. LazyColumn without maximum height).
138+
* An object through which the composable content of the scene can be queried for its size
139+
* properties.
140140
*/
141-
fun calculateContentSize(): IntSize
141+
val measurableContent: MeasurableRootContent
142142

143143
/**
144144
* Invalidates position of [ComposeScene] in window. It will trigger callbacks like
@@ -309,3 +309,16 @@ sealed interface ComposeScene : AutoCloseable {
309309
*/
310310
var showLayoutBounds: Boolean
311311
}
312+
313+
/**
314+
* Returns the current content size (in pixels) in infinity constraints.
315+
*
316+
* @throws IllegalStateException when [ComposeScene] content has lazy layouts without maximum
317+
* size bounds (e.g., LazyColumn without maximum height).
318+
*/
319+
@InternalComposeUiApi
320+
fun ComposeScene.unconstrainedSize(): IntSize {
321+
return measurableContent.measuringIn(Constraints()) {
322+
IntSize(it.measuredWidth, it.measuredHeight)
323+
}
324+
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import androidx.compose.ui.graphics.Canvas
2626
import androidx.compose.ui.input.key.KeyEvent
2727
import androidx.compose.ui.input.pointer.PointerInputEvent
2828
import androidx.compose.ui.input.rotary.RotaryScrollEvent
29+
import androidx.compose.ui.layout.MeasurableRootContent
2930
import androidx.compose.ui.node.InternalCoreApi
3031
import androidx.compose.ui.node.LayoutNode
3132
import androidx.compose.ui.node.RootNodeOwner
3233
import androidx.compose.ui.platform.setContent
33-
import androidx.compose.ui.unit.Constraints
3434
import androidx.compose.ui.unit.Density
3535
import androidx.compose.ui.unit.Dp
3636
import androidx.compose.ui.unit.IntSize
@@ -145,10 +145,11 @@ private class PlatformLayersComposeSceneImpl(
145145
super.close()
146146
}
147147

148-
override fun calculateContentSize(): IntSize {
149-
check(!isClosed) { "calculateContentSize called after ComposeScene is closed" }
150-
return mainOwner.measureInConstraints(Constraints())
151-
}
148+
override val measurableContent: MeasurableRootContent
149+
get() {
150+
check(!isClosed) { "measurableContent requested after ComposeScene is closed" }
151+
return mainOwner.measurableRootContent
152+
}
152153

153154
override fun invalidatePositionInWindow() {
154155
check(!isClosed) { "invalidatePositionInWindow called after ComposeScene is closed" }

0 commit comments

Comments
 (0)