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

Class Method, % Branch, % Line, % Instruction, %
MetronomeScreenViewKt 0% (0/12) 0% (0/62) 0% (0/96) 0% (0/1352)
MetronomeScreenViewKt$bottomSheetState$1$1 0% (0/1) 25% (1/4) 0% (0/3) 0% (0/38)
MetronomeScreenViewKt$bottomSheetState$2$1 0% (0/1) 0% (0/3) 0% (0/3) 18.2% (4/22)
MetronomeScreenViewKt$bottomSheetState$2$1$WhenMappings
MetronomeScreenViewKt$Content$1$4$1 0% (0/2) 0% (0/9) 0% (0/85)
MetronomeScreenViewKt$MetronomeScreenPreview$1$1 0% (0/9) 0% (0/17) 0% (0/46)
Total 0% (0/25) 1.4% (1/69) 0% (0/128) 0.3% (4/1543)


 package com.vsevolodganin.clicktrack.ui.screen
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Tune
 import androidx.compose.material3.Card
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.SheetState
 import androidx.compose.material3.SheetValue
 import androidx.compose.material3.Text
 import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import clicktrack.multiplatform.generated.resources.Res
 import clicktrack.multiplatform.generated.resources.general_metronome_click_track_title
 import clicktrack.multiplatform.generated.resources.metronome_bpm_meter_tap
 import clicktrack.multiplatform.generated.resources.metronome_screen_title
 import com.vsevolodganin.clicktrack.metronome.MetronomeState
 import com.vsevolodganin.clicktrack.metronome.MetronomeViewModel
 import com.vsevolodganin.clicktrack.metronome.metronomeClickTrack
 import com.vsevolodganin.clicktrack.model.BeatsPerMinuteOffset
 import com.vsevolodganin.clicktrack.model.NotePattern
 import com.vsevolodganin.clicktrack.model.PlayProgress
 import com.vsevolodganin.clicktrack.model.TimeSignature
 import com.vsevolodganin.clicktrack.model.bpm
 import com.vsevolodganin.clicktrack.ui.piece.BpmWheel
 import com.vsevolodganin.clicktrack.ui.piece.ClickTrackView
 import com.vsevolodganin.clicktrack.ui.piece.DarkTopAppBarWithBack
 import com.vsevolodganin.clicktrack.ui.piece.PlayStopButton
 import com.vsevolodganin.clicktrack.ui.piece.SubdivisionsChooser
 import com.vsevolodganin.clicktrack.ui.piece.TimeSignatureView
 import com.vsevolodganin.clicktrack.ui.theme.ClickTrackTheme
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import org.jetbrains.compose.resources.stringResource
 import kotlin.time.Duration.Companion.milliseconds
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun MetronomeScreenView(viewModel: MetronomeViewModel, modifier: Modifier = Modifier) {
     Scaffold(
         topBar = {
             DarkTopAppBarWithBack(
                 onBackClick = viewModel::onBackClick,
                 title = { Text(text = stringResource(Res.string.metronome_screen_title)) },
                 actions = {
                     IconButton(onClick = viewModel::onToggleOptions) {
                         Icon(imageVector = Icons.Default.Tune, contentDescription = null)
                     }
                 },
             )
         },
         modifier = modifier,
     ) { paddingValues ->
         Content(
             viewModel = viewModel,
             state = viewModel.state.collectAsState().value ?: return@Scaffold,
             modifier = Modifier.padding(paddingValues),
         )
     }
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun Content(
     viewModel: MetronomeViewModel,
     state: MetronomeState,
     modifier: Modifier = Modifier,
 ) {
     Column(
         modifier = modifier
             .fillMaxSize()
             .padding(bottom = 32.dp),
         verticalArrangement = Arrangement.SpaceBetween,
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         Card(
             modifier = Modifier
                 .padding(8.dp)
                 .fillMaxWidth()
                 .height(200.dp),
         ) {
             val metronomeClickTrackName = stringResource(Res.string.general_metronome_click_track_title)
             val metronomeClickTrack = remember(state.bpm, state.pattern, state.timeSignature) {
                 metronomeClickTrack(
                     name = metronomeClickTrackName,
                     bpm = state.bpm,
                     pattern = state.pattern,
                     timeSignature = state.timeSignature,
                 )
             }
 
             ClickTrackView(
                 clickTrack = metronomeClickTrack,
                 drawAllBeatsMarks = true,
                 drawTextMarks = false,
                 progress = state.progress,
                 defaultLineWidth = with(LocalDensity.current) { 1f.dp.toPx() },
             )
         }
 
         Text(
             text = state.bpm.value.toString(),
             style = MaterialTheme.typography.displayLarge.copy(
                 fontWeight = FontWeight.Medium,
                 letterSpacing = 8.sp,
             ),
         )
 
         TimeSignatureView(
             value = state.timeSignature,
             onValueChange = viewModel::onTimeSignatureChange,
         )
 
         Layout(
             content = {
                 Box(contentAlignment = Alignment.Center) {
                     BpmWheel(
                         value = state.bpm,
                         onValueChange = viewModel::onBpmChange,
                         modifier = Modifier.size(200.dp),
                     )
                     PlayStopButton(
                         isPlaying = state.isPlaying,
                         onToggle = viewModel::onTogglePlay,
                     )
                 }
 
                 FloatingActionButton(
                     onClick = viewModel::onBpmMeterClick,
                     modifier = Modifier.size(64.dp),
                     shape = CircleShape,
                 ) {
                     Text(
                         text = stringResource(Res.string.metronome_bpm_meter_tap),
                         style = LocalTextStyle.current.copy(
                             fontWeight = FontWeight.Black,
                             letterSpacing = 4.sp,
                         ),
                     )
                 }
             },
             modifier = Modifier.fillMaxWidth(),
         ) { measurables, constraints ->
             val wheel = measurables[0].measure(Constraints())
             val fab = measurables[1].measure(Constraints())
 
             val width = constraints.maxWidth
             val height = maxOf(wheel.height, fab.height)
 
             layout(width, height) {
                 // Wheel is centered horizontally, nothing special
                 wheel.placeRelative(x = (width - wheel.width) / 2, y = 0)
 
                 // FAB is placed in the middle between wheel's and parent's right borders
                 fab.placeRelative(
                     x = (width * 3 + wheel.width - fab.width * 2) / 4,
                     y = (height - fab.height) / 2,
                 )
             }
         }
     }
 
     val bottomSheetState = bottomSheetState(viewModel)
     // FIXME: Checking both states to give animation an opportunity to finish
     if (state.areOptionsExpanded || bottomSheetState.isVisible) {
         ModalBottomSheet(
             onDismissRequest = { viewModel.onOptionsExpandedChange(false) },
             sheetState = bottomSheetState,
         ) {
             Options(viewModel)
         }
     }
 }
 
 @Composable
 private fun Options(viewModel: MetronomeViewModel) {
     val state = viewModel.state.collectAsState().value ?: return
     SubdivisionsChooser(
         pattern = state.pattern,
         timeSignature = state.timeSignature,
         onSubdivisionChoose = viewModel::onPatternChoose,
         modifier = Modifier.padding(8.dp).navigationBarsPadding(),
         alwaysExpanded = true,
     )
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun bottomSheetState(viewModel: MetronomeViewModel): SheetState {
     val externalAreOptionsExpanded = viewModel.state.collectAsState().value?.areOptionsExpanded ?: false
     val localBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
 
     // Sync the external state with the local state
     LaunchedEffect(externalAreOptionsExpanded) {
         when (externalAreOptionsExpanded) {
             true -> localBottomSheetState.expand()
             false -> localBottomSheetState.hide()
         }
     }
     LaunchedEffect(localBottomSheetState.currentValue) {
         when (localBottomSheetState.currentValue) {
             SheetValue.PartiallyExpanded,
             SheetValue.Hidden,
             -> viewModel.onOptionsExpandedChange(false)
             SheetValue.Expanded -> viewModel.onOptionsExpandedChange(true)
         }
     }
 
     return localBottomSheetState
 }
 
 @Preview
 @Composable
 internal fun MetronomeScreenPreview(expanded: Boolean = false) = ClickTrackTheme {
     MetronomeScreenView(
         viewModel = object : MetronomeViewModel {
             override val state: StateFlow<MetronomeState?> = MutableStateFlow(
                 MetronomeState(
                     bpm = 90.bpm,
                     pattern = NotePattern.QUINTUPLET_X2,
                     timeSignature = TimeSignature(4, 4),
                     progress = PlayProgress(100.milliseconds),
                     isPlaying = false,
                     areOptionsExpanded = expanded,
                 ),
             )
 
             override fun onBackClick() = Unit
 
             override fun onToggleOptions() = Unit
 
             override fun onOptionsExpandedChange(isOpened: Boolean) = Unit
 
             override fun onPatternChoose(pattern: NotePattern) = Unit
 
             override fun onTimeSignatureChange(timeSignature: TimeSignature) = Unit
 
             override fun onBpmChange(bpmDiff: BeatsPerMinuteOffset) = Unit
 
             override fun onTogglePlay() = Unit
 
             override fun onBpmMeterClick() = Unit
         },
     )
 }
 
 @Preview
 @Composable
 private fun ExpandedMetronomeScreenPreview() = MetronomeScreenPreview(expanded = true)