@file:Suppress("FunctionName") package link.roadio.riff.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.selected import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import link.roadio.riff.player.core.selection.AiPteBand @Composable internal fun AiControls( state: PlayerUiState, modifier: Modifier, ) { BoxWithConstraints(modifier) { val geometry = aiWideControlGeometry(maxWidth.value, maxHeight.value, TILE_GAP_DP) Column(Modifier.fillMaxSize()) { AiPteChoices(state, geometry.choiceWidthDp.dp, geometry.controlHeightDp.dp) Spacer(Modifier.height(TILE_GAP_DP.dp)) AiSizeChoices(state, geometry.choiceWidthDp.dp, geometry.controlHeightDp.dp) Spacer(Modifier.height(TILE_GAP_DP.dp)) AiPlayControl( state = state, modifier = Modifier.fillMaxWidth().height(geometry.controlHeightDp.dp), ) } } } @Composable internal fun AiNarrowControls( state: PlayerUiState, modifier: Modifier, ) { BoxWithConstraints(modifier) { val split = canUseAiNarrowSplit( widthDp = maxWidth.value, heightDp = maxHeight.value, gapDp = TILE_GAP_DP, minimumChoiceDp = AI_CHOICE_MIN_DP, minimumPlayDp = AI_PLAY_MIN_DP, ) if (split) { val geometry = aiNarrowControlGeometry(maxWidth.value, maxHeight.value, TILE_GAP_DP) CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { Row(Modifier.fillMaxSize()) { Column(Modifier.width(geometry.choicesWidthDp.dp).fillMaxHeight()) { AiPteChoices(state, geometry.choiceWidthDp.dp, geometry.choiceHeightDp.dp) Spacer(Modifier.height(TILE_GAP_DP.dp)) AiSizeChoices(state, geometry.choiceWidthDp.dp, geometry.choiceHeightDp.dp) } Spacer(Modifier.width(TILE_GAP_DP.dp)) AiPlayControl( state = state, modifier = Modifier.width(geometry.playWidthDp.dp).fillMaxHeight(), ) } } } else { val target = minOf(uiDimension("navTarget"), (maxWidth - 48.dp) / 3).coerceAtLeast(64.dp) Column(Modifier.fillMaxWidth()) { AiPteChoices(state, target, target) Spacer(Modifier.height(uiDimension("targetSpacingMin"))) AiSizeChoices(state, target, target) Spacer(Modifier.height(uiDimension("targetSpacingMin"))) AiPlayControl( state = state, modifier = Modifier.fillMaxWidth().height(uiDimension("touchTargetMin")), ) } } } } @Composable internal fun AiPteChoices( state: PlayerUiState, choiceWidth: Dp, choiceHeight: Dp, ) { val dispatch = LocalRiffDispatch.current AiChoiceRow(Modifier.height(choiceHeight)) { AiPteBand.entries.forEachIndexed { index, pteBand -> if (index > 0) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) val label = pteBand.name.lowercase().replaceFirstChar(Char::uppercase) val selected = pteBand in state.aiSelection.pteBands AiIconChoice( icon = pteBand.iconName, label = "$label perceived speed", selected = selected, enabled = state.aiSelection.isPteBandEnabled(pteBand, state.aiPteAvailable), modifier = Modifier.width(choiceWidth).fillMaxHeight(), onClick = { dispatch(PlayerUiAction.ToggleAiPteBand(pteBand)) }, ) } } } internal val AiPteBand.iconName: String get() = when (this) { AiPteBand.SLOW -> "tempo_slow" AiPteBand.MEDIUM -> "tempo_normal" AiPteBand.FAST -> "tempo_fast" } @Composable internal fun AiSizeChoices( state: PlayerUiState, choiceWidth: Dp, choiceHeight: Dp, ) { val dispatch = LocalRiffDispatch.current AiChoiceRow(Modifier.height(choiceHeight)) { listOf(10, 20, 50).forEachIndexed { index, queueSize -> if (index > 0) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) AiIconChoice( icon = "scale_eur_$queueSize", label = "$queueSize track queue", selected = state.aiSelection.size == queueSize, modifier = Modifier.width(choiceWidth).fillMaxHeight(), onClick = { dispatch(PlayerUiAction.SetAiSize(queueSize)) }, ) } } } @Composable internal fun AiPlayControl( state: PlayerUiState, modifier: Modifier, ) { val palette = LocalRiffPalette.current val dispatch = LocalRiffDispatch.current val enabled = state.aiPlayEnabled RiffControl( description = "Play AI queue", enabled = enabled, onClick = { dispatch(PlayerUiAction.CreateAiQueue) }, modifier = modifier, ) { RiffIcon( iconDrawableName("play"), Color(if (enabled) palette.text else palette.muted), modifier = Modifier.fillMaxSize(), ) } } @Composable internal fun AiChoiceRow( modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { Row( modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } @Composable internal fun AiIconChoice( icon: String, label: String, selected: Boolean, modifier: Modifier, enabled: Boolean = true, onClick: () -> Unit, ) { val palette = LocalRiffPalette.current RiffControl( description = "$label; ${if (selected) "selected" else "not selected"}", enabled = enabled, selected = selected, onClick = onClick, modifier = modifier, ) { RiffIcon( iconDrawableName(icon), Color(if (enabled) palette.text else palette.muted), modifier = Modifier.fillMaxSize(), ) } }