package com.suno.android.common_ui.components.waveform import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.suno.android.common_ui.components.preview.SunoPreview import com.suno.android.common_ui.components.timebox.TimeBox import com.suno.android.common_ui.components.timebox.TimeBoxPlaceableScope import com.suno.android.common_ui.components.timebox.rememberTimeBoxState import com.suno.android.common_ui.components.timebox.timeBoxGestures import com.suno.android.common_ui.theme.ExtendedTheme import kotlin.random.Random import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import com.suno.android.common_res.R as CommonResR import com.suno.android.common_ui.R as CommonUiR @Composable fun TimeBoxPlaceableScope.CropHandles( cropTime: ClosedRange, onStartTimeDrag: (delta: Duration) -> Unit, onEndTimeDrag: (delta: Duration) -> Unit, onCropperDrag: (delta: Duration) -> Unit, modifier: Modifier = Modifier, onDragEnd: () -> Unit = {}, ) { val startTime = cropTime.start val endTime = cropTime.endInclusive val density = LocalDensity.current var isStartDragging by remember { mutableStateOf(false) } var isEndDragging by remember { mutableStateOf(false) } var isCropperDragging by remember { mutableStateOf(false) } CompositionLocalProvider( LocalLayoutDirection provides LayoutDirection.Ltr, ) { Row( modifier = modifier .atTime(startTime to endTime) .clip(MaterialTheme.shapes.small), ) { CropHandle( painter = painterResource(CommonUiR.drawable.extend_left), contentDescription = stringResource(CommonResR.string.extend_left), modifier = Modifier .fillMaxHeight() .width(24.dp) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> val timeDelta = with(density) { delta.toDp() / resolution } onStartTimeDrag(timeDelta) }, onDragStarted = { isStartDragging = true }, onDragStopped = { isStartDragging = false if (!isEndDragging && !isCropperDragging) { onDragEnd() } }, ), ) Box( contentAlignment = Alignment.TopCenter, modifier = Modifier .fillMaxHeight() .weight(1f), ) { Spacer( modifier = Modifier .matchParentSize() .background(color = ExtendedTheme.colors.sunoPink.copy(alpha = .5f)), ) Spacer( modifier = Modifier .padding(top = 4.dp) .size( height = 4.dp, width = 16.dp, ) .clip(CircleShape) .background(ExtendedTheme.colors.sunoPink) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> val timeDelta = with(density) { delta.toDp() / resolution } onCropperDrag(timeDelta) }, onDragStarted = { isEndDragging = true }, onDragStopped = { isEndDragging = false if (!isStartDragging && !isCropperDragging) { onDragEnd() } }, ), ) } CropHandle( painter = painterResource(CommonUiR.drawable.extend_right), contentDescription = stringResource(CommonResR.string.extend_right), modifier = Modifier .fillMaxHeight() .width(24.dp) .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> val timeDelta = with(density) { delta.toDp() / resolution } onEndTimeDrag(timeDelta) }, onDragStarted = { isCropperDragging = true }, onDragStopped = { isCropperDragging = false if (!isStartDragging && !isEndDragging) { onDragEnd() } }, ), ) } } } @Composable internal fun CropHandle( painter: Painter, contentDescription: String, modifier: Modifier = Modifier, ) { Box( contentAlignment = Alignment.Center, modifier = modifier.background(ExtendedTheme.colors.sunoPink), ) { Icon( painter = painter, contentDescription = contentDescription, modifier = Modifier.requiredSize(16.dp), tint = Color.White, ) } } @PreviewLightDark @Composable private fun CropHandles_Preview() { var startTime by remember { mutableStateOf(0.seconds) } var endTime by remember { mutableStateOf(30.seconds) } val waveformState = remember { AudioWaveformState( waveform = FloatArray(10000) { Random.nextFloat() }, duration = 30.seconds, ) } val waveDuration = 30.seconds val timeBoxState = rememberTimeBoxState( resolution = 20.dp.perSecond, ) SunoPreview { TimeBox( state = timeBoxState, modifier = Modifier .height(80.dp) .timeBoxGestures(timeBoxState), ) { AudioWaveform( state = waveformState, modifier = Modifier.height(80.dp), ) CropHandles( cropTime = startTime..endTime, onStartTimeDrag = { startTime = (startTime + it).coerceIn(Duration.ZERO, endTime - 48.dp / resolution) }, onEndTimeDrag = { endTime = (endTime + it).coerceIn(startTime + 48.dp / resolution, waveDuration) }, onCropperDrag = { val consumed = listOf( startTime - (startTime + it).coerceAtLeast(Duration.ZERO), endTime - (endTime + it).coerceAtMost(waveDuration), ).minBy { it.absoluteValue } startTime = startTime - consumed endTime = endTime - consumed }, ) } } }