package com.suno.android.common_ui.components.timebox import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ParentDataModifierNode import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Density import com.suno.android.common_ui.components.waveform.div import kotlin.time.Duration /** * A scope that provides modifiers for positioning content within a [TimeBox] based on time. * * This scope is provided to the content lambda of [TimeBox] and offers modifiers to * position children at specific time points or time ranges. The scope also provides * access to the current [TimeBoxState] properties. * * @property timeBoxState The [TimeBoxState] that controls the TimeBox viewport */ class TimeBoxPlaceableScope( val timeBoxState: TimeBoxState, ) { /** The current resolution (pixels per second) of the TimeBox */ val resolution get() = timeBoxState.resolution /** The current start time offset of the TimeBox viewport */ val startTimeOffset get() = timeBoxState.startTimeOffset /** * Positions a composable at a specific time point. * * The composable will be centered at the specified time point. The width of the * composable will be determined by its own size constraints. * * @param time The time point at which to position the composable */ fun Modifier.atTime( time: Duration, ) = this then TimePlaceableDataElement( time = time..time, inspectorInfo = debugInspectorInfo { name = "time" value = time }, ) /** * Positions a composable spanning a time range. * * The composable will be positioned to span the entire specified time range. * Its width will be calculated based on the time range and current resolution. * * @param time The time range (start to end) that the composable should span */ fun Modifier.atTime( time: ClosedRange, ) = this then TimePlaceableDataElement( time = time, timeWidth = time.endInclusive - time.start, inspectorInfo = debugInspectorInfo { name = "time" value = time }, ) /** * Positions a composable spanning a time range specified as a pair of durations. * * This is a convenience overload that converts a pair of durations into a * closed range for positioning. * * @param time A pair of durations representing the start and end times */ fun Modifier.atTime( time: Pair, ): Modifier = atTime(time.first..time.second) /** * Marks a composable as self-rendering, meaning it will be positioned at (0,0) * and handle its own time-based positioning. * * This is useful for composables that need to implement their own time-based * positioning logic (i.e. for optimization purposes) or for static content that should always be visible. * * @param ignorePositioning Whether the composable should handle its own positioning */ fun Modifier.timeBoxIgnorePositioning( ignorePositioning: Boolean = true, ) = this then TimePlaceableRenderParentElement( isSelfRendering = ignorePositioning, ) /** * Capture drag events and send updates to onTimeDelta representing how much time the modified composable has been * moved * * @param onTimeDelta Callback that receives the time delta as the user drags */ @Composable fun Modifier.timeDraggable( onDragStarted: () -> Unit = {}, onDragStopped: () -> Unit = {}, onTimeDelta: (Duration) -> Unit, ): Modifier { val density = LocalDensity.current return this.draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { deltaPx -> with(density) { onTimeDelta(deltaPx.toDp() / resolution) } }, onDragStarted = { onDragStarted() }, onDragStopped = { onDragStopped() }, ) } /** * Capture drag events and send updates for what the new state of the item should be * * Solves issues where backing data for an item's position takes too long to update * * @param onTimeDelta Callback that receives the time delta as the user drags */ @Composable fun Modifier.timeDraggableWithState( backingTime: Duration, onDragStarted: () -> Unit = {}, onDragStopped: () -> Unit = {}, onTimeChanged: (Duration) -> Unit, ): Modifier { var time by remember { mutableStateOf(backingTime) } val density = LocalDensity.current return this.timeDraggable( onTimeDelta = { delta -> time += delta onTimeChanged(time) }, onDragStarted = { time = backingTime onDragStarted() }, onDragStopped = { onDragStopped() }, ) } } private data class TimePlaceableDataElement( val time: ClosedRange, val timeWidth: Duration? = null, val inspectorInfo: InspectorInfo.() -> Unit, ) : ModifierNodeElement() { override fun create(): TimePlaceableDataNode = TimePlaceableDataNode( time = time, timeWidth = timeWidth, ) override fun update( node: TimePlaceableDataNode, ) { node.time = time node.timeWidth = timeWidth } } private data class TimePlaceableRenderParentElement( val isSelfRendering: Boolean = false, ) : ModifierNodeElement() { override fun create(): TimePlaceableDataNode = TimePlaceableDataNode( isSelfRendering = isSelfRendering, ) override fun update( node: TimePlaceableDataNode, ) { node.isSelfRendering = isSelfRendering } } internal class TimePlaceableDataNode( var time: ClosedRange? = null, var timeWidth: Duration? = null, var isSelfRendering: Boolean = false, ) : Modifier.Node(), ParentDataModifierNode { override fun Density.modifyParentData( parentData: Any?, ): Any? = this@TimePlaceableDataNode }