From 75c2931c8d1a03fa6218ea1c5bada58558fa3509 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 30 Jan 2026 10:03:05 -0400 Subject: [PATCH] Ensure placeables are only measured once in call elements screen. --- .../webrtc/v2/CallElementsLayout.kt | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt index 8a0bb54ef6..6eb9aaa46b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallElementsLayout.kt @@ -35,6 +35,17 @@ import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +/** + * Enum identifying each slot in the BlurrableContentLayer. + * Used as subcomposition keys to ensure each slot is only composed once. + */ +private enum class BlurrableContentSlot { + BARS, + GRID, + REACTIONS, + OVERFLOW +} + @Composable fun CallElementsLayout( callGridSlot: @Composable () -> Unit, @@ -71,30 +82,14 @@ fun CallElementsLayout( } SubcomposeLayout(modifier = modifier) { constraints -> - // First, measure the bars to get their height val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) - // Measure overflow to determine constraints for bars - val overflowPlaceables = subcompose("overflow_measure", callOverflowSlot).map { it.measure(looseConstraints) } - val constrainedHeightOffset = if (isPortrait) overflowPlaceables.maxOfOrNull { it.height } ?: 0 else 0 - val constrainedWidthOffset = if (isPortrait) { 0 } else overflowPlaceables.maxOfOrNull { it.width } ?: 0 + // Holder to capture measurements from BlurrableContentLayer + var measuredBarsHeightPx = 0 + var measuredBarsWidthPx = 0 - val nonOverflowConstraints = looseConstraints.offset(horizontal = -constrainedWidthOffset, vertical = -constrainedHeightOffset) - - val barConstraints = if (bottomInsetPx > constrainedHeightOffset) { - looseConstraints.offset(-constrainedWidthOffset, -bottomInsetPx) - } else { - nonOverflowConstraints - } - - val barsMaxWidth = minOf(barConstraints.maxWidth, bottomSheetWidthPx) - val barsConstrainedToSheet = barConstraints.copy(maxWidth = barsMaxWidth) - - val barsPlaceables = subcompose("bars_measure") { Bars() }.map { it.measure(barsConstrainedToSheet) } - val barsHeightPx = barsPlaceables.sumOf { it.height } - val barsWidthPx = barsPlaceables.maxOfOrNull { it.width } ?: 0 - - // Now compose and measure the actual layers with the bars height available + // Subcompose and measure the blurrable layer first - it will measure all content internally + // and report back the bars dimensions via the onMeasured callback val blurrableLayerPlaceable = subcompose("blurrable") { BlurrableContentLayer( isFocused = isFocused, @@ -104,19 +99,23 @@ fun CallElementsLayout( barsSlot = { Bars() }, callGridSlot = callGridSlot, reactionsSlot = reactionsSlot, - callOverflowSlot = callOverflowSlot + callOverflowSlot = callOverflowSlot, + onMeasured = { barsHeight, barsWidth -> + measuredBarsHeightPx = barsHeight + measuredBarsWidthPx = barsWidth + } ) }.map { it.measure(constraints) } // Use the wider of bars or bottom sheet for space calculation - val centeredContentWidthPx = maxOf(barsWidthPx, bottomSheetWidthPx) + val centeredContentWidthPx = maxOf(measuredBarsWidthPx, bottomSheetWidthPx) val pipLayerPlaceable = subcompose("pip") { PipLayer( pictureInPictureSlot = pictureInPictureSlot, localRenderState = localRenderState, bottomInsetPx = bottomInsetPx, - barsHeightPx = barsHeightPx, + barsHeightPx = measuredBarsHeightPx, pipSizePx = pipSizePx, centeredContentWidthPx = centeredContentWidthPx ) @@ -129,13 +128,14 @@ fun CallElementsLayout( } } -private enum class BlurrableContentSlot { - BARS, - GRID, - REACTIONS, - OVERFLOW -} - +/** + * A layer that contains content which can be blurred when the local participant video is focused. + * All slots are subcomposed here ONCE to avoid duplicate subcomposition that would cause + * IllegalArgumentException when slots contain SubcomposeLayout (like BoxWithConstraints). + * + * @param onMeasured Callback invoked during measurement with (barsHeight, barsWidth) to report + * dimensions needed by the parent layout for PipLayer positioning. + */ @Composable private fun BlurrableContentLayer( isFocused: Boolean, @@ -145,7 +145,8 @@ private fun BlurrableContentLayer( barsSlot: @Composable () -> Unit, callGridSlot: @Composable () -> Unit, reactionsSlot: @Composable () -> Unit, - callOverflowSlot: @Composable () -> Unit + callOverflowSlot: @Composable () -> Unit, + onMeasured: (barsHeight: Int, barsWidth: Int) -> Unit ) { BlurContainer( isBlurred = isFocused, @@ -176,8 +177,13 @@ private fun BlurrableContentLayer( val barsPlaceables = subcompose(BlurrableContentSlot.BARS, barsSlot) .map { it.measure(barsConstrainedToSheet) } - val barsHeightOffset = barsPlaceables.sumOf { it.height } - val reactionsConstraints = barConstraints.offset(vertical = -barsHeightOffset) + val barsHeightPx = barsPlaceables.sumOf { it.height } + val barsWidthPx = barsPlaceables.maxOfOrNull { it.width } ?: 0 + + // Report measurements to parent for PipLayer positioning + onMeasured(barsHeightPx, barsWidthPx) + + val reactionsConstraints = barConstraints.offset(vertical = -barsHeightPx) val reactionsPlaceables = subcompose(BlurrableContentSlot.REACTIONS, reactionsSlot) .map { it.measure(reactionsConstraints) }