diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt index f44dcf2bb..58c50927f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt @@ -20,14 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.layouts -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp @@ -47,8 +45,10 @@ private data class NoteLayoutPx( ) /** - * Custom single-pass layout for note items that replaces nested Row/Column - * measurements with direct constraint calculation using known fixed dimensions. + * Custom zero-overhead layout for note items. Uses the multi-content [Layout] + * overload to eliminate ALL intermediate wrapper nodes (Box/Column). Each slot's + * composables become direct measurables measured with pre-computed constraints + * in a single pass. * * Layout structure (when [showAuthorColumn] is true): * ``` @@ -67,11 +67,12 @@ private data class NoteLayoutPx( * └───────────────────────────────────────────────┘ * ``` * - * Performance benefits over nested Row/Column: - * - Eliminates Row's two-pass measurement (measure author, then content with remaining width) - * - Uses pre-computed pixel dimensions for author column width (55dp) and gaps - * - Reduces content Column children from ~6 to ~3 - * - Single measure pass positions all sections with known constraints + * Performance characteristics: + * - Zero intermediate layout nodes (no Box/Column wrappers) + * - Pre-computed pixel dimensions cached across recompositions + * - Single measure pass with known constraints for all 6 slots + * - Multi-child slots (noteContent, reactionsRow) stacked inline + * - Hidden slots skip measurement entirely (empty measurable lists) * * @param addPadding Whether to add standard note padding (12dp sides, 10dp top) * @param showAuthorColumn Whether to reserve space for the author picture column @@ -109,22 +110,17 @@ fun NoteComposeLayout( } Layout( - content = { - // Slot 0: Author picture (55x55 area) - Box { authorPicture() } - // Slot 1: Relay badges (55dp wide, below author) - Box { relayBadges() } - // Slot 2: First row (author name, time, options) - Box { firstRow() } - // Slot 3: Second row (NIP05, location, PoW) - Box { secondRow() } - // Slot 4: Note content (event-specific + zap splits + approval) - Column(Modifier.fillMaxWidth()) { noteContent() } - // Slot 5: Reactions row (full width) - Column(Modifier.fillMaxWidth()) { reactionsRow() } - }, + contents = + listOf( + authorPicture, + relayBadges, + firstRow, + secondRow, + noteContent, + reactionsRow, + ), modifier = modifier, - ) { measurables, constraints -> + ) { allMeasurables, constraints -> val maxWidth = constraints.maxWidth // Compute padding offsets @@ -138,70 +134,94 @@ fun NoteComposeLayout( val gap = if (showAuthorColumn) px.authorContentGap else 0 val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0) - // Build constraints for each section - val authorConstraints = - if (showAuthorColumn) Constraints(maxWidth = px.authorWidth) else ZeroConstraints + // Pre-compute constraint objects once + val authorConstraints = Constraints(maxWidth = px.authorWidth) val contentConstraints = Constraints(maxWidth = contentWidth) + val fullWidthConstraints = Constraints(maxWidth = maxWidth) - // Measure all children in a single pass with pre-computed constraints - val authorPlaceable = measurables[0].measure(authorConstraints) - val relayPlaceable = measurables[1].measure(authorConstraints) - val firstRowPlaceable = measurables[2].measure(contentConstraints) + // Measure author column (single child per slot, skip if empty) + val authorPlaceable = allMeasurables[0].firstOrNull()?.measure(authorConstraints) + val relayPlaceable = allMeasurables[1].firstOrNull()?.measure(authorConstraints) + + // Measure header rows + val firstRowPlaceable = allMeasurables[2].firstOrNull()?.measure(contentConstraints) val secondRowPlaceable = - measurables[3].measure( - if (showSecondRow) contentConstraints else ZeroConstraints, - ) - val contentPlaceable = measurables[4].measure(contentConstraints) - val reactionsPlaceable = - measurables[5].measure(Constraints(maxWidth = maxWidth)) + if (showSecondRow) allMeasurables[3].firstOrNull()?.measure(contentConstraints) else null + + // Measure note content children directly (no Column wrapper) + val contentMeasurables = allMeasurables[4] + val contentPlaceables = arrayOfNulls(contentMeasurables.size) + var contentStackHeight = 0 + for (i in contentMeasurables.indices) { + val placeable = contentMeasurables[i].measure(contentConstraints) + contentPlaceables[i] = placeable + contentStackHeight += placeable.height + } + + // Measure reactions row children directly (no Column wrapper) + val reactionsMeasurables = allMeasurables[5] + val reactionsPlaceables = arrayOfNulls(reactionsMeasurables.size) + var reactionsHeight = 0 + for (i in reactionsMeasurables.indices) { + val placeable = reactionsMeasurables[i].measure(fullWidthConstraints) + reactionsPlaceables[i] = placeable + reactionsHeight += placeable.height + } // Calculate section heights val spacer = if (showContentSpacer) px.contentSpacer else 0 val contentColumnHeight = - firstRowPlaceable.height + - (if (showSecondRow) secondRowPlaceable.height else 0) + + (firstRowPlaceable?.height ?: 0) + + (secondRowPlaceable?.height ?: 0) + spacer + - contentPlaceable.height + contentStackHeight val authorColumnHeight = - if (showAuthorColumn) { - authorPlaceable.height + px.authorBadgeGap + relayPlaceable.height + if (showAuthorColumn && authorPlaceable != null) { + authorPlaceable.height + px.authorBadgeGap + (relayPlaceable?.height ?: 0) } else { 0 } val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight) - val totalHeight = padTop + mainAreaHeight + reactionsPlaceable.height + val totalHeight = padTop + mainAreaHeight + reactionsHeight layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) { - // Place author column (inside padding) + // Place author column if (showAuthorColumn) { - authorPlaceable.placeRelative(padStart, padTop) - relayPlaceable.placeRelative( + authorPlaceable?.placeRelative(padStart, padTop) + relayPlaceable?.placeRelative( padStart, - padTop + authorPlaceable.height + px.authorBadgeGap, + padTop + (authorPlaceable?.height ?: 0) + px.authorBadgeGap, ) } - // Place content column (to the right of author, inside padding) + // Place content column val contentX = padStart + authorW + gap var y = padTop - firstRowPlaceable.placeRelative(contentX, y) - y += firstRowPlaceable.height + firstRowPlaceable?.placeRelative(contentX, y) + y += firstRowPlaceable?.height ?: 0 if (showSecondRow) { - secondRowPlaceable.placeRelative(contentX, y) - y += secondRowPlaceable.height + secondRowPlaceable?.placeRelative(contentX, y) + y += secondRowPlaceable?.height ?: 0 } y += spacer - contentPlaceable.placeRelative(contentX, y) + for (placeable in contentPlaceables) { + placeable?.placeRelative(contentX, y) + y += placeable?.height ?: 0 + } // Place reactions row at full width, below main area - reactionsPlaceable.placeRelative(0, padTop + mainAreaHeight) + var reactionsY = padTop + mainAreaHeight + for (placeable in reactionsPlaceables) { + placeable?.placeRelative(0, reactionsY) + reactionsY += placeable?.height ?: 0 + } } } }