Coverage Summary for Class: PolyrhythmsScreenViewKt (com.vsevolodganin.clicktrack.ui.screen)

Class Method, % Branch, % Line, % Instruction, %
PolyrhythmsScreenViewKt 0% (0/11) 0% (0/58) 0% (0/74) 0% (0/1144)
PolyrhythmsScreenViewKt$progressAngle$1$1 0% (0/1) 0% (0/2) 0% (0/10) 0% (0/83)
Total 0% (0/12) 0% (0/60) 0% (0/84) 0% (0/1227)


 package com.vsevolodganin.clicktrack.ui.screen
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.FabPosition
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ChevronLeft
 import androidx.compose.material.icons.filled.ChevronRight
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import com.vsevolodganin.clicktrack.generated.resources.MR
 import com.vsevolodganin.clicktrack.model.PlayProgress
 import com.vsevolodganin.clicktrack.model.TwoLayerPolyrhythm
 import com.vsevolodganin.clicktrack.model.bpm
 import com.vsevolodganin.clicktrack.polyrhythm.PolyrhythmsState
 import com.vsevolodganin.clicktrack.polyrhythm.PolyrhythmsViewModel
 import com.vsevolodganin.clicktrack.ui.piece.PlayStopButton
 import com.vsevolodganin.clicktrack.ui.piece.PolyrhythmCircle
 import com.vsevolodganin.clicktrack.ui.piece.TopAppBarWithBack
 import com.vsevolodganin.clicktrack.ui.theme.ClickTrackTheme
 import com.vsevolodganin.clicktrack.utils.compose.AnimatableFloat
 import com.vsevolodganin.clicktrack.utils.compose.FULL_ANGLE_DEGREES
 import com.vsevolodganin.clicktrack.utils.compose.widthByText
 import dev.icerock.moko.resources.compose.stringResource
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.DurationUnit
 
 @Composable
 fun PolyrhythmsScreenView(viewModel: PolyrhythmsViewModel, modifier: Modifier = Modifier) {
     val state by viewModel.state.collectAsState()
     Scaffold(
         topBar = {
             TopAppBarWithBack(
                 onBackClick = viewModel::onBackClick,
                 title = { Text(stringResource(MR.strings.polyrhythms_screen_title)) },
             )
         },
         floatingActionButtonPosition = FabPosition.Center,
         floatingActionButton = {
             PlayStopButton(
                 isPlaying = state?.isPlaying ?: return@Scaffold,
                 onToggle = viewModel::onTogglePlay,
             )
         },
         modifier = modifier,
     ) {
         Content(viewModel, state ?: return@Scaffold)
     }
 }
 
 @Composable
 private fun Content(viewModel: PolyrhythmsViewModel, state: PolyrhythmsState) {
     Column(modifier = Modifier.padding(16.dp)) {
         Row {
             NumberChooser(
                 value = state.twoLayerPolyrhythm.layer1,
                 onValueChoose = viewModel::onLayer1Change,
             )
             Spacer(modifier = Modifier.weight(1f))
             NumberChooser(
                 value = state.twoLayerPolyrhythm.layer2,
                 onValueChoose = viewModel::onLayer2Change,
             )
         }
 
         PolyrhythmCircleWrapper(
             layer1 = state.twoLayerPolyrhythm.layer1,
             layer2 = state.twoLayerPolyrhythm.layer2,
             progress = state.playableProgress,
             totalDuration = state.twoLayerPolyrhythm.durationInTime,
             modifier = Modifier
                 .fillMaxWidth()
                 .padding(32.dp),
         )
     }
 }
 
 @Composable
 private fun NumberChooser(value: Int, onValueChoose: (Int) -> Unit, modifier: Modifier = Modifier) {
     Row(modifier = modifier) {
         IconButton(
             onClick = { onValueChoose(value - 1) },
             modifier = Modifier.align(CenterVertically),
         ) {
             Icon(Icons.Default.ChevronLeft, contentDescription = null)
         }
         val textStyle = MaterialTheme.typography.h5
         Text(
             text = value.toString(),
             modifier = Modifier
                 .align(CenterVertically)
                 .widthByText("99", textStyle),
             style = textStyle,
             textAlign = TextAlign.Center,
         )
         IconButton(
             onClick = { onValueChoose(value + 1) },
             modifier = Modifier.align(CenterVertically),
         ) {
             Icon(Icons.Default.ChevronRight, contentDescription = null)
         }
     }
 }
 
 @Composable
 private fun PolyrhythmCircleWrapper(layer1: Int, layer2: Int, progress: PlayProgress?, totalDuration: Duration, modifier: Modifier) {
     val progressAngle = progressAngle(progress, totalDuration)?.asState()
 
     PolyrhythmCircle(
         outerDotNumber = layer1,
         innerDotNumber = layer2,
         modifier = modifier,
         progressAngle = progressAngle?.value,
         progressVelocity = (FULL_ANGLE_DEGREES / totalDuration.toDouble(DurationUnit.SECONDS)).toFloat(),
     )
 }
 
 @Composable
 private fun progressAngle(progress: PlayProgress?, totalDuration: Duration): AnimatableFloat? {
     progress ?: return null
 
     val animatableProgressAngle = remember {
         Animatable(0f).apply {
             updateBounds(0f, FULL_ANGLE_DEGREES)
         }
     }
 
     LaunchedEffect(progress) {
         val progressTimePosition = progress.realPosition
         val progressAnglePosition = progressTimePosition.toAngle(totalDuration)
         val animationDuration = totalDuration - progressTimePosition
 
         animatableProgressAngle.snapTo(progressAnglePosition)
 
         if (!progress.isPaused) {
             animatableProgressAngle.animateTo(
                 targetValue = FULL_ANGLE_DEGREES,
                 animationSpec = tween(
                     durationMillis = animationDuration.coerceAtLeast(Duration.ZERO).inWholeMilliseconds.toInt(),
                     easing = LinearEasing,
                 ),
             )
         }
     }
 
     return animatableProgressAngle
 }
 
 private fun Duration.toAngle(totalDuration: Duration): Float {
     return if (totalDuration == Duration.ZERO) {
         0f
     } else {
         (this / totalDuration * FULL_ANGLE_DEGREES).toFloat()
     }
 }
 
 @ScreenPreview
 @Composable
 private fun Preview() = ClickTrackTheme {
     PolyrhythmsScreenView(
         viewModel = object : PolyrhythmsViewModel {
             override val state: StateFlow<PolyrhythmsState?> = MutableStateFlow(
                 PolyrhythmsState(
                     twoLayerPolyrhythm = TwoLayerPolyrhythm(
                         bpm = 120.bpm,
                         layer1 = 3,
                         layer2 = 2,
                     ),
                     isPlaying = true,
                     playableProgress = PlayProgress(100.milliseconds),
                 ),
             )
 
             override fun onBackClick() = Unit
 
             override fun onTogglePlay() = Unit
 
             override fun onLayer1Change(value: Int) = Unit
 
             override fun onLayer2Change(value: Int) = Unit
         },
     )
 }