package com.suno.android.common_ui.components.timebox import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size 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.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Layout import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.suno.android.common_ui.components.preview.SunoPreview import com.suno.android.common_ui.components.waveform.div import com.suno.android.common_ui.components.waveform.perSecond import com.suno.android.common_ui.components.waveform.times import kotlin.time.Duration.Companion.seconds /** * A composable that renders time-based content within a scrollable and zoomable viewport. * * TimeBox is a layout component that positions its children based on their time positions. * Children can be placed at specific time points or time ranges using the [TimeBoxPlaceableScope] * modifiers like [atTime]. * * The component supports: * - Panning to scroll through time * - Zooming to change the time resolution (when enabled) * - Automatic positioning of time-based content * - Efficient rendering of only visible content * * Example usage: * ``` * TimeBox( * state = timeBoxState, * modifier = Modifier.size(300.dp, 80.dp) * ) { * // Place content at specific time points * Spacer( * modifier = Modifier * .atTime(2.seconds) * .size(20.dp) * .background(Color.Blue) * ) * * // Place content spanning a time range * Spacer( * modifier = Modifier * .atTime(2.seconds to 3.seconds) * .background(Color.Red) * ) * } * ``` * * @param state The [TimeBoxState] that controls the viewport's time range and resolution * @param modifier Optional [Modifier] to be applied to the TimeBox * @param content The content to be laid out within the TimeBox, using [TimeBoxPlaceableScope] modifiers */ @Composable fun TimeBox( state: TimeBoxState, modifier: Modifier = Modifier, content: @Composable TimeBoxPlaceableScope.() -> Unit, ) { val contentScope = remember(state) { TimeBoxPlaceableScope( timeBoxState = state, ) } Layout( content = { contentScope.content() }, modifier = modifier.clipToBounds(), ) { measurables, parentConstraints -> val placeables = measurables.mapNotNull { val data = it.parentData as? TimePlaceableDataNode ?: return@mapNotNull null val constraints = data.timeWidth?.let { timeWidth -> parentConstraints.copy( minWidth = (timeWidth * state.resolution).roundToPx(), maxWidth = (timeWidth * state.resolution).roundToPx(), ) } ?: parentConstraints.copy( minWidth = 0, minHeight = 0, ) it.measure(constraints) } val width = parentConstraints.maxWidth val height = placeables.maxOfOrNull { it.height } ?: parentConstraints.minHeight layout(width, height) { val endTime = state.startTimeOffset + width.toDp() / state.resolution placeables.forEach { val data = it.parentData as? TimePlaceableDataNode ?: return@forEach if (data.isSelfRendering) { it.place(0, 0) return@forEach } val time = data.time ?: return@forEach val centerTime = (time.endInclusive - time.start) / 2 + time.start val centerPos = ((centerTime - state.startTimeOffset) * state.resolution).toPx() // re-derive float width to avoid jittering from rounding when item width changes val placeableWidth = data.timeWidth?.let { it * state.resolution }?.toPx() ?: it.width.toFloat() val x = (centerPos - (placeableWidth / 2f)).toInt() if (x > width || x + placeableWidth < 0) return@forEach it.place( x = x, y = 0, // todo vertical alignment ) } } } } /** * A preview of the TimeBox component showing example usage with different time-based elements. * * This preview demonstrates: * - A blue spacer at a specific time point (20 seconds) * - A red spacer spanning a time range (2-3 seconds) * - Basic gesture support for panning and zooming */ @Preview @Composable private fun TimeBoxPreview() { SunoPreview { val state = rememberTimeBoxState(1.dp.perSecond) TimeBox( state = state, modifier = Modifier .size(300.dp, 80.dp) .timeBoxGestures(state), ) { Spacer( modifier = Modifier .atTime(20.seconds) .fillMaxHeight() .width(20.dp) .background(Color.Blue), ) Spacer( modifier = Modifier .fillMaxHeight() .atTime(2.seconds to 3.seconds) .background(Color.Red), ) } } }