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 24be022e43..cdf60a262d 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 @@ -16,11 +16,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Constraints @@ -35,6 +35,31 @@ import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +/** + * Mutable holder for bar dimensions, used to pass measurement results from + * BlurrableContentLayer to PipLayer during the same Layout measurement pass + * without requiring SubcomposeLayout. + */ +private class BarDimensions { + var heightPx: Int = 0 + var widthPx: Int = 0 +} + +/** + * Arranges call screen content in coordinated layers so the local PiP can avoid centered bars. + * + * @param callGridSlot Main call grid content. + * @param pictureInPictureSlot Local participant PiP content. + * @param reactionsSlot Reactions overlay content. + * @param raiseHandSlot Slot for the raised-hand bar. + * @param callLinkBarSlot Slot for the call-link bar. + * @param callOverflowSlot Overflow participants strip. + * @param audioIndicatorSlot Participant audio indicator content. + * @param bottomInset Bottom inset used to keep content clear of anchored UI. + * @param bottomSheetWidth Maximum width of centered bottom content. + * @param localRenderState Current local renderer mode. + * @param modifier Modifier applied to the root layout. + */ @Composable fun CallElementsLayout( callGridSlot: @Composable () -> Unit, @@ -71,48 +96,46 @@ fun CallElementsLayout( bottomSheetWidth.roundToPx() } - SubcomposeLayout(modifier = modifier) { constraints -> - // Holder to capture measurements from BlurrableContentLayer - var measuredBarsHeightPx = 0 - var measuredBarsWidthPx = 0 + val barDimensions = remember { BarDimensions() } - // 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, - isPortrait = isPortrait, - bottomInsetPx = bottomInsetPx, - bottomSheetWidthPx = bottomSheetWidthPx, - barsSlot = { Bars() }, - callGridSlot = callGridSlot, - reactionsSlot = reactionsSlot, - callOverflowSlot = callOverflowSlot, - audioIndicatorSlot = audioIndicatorSlot, - 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(measuredBarsWidthPx, bottomSheetWidthPx) - - val pipLayerPlaceable = subcompose("pip") { - PipLayer( - pictureInPictureSlot = pictureInPictureSlot, - localRenderState = localRenderState, - bottomInsetPx = bottomInsetPx, - barsHeightPx = measuredBarsHeightPx, - pipSizePx = pipSizePx, - centeredContentWidthPx = centeredContentWidthPx - ) - }.map { it.measure(constraints) } + Layout( + contents = listOf( + { + BlurrableContentLayer( + isFocused = isFocused, + isPortrait = isPortrait, + bottomInsetPx = bottomInsetPx, + bottomSheetWidthPx = bottomSheetWidthPx, + barsSlot = { Bars() }, + callGridSlot = callGridSlot, + reactionsSlot = reactionsSlot, + callOverflowSlot = callOverflowSlot, + audioIndicatorSlot = audioIndicatorSlot, + onMeasured = { barsHeight, barsWidth -> + barDimensions.heightPx = barsHeight + barDimensions.widthPx = barsWidth + } + ) + }, + { + PipLayer( + pictureInPictureSlot = pictureInPictureSlot, + localRenderState = localRenderState, + bottomInsetPx = bottomInsetPx, + barDimensions = barDimensions, + pipSizePx = pipSizePx, + bottomSheetWidthPx = bottomSheetWidthPx + ) + } + ), + modifier = modifier + ) { (blurrableMeasurables, pipMeasurables), constraints -> + val blurrablePlaceables = blurrableMeasurables.map { it.measure(constraints) } + val pipPlaceables = pipMeasurables.map { it.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { - blurrableLayerPlaceable.forEach { it.place(0, 0) } - pipLayerPlaceable.forEach { it.place(0, 0) } + blurrablePlaceables.forEach { it.place(0, 0) } + pipPlaceables.forEach { it.place(0, 0) } } } } @@ -169,7 +192,6 @@ private fun BlurrableContentLayer( nonOverflowConstraints } - // Cap bars width to sheet max width (bars can be narrower if content doesn't fill) val barsMaxWidth = minOf(barConstraints.maxWidth, bottomSheetWidthPx) val barsConstrainedToSheet = barConstraints.copy(maxWidth = barsMaxWidth) @@ -177,7 +199,6 @@ private fun BlurrableContentLayer( 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) @@ -229,9 +250,9 @@ private fun PipLayer( pictureInPictureSlot: @Composable () -> Unit, localRenderState: WebRtcLocalRenderState, bottomInsetPx: Int, - barsHeightPx: Int, + barDimensions: BarDimensions, pipSizePx: Size, - centeredContentWidthPx: Int + bottomSheetWidthPx: Int ) { Layout( content = pictureInPictureSlot, @@ -239,13 +260,14 @@ private fun PipLayer( ) { measurables, constraints -> val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val centeredContentWidthPx = maxOf(barDimensions.widthPx, bottomSheetWidthPx) + val pictureInPictureConstraints: Constraints = when (localRenderState) { WebRtcLocalRenderState.GONE, WebRtcLocalRenderState.SMALLER_RECTANGLE, WebRtcLocalRenderState.LARGE, WebRtcLocalRenderState.LARGE_NO_VIDEO, WebRtcLocalRenderState.FOCUSED -> constraints WebRtcLocalRenderState.SMALL_RECTANGLE, WebRtcLocalRenderState.EXPANDED -> { - // Check if there's enough space on either side of the centered content (bars/sheet) val spaceOnEachSide = (looseConstraints.maxWidth - centeredContentWidthPx) / 2 val shouldOffset = centeredContentWidthPx > 0 && spaceOnEachSide < pipSizePx.width - val offsetAmount = bottomInsetPx + barsHeightPx + val offsetAmount = bottomInsetPx + barDimensions.heightPx looseConstraints.offset(vertical = if (shouldOffset) -offsetAmount else 0) } }