From fdfbda9e107eeb90210280153db5ff1a2a276440 Mon Sep 17 00:00:00 2001 From: Arutemu Date: Tue, 15 Oct 2024 00:12:59 +0300 Subject: [PATCH] Messy changes: - a huge amount of small changes, lost track of them, but nothing super important AND - visually refactored Knob, now size is fully dynamic! - covers are disabled in sample Local Data Provider - as usual, have updated some dependencies --- app/build.gradle | 10 +- .../mukuro/pedalboard/data/PluginElement.kt | 2 +- .../data/local/LocalPluginsDataProvider.kt | 62 +- .../com/mukuro/pedalboard/ui/PedalboardApp.kt | 8 +- .../pedalboard/ui/PedalboardHomeViewModel.kt | 7 + .../pedalboard/ui/PedalboardListContent.kt | 22 +- .../pedalboard/ui/components/CustomSlider.kt | 265 +++++ .../ui/components/PedalboardPluginCard.kt | 238 +---- .../ui/components/plugin/PluginKnob.kt | 924 ++++++++++++++++++ .../ui/components/plugin/PluginSlider.kt | 262 +++++ .../navigation/PedalboardNavigationActions.kt | 8 + .../PedalboardNavigationComponents.kt | 42 +- app/src/main/res/values/strings.xml | 1 + build.gradle | 8 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 6 +- 17 files changed, 1619 insertions(+), 250 deletions(-) create mode 100644 app/src/main/java/com/mukuro/pedalboard/ui/components/CustomSlider.kt create mode 100644 app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginKnob.kt create mode 100644 app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginSlider.kt diff --git a/app/build.gradle b/app/build.gradle index 585cef8..a49e3ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,7 +60,7 @@ android { } dependencies { - def nav_version = "2.8.1" + def nav_version = "2.8.2" def lifecycle_version = "2.8.6" def accompanist = '0.36.0' @@ -98,7 +98,7 @@ dependencies { // palette implementation 'androidx.palette:palette-ktx:1.0.0' - implementation platform('androidx.compose:compose-bom:2024.09.02') + implementation platform('androidx.compose:compose-bom:2024.09.03') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' @@ -111,7 +111,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.09.02') + androidTestImplementation platform('androidx.compose:compose-bom:2024.09.03') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' @@ -125,4 +125,8 @@ dependencies { // LazyList scrollbar implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' + + // Android Wear libs for rounded/radial text + //implementation "androidx.wear.compose:compose-material:1.4.0" + //implementation "androidx.wear.compose:compose-foundation:1.4.0" } \ No newline at end of file diff --git a/app/src/main/java/com/mukuro/pedalboard/data/PluginElement.kt b/app/src/main/java/com/mukuro/pedalboard/data/PluginElement.kt index fc7e373..441b534 100644 --- a/app/src/main/java/com/mukuro/pedalboard/data/PluginElement.kt +++ b/app/src/main/java/com/mukuro/pedalboard/data/PluginElement.kt @@ -32,7 +32,7 @@ data class Slider( override val name: String, val startValue: Float, val endValue: Float, - val value: Float, + //val value: Float, val measure: String ) : PluginElement(id, name) diff --git a/app/src/main/java/com/mukuro/pedalboard/data/local/LocalPluginsDataProvider.kt b/app/src/main/java/com/mukuro/pedalboard/data/local/LocalPluginsDataProvider.kt index 4633fae..a16b91a 100644 --- a/app/src/main/java/com/mukuro/pedalboard/data/local/LocalPluginsDataProvider.kt +++ b/app/src/main/java/com/mukuro/pedalboard/data/local/LocalPluginsDataProvider.kt @@ -6,6 +6,7 @@ import com.mukuro.pedalboard.data.Knob import com.mukuro.pedalboard.data.PluginElement import com.mukuro.pedalboard.data.Plugin import com.mukuro.pedalboard.data.PluginType +import com.mukuro.pedalboard.data.Slider /** * A static data store of [Plugin]s. @@ -21,7 +22,7 @@ object LocalPluginsDataProvider { name = "test Nayuta plugin", aspectRatio = 0.6f, elements = TestKnob.knobSet1, - coverDrawable = R.drawable.nayuta + //coverDrawable = R.drawable.nayuta ), Plugin( id = 2L, @@ -29,7 +30,7 @@ object LocalPluginsDataProvider { name = "test chorus plugin", aspectRatio = 0.6f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.power + //coverDrawable = R.drawable.power ), Plugin( id = 3L, @@ -47,7 +48,7 @@ object LocalPluginsDataProvider { name = "Amptweaker Small Form", aspectRatio = 0.581f, elements = TestKnob.knobSet1, - coverDrawable = R.drawable.anime_girl_3, + //coverDrawable = R.drawable.anime_girl_3, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -57,7 +58,7 @@ object LocalPluginsDataProvider { name = "Boss single switch", aspectRatio = 0.574f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.illustration_1, + //coverDrawable = R.drawable.illustration_1, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -67,7 +68,7 @@ object LocalPluginsDataProvider { name = "Boss double switch", aspectRatio = 1.088f, elements = TestKnob.knobSet3, - coverDrawable = R.drawable.makima, + //coverDrawable = R.drawable.makima, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -77,7 +78,7 @@ object LocalPluginsDataProvider { name = "DigiTech Whammy", aspectRatio = 0.839f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.frieren_1, + //coverDrawable = R.drawable.frieren_1, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -87,7 +88,7 @@ object LocalPluginsDataProvider { name = "EHX Big Muff", aspectRatio = 0.809f, elements = TestKnob.knobSet1, - coverDrawable = R.drawable.nayuta, + //coverDrawable = R.drawable.nayuta, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -97,7 +98,7 @@ object LocalPluginsDataProvider { name = "EHX Small Clone", aspectRatio = 0.647f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.madoka_2, + //coverDrawable = R.drawable.madoka_2, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -107,7 +108,7 @@ object LocalPluginsDataProvider { name = "Ibanez Tube Screamer Classic super duper long name", aspectRatio = 0.542f, elements = TestKnob.knobSet3, - coverDrawable = R.drawable.anime_girl_4, + //coverDrawable = R.drawable.anime_girl_4, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -117,7 +118,7 @@ object LocalPluginsDataProvider { name = "Line 6 Modeler Pedals", aspectRatio = 1.667f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.makima_6, + //coverDrawable = R.drawable.makima_6, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -127,7 +128,7 @@ object LocalPluginsDataProvider { name = "Small MXR Pedals", aspectRatio = 0.535f, elements = TestKnob.knobSet1, - coverDrawable = R.drawable.nayuta_1, + //coverDrawable = R.drawable.nayuta_1, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -137,7 +138,7 @@ object LocalPluginsDataProvider { name = "Morley Wah Pedals", aspectRatio = 1.553f, elements = TestKnob.knobSet3, - coverDrawable = R.drawable.madoka_1 + //coverDrawable = R.drawable.madoka_1 ), Plugin( id = 11L, @@ -145,7 +146,7 @@ object LocalPluginsDataProvider { name = "ProCo Rat Two", aspectRatio = 0.875f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.makima_7, + //coverDrawable = R.drawable.makima_7, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -155,7 +156,7 @@ object LocalPluginsDataProvider { name = "Strymon Small Form", aspectRatio = 0.889f, elements = TestKnob.knobSet1, - coverDrawable = R.drawable.illustration_3 + //coverDrawable = R.drawable.illustration_3 ), Plugin( id = 13L, @@ -163,7 +164,7 @@ object LocalPluginsDataProvider { name = "Strymon Large Form", aspectRatio = 1.324f, elements = TestKnob.knobSet3, - coverDrawable = R.drawable.illustration_2 + //coverDrawable = R.drawable.illustration_2 ), Plugin( id = 14L, @@ -171,7 +172,7 @@ object LocalPluginsDataProvider { name = "TC Electronic Mini", aspectRatio = 0.514f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.bocchi_1, + //coverDrawable = R.drawable.bocchi_1, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -181,7 +182,7 @@ object LocalPluginsDataProvider { name = "TC Electronic Single Stomp", aspectRatio = 0.583f, elements = TestKnob.knobSet3, - coverDrawable = R.drawable.bocchi_2, + //coverDrawable = R.drawable.bocchi_2, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -191,7 +192,7 @@ object LocalPluginsDataProvider { name = "Way Huge", aspectRatio = 0.798f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.hutao_1, + //coverDrawable = R.drawable.hutao_1, horizontal = Arrangement.Center, vertical = Arrangement.Bottom ), @@ -201,7 +202,7 @@ object LocalPluginsDataProvider { name = "Walrus Audio Large Form", aspectRatio = 1.217f, elements = TestKnob.knobSet2, - coverDrawable = R.drawable.crymachina_1 + //coverDrawable = R.drawable.crymachina_1 ) ) @@ -258,7 +259,28 @@ object LocalPluginsDataProvider { startPoint = 0f, endPoint = 100f, measure = "db" - ) + ), + Slider( + id = 5, + name = "Drive", + startValue = 0f, + endValue = 100f, + measure = "db" + ), + Slider( + id = 6, + name = "Drive", + startValue = 0f, + endValue = 100f, + measure = "db" + ), + Knob( + id = 7, + name = "Overdrive", + startPoint = 0f, + endPoint = 100f, + measure = "db" + ), ) val knobSet3 = listOf( diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardApp.kt b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardApp.kt index dd34a63..8e75f31 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardApp.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardApp.kt @@ -212,6 +212,10 @@ private fun PedalboardNavigationWrapper( mainTitle = "Drums"; subTitle = "Backing tracks" } + PedalboardRoute.TEST -> { + mainTitle = "Test page"; + subTitle = "Experimental central" + } } Column(modifier = Modifier){ @@ -434,7 +438,6 @@ private fun PedalboardNavHost( ) } composable(PedalboardRoute.EFFECTS) { - //EmptyComingSoon() PedalboardEffectsScreen( contentType = contentType, pedalboardHomeUIState = pedalboardHomeUIState, @@ -454,5 +457,8 @@ private fun PedalboardNavHost( composable(PedalboardRoute.DRUMS) { EmptyComingSoon() } + composable(PedalboardRoute.TEST) { + EmptyComingSoon() + } } } diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardHomeViewModel.kt b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardHomeViewModel.kt index 8494ef5..74e6007 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardHomeViewModel.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardHomeViewModel.kt @@ -77,6 +77,13 @@ class PedalboardHomeViewModel(private val pluginsRepository: PluginsRepository = openedPreset = preset, ) }*/ + fun closeQuickMenu() { + _uiState.value = _uiState + .value.copy( + isDetailOnlyOpen = false, + //openedPlugin = _uiState.value.plugins.first() + ) + } // TODO - implement adding plugin to the board fun addPlugin(pluginId: Long) {} diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardListContent.kt b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardListContent.kt index 022f94b..039383a 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardListContent.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/PedalboardListContent.kt @@ -2,6 +2,7 @@ package com.mukuro.pedalboard.ui import android.net.Uri import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -250,7 +251,7 @@ fun PedalboardPluginsList( // changed from COLUMN to ROW, let's check it Column( modifier = modifier - //.padding(horizontal = 12.dp) + //.padding(horizontal = 12.dp) // TODO - need to make the clip modifier to be used only on tablets .clip(shape = RoundedCornerShape(36.dp, 0.dp, 0.dp, 0.dp)) .background(MaterialTheme.colorScheme.surfaceDim) ) { @@ -294,10 +295,6 @@ fun PedalboardPluginsList( items(items = plugins, key = { it.id }) { plugin -> PedalboardPluginCard( plugin = plugin, - navigateToDetail = { pluginId -> - navigateToDetail(pluginId, PedalboardContentType.SINGLE_PANE) - }, - toggleSelection = togglePluginSelection, isOpened = openedPlugin?.id == plugin.id, isSelected = selectedPluginIds.contains(plugin.id) ) @@ -305,8 +302,8 @@ fun PedalboardPluginsList( } InternalLazyRowScrollbar( modifier = Modifier - //.fillMaxHeight() - .padding(horizontal = 24.dp), + .fillMaxHeight(), + //.padding(horizontal = 24.dp), state = pluginLazyListState, settings = ScrollbarSettings( selectionMode = ScrollbarSelectionMode.Full, @@ -315,7 +312,9 @@ fun PedalboardPluginsList( //scrollbarPadding = 12.dp, //thumbThickness = 36.dp, thumbMinLength = 0.3f - ) + ), + // TODO - implement custom indicatorContent for custom scrollbar design :3 + //indicatorContent = Scrollbar() ) } @@ -361,10 +360,6 @@ fun PedalboardAllPluginsList( items(items = plugins, key = { it.id }) { plugin -> PedalboardPluginCard( plugin = plugin, - navigateToDetail = { pluginId -> - navigateToDetail(pluginId, PedalboardContentType.SINGLE_PANE) - }, - toggleSelection = togglePluginSelection, isOpened = openedPlugin?.id == plugin.id, isSelected = selectedPluginIds.contains(plugin.id) ) @@ -410,13 +405,14 @@ fun ImageLoader(plugin: Plugin) { ) } +// OLD STUFF, DOES NOT WORK, IS NOT USED, REMOVE IT! @Composable fun Scrollbar() { Card( modifier = Modifier .height(16.dp) .width(120.dp) - .padding(vertical = 20.dp) + .padding(vertical = 8.dp) .clip(CardDefaults.shape), colors = CardDefaults.cardColors(), elevation = CardDefaults.cardElevation(), diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/components/CustomSlider.kt b/app/src/main/java/com/mukuro/pedalboard/ui/components/CustomSlider.kt new file mode 100644 index 0000000..9ba5dc6 --- /dev/null +++ b/app/src/main/java/com/mukuro/pedalboard/ui/components/CustomSlider.kt @@ -0,0 +1,265 @@ +package com.mukuro.pedalboard.ui.components + +import androidx.annotation.IntRange +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SliderDefaults.colors +//import androidx.compose.material3.SliderImpl +import androidx.compose.material3.SliderState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +//@Preview +@OptIn(ExperimentalMaterial3Api::class) +@Composable + +fun CustomSlider( + state: SliderState, + modifier: Modifier = Modifier, + enabled: Boolean = true, // TODO - remove? + colors: SliderColors = SliderDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + thumb: @Composable (SliderState) -> Unit = { + CustomThumb( + interactionSource = interactionSource, + colors = colors, + enabled = enabled + ) + }, + track: @Composable (SliderState) -> Unit = { sliderState -> + CustomTrack(colors = colors, enabled = enabled, sliderState = sliderState) + } +) { + require(state.steps >= 0) { "steps should be >= 0" } + + Slider( + state = state, + modifier = modifier, + enabled = enabled, + interactionSource = interactionSource, + thumb = thumb, + track = track + ) +} + +/** + * + * Copied from older material 3 library source code + * + */ +@Composable +@ExperimentalMaterial3Api +fun CustomTrack( + sliderState: SliderState, + modifier: Modifier = Modifier, + colors: SliderColors = colors(), + enabled: Boolean = true +) { + val inactiveTrackColor = colors.trackColor(enabled, active = false) + val activeTrackColor = colors.trackColor(enabled, active = true) + val inactiveTickColor = colors.tickColor(enabled, active = false) + val activeTickColor = colors.tickColor(enabled, active = true) + Canvas( + modifier + .fillMaxWidth() + .height(TrackHeight) + ) { + drawTrack( + sliderState.tickFractions, + 0f, + sliderState.coercedValueAsFraction, + inactiveTrackColor, + activeTrackColor, + inactiveTickColor, + activeTickColor + ) + } +} + + +private fun DrawScope.drawTrack( + tickFractions: FloatArray, + activeRangeStart: Float, + activeRangeEnd: Float, + inactiveTrackColor: Color, + activeTrackColor: Color, + inactiveTickColor: Color, + activeTickColor: Color +) { + val isRtl = layoutDirection == LayoutDirection.Rtl + val sliderLeft = Offset(0f, center.y) + val sliderRight = Offset(size.width, center.y) + val sliderStart = if (isRtl) sliderRight else sliderLeft + val sliderEnd = if (isRtl) sliderLeft else sliderRight + val tickSize = TickSize.toPx() + val trackStrokeWidth = TrackHeight.toPx() + drawLine( + inactiveTrackColor, + sliderStart, + sliderEnd, + trackStrokeWidth, + StrokeCap.Round + ) + val sliderValueEnd = Offset( + sliderStart.x + + (sliderEnd.x - sliderStart.x) * activeRangeEnd, + center.y + ) + + val sliderValueStart = Offset( + sliderStart.x + + (sliderEnd.x - sliderStart.x) * activeRangeStart, + center.y + ) + + drawLine( + activeTrackColor, + sliderValueStart, + sliderValueEnd, + trackStrokeWidth, + StrokeCap.Round + ) + + for (tick in tickFractions) { + val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart + drawCircle( + color = if (outsideFraction) inactiveTickColor else activeTickColor, + center = Offset( + androidx.compose.ui.geometry.lerp(sliderStart, sliderEnd, tick).x, + center.y + ), + radius = tickSize / 2f + ) + } +} + + +@Composable +fun CustomThumb( + interactionSource: MutableInteractionSource, + modifier: Modifier = Modifier, + colors: SliderColors = colors(), + enabled: Boolean = true, + thumbSize: DpSize = ThumbSize +) { + val interactions = remember { mutableStateListOf() } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> interactions.add(interaction) + is PressInteraction.Release -> interactions.remove(interaction.press) + is PressInteraction.Cancel -> interactions.remove(interaction.press) + is DragInteraction.Start -> interactions.add(interaction) + is DragInteraction.Stop -> interactions.remove(interaction.start) + is DragInteraction.Cancel -> interactions.remove(interaction.start) + } + } + } + + val elevation = if (interactions.isNotEmpty()) { + ThumbPressedElevation + } else { + ThumbDefaultElevation + } + val shape = CircleShape + + @Suppress("DEPRECATION_ERROR") + (Spacer( + modifier + .size(thumbSize) + .indication( + interactionSource = interactionSource, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = StateLayerSize / 2 + ) + ) + .hoverable(interactionSource = interactionSource) + .shadow(if (enabled) elevation else 0.dp, shape, clip = false) + .background(colors.thumbColor(enabled), shape) + )) +} + +@Stable +internal fun SliderColors.thumbColor(enabled: Boolean): Color = + if (enabled) thumbColor else disabledThumbColor + +@Stable +internal fun SliderColors.trackColor(enabled: Boolean, active: Boolean): Color = + if (enabled) { + if (active) activeTrackColor else inactiveTrackColor + } else { + if (active) disabledActiveTrackColor else disabledInactiveTrackColor + } + +@Stable +internal fun SliderColors.tickColor(enabled: Boolean, active: Boolean): Color = + if (enabled) { + if (active) activeTickColor else inactiveTickColor + } else { + if (active) disabledActiveTickColor else disabledInactiveTickColor + } + +@OptIn(ExperimentalMaterial3Api::class) +internal val SliderState.coercedValueAsFraction + get() = calcFraction( + valueRange.start, + valueRange.endInclusive, + value.coerceIn(valueRange.start, valueRange.endInclusive) + ) + +private fun calcFraction(a: Float, b: Float, pos: Float) = + (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) + +@OptIn(ExperimentalMaterial3Api::class) +internal val SliderState.tickFractions: FloatArray + get() = stepsToTickFractions(steps) + + +private fun stepsToTickFractions(steps: Int): FloatArray { + return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) } +} + +private val HandleHeight = 20.0.dp +private val HandleWidth = 20.0.dp + +private val StateLayerSize = 40.0.dp +internal val ThumbWidth = HandleWidth +private val ThumbHeight = HandleHeight +private val ThumbSize = DpSize(ThumbWidth, ThumbHeight) +private val ThumbDefaultElevation = 1.dp +private val ThumbPressedElevation = 6.dp +private val TickMarksContainerSize = 2.0.dp +private val TickSize = TickMarksContainerSize + +// Internal to be referred to in tests +private val InactiveTrackHeight = 4.0.dp +private val TrackHeight = InactiveTrackHeight \ No newline at end of file diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/components/PedalboardPluginCard.kt b/app/src/main/java/com/mukuro/pedalboard/ui/components/PedalboardPluginCard.kt index b878b1b..7f092e7 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/components/PedalboardPluginCard.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/components/PedalboardPluginCard.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -55,6 +56,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin @@ -77,14 +79,18 @@ import com.mukuro.pedalboard.data.Plugin import kotlin.math.cos import kotlin.math.sin import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.palette.graphics.Palette import coil.compose.AsyncImage import com.mukuro.pedalboard.R import com.mukuro.pedalboard.data.Knob +import com.mukuro.pedalboard.ui.components.plugin.PluginKnob import com.mukuro.pedalboard.data.RangeSlider import com.mukuro.pedalboard.data.Slider import com.mukuro.pedalboard.data.Switch import com.mukuro.pedalboard.data.local.LocalPluginsDataProvider +import com.mukuro.pedalboard.ui.components.plugin.NeuroPluginKnob +import com.mukuro.pedalboard.ui.components.plugin.PluginSlider /** * TODO @@ -107,8 +113,8 @@ import com.mukuro.pedalboard.data.local.LocalPluginsDataProvider @Composable fun PedalboardPluginCard( plugin: Plugin, - navigateToDetail: (Long) -> Unit, - toggleSelection: (Long) -> Unit, + //navigateToDetail: (Long) -> Unit, + //toggleSelection: (Long) -> Unit, modifier: Modifier = Modifier, isOpened: Boolean = false, isSelected: Boolean = false, @@ -193,6 +199,15 @@ fun PedalboardPluginCard( Box( modifier = Modifier .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surfaceVariant, + ) + ), + shape = RectangleShape + ) // Possible draw_behind implementation /* .drawBehind { drawIntoCanvas { canvas -> @@ -237,12 +252,15 @@ fun PedalboardPluginCard( ) // TODO - reenable? // Generate palette - *//*val bitmap = remember { + val bitmap = remember { BitmapFactory.decodeResource( context.resources, plugin.coverDrawable ) - }*//* + } + }*/ + + /* *//* Create the Palette, pass the bitmap to it *//**//* //val palette = remember { Palette.from(bitmap).generate() } @@ -373,6 +391,7 @@ fun PedalboardPluginCard( //.fillMaxWidth() .fillMaxSize() .padding(4.dp), // 4-8 is ok + //.align(Alignment.CenterVertically), // TODO - why isn't this working? MUST MORE BLOOD BE SHED?!11 horizontalArrangement = plugin.horizontal ?: Arrangement.Center, //verticalArrangement = plugin.vertical ?: Arrangement.Top, //Arrangement.Center, //Arrangement.Absolute.SpaceEvenly, //.spacedBy(4.dp), maxItemsInEachRow = maxItemsCount //3 @@ -388,9 +407,9 @@ fun PedalboardPluginCard( */ plugin.elements?.forEach { when (it) { - is Knob -> PluginKnob( + is Knob -> NeuroPluginKnob( it, - knobSize = 100f, + knobSize = 80f, //100f - (it.id - 1f) * 10f, // < -- Stuff to check dynamic sizes onValueChanged = { value -> /* Placeholder code for handling volume change * Put here some action if needed in the future. @@ -398,11 +417,22 @@ fun PedalboardPluginCard( */ println(it.name+" changed: $value")}, value = 1f, - modifier = Modifier.size(height = 150.dp, width = 120.dp), + //modifier = Modifier.size(height = 150.dp, width = 120.dp), // TODO - wtf, remove this!! colors = colorArray// Let's settle for this size for now .____. ) is Switch -> {} - is Slider -> {} + is Slider -> { PluginSlider( + it, + onValueChanged = { value -> + /* Placeholder code for handling volume change + * Put here some action if needed in the future. + * Right now only prints logs on change. + */ + println(it.name+" changed: $value")}, + value = 1f, + //colors = colorArray + ) + } is RangeSlider -> {} // TODO - add final elements @@ -418,180 +448,7 @@ fun PedalboardPluginCard( } } -/** Small lerp function, cuz I found no easier way to map value from (0..1) to (min..max) - * WTF, there is no built-in lerp function? or am I stupid? (not hehe) - * TODO - refactor, maybe this additional func is overkill - */ -fun Float.mapRange( - fromMin: Float, - fromMax: Float, - toMin: Float, - toMax: Float -): Float { - if (fromMin == fromMax) { - throw IllegalArgumentException("Input range cannot have equal min and max values") - } - val clampedValue = this.coerceIn(fromMin, fromMax) - return (clampedValue - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin -} - -@Composable -fun PluginKnob( - knob: Knob, - modifier: Modifier = Modifier, - knobSize: Float = 100f, - knobColor: Color = Color.DarkGray, - indicatorColor: Color = Color.LightGray, - onValueChanged: (Float) -> Unit, - value: Float = 0f, - colors: Array? = null - //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future -) { - val startPosition : Float = (value * (knob.endPoint - knob.startPoint) / 360) // TODO fix calculation - var angle: Float by rememberSaveable { mutableStateOf(startPosition) } - - Column( - modifier = modifier - .wrapContentSize() - .pointerInput(Unit) { // Probably a good idea is to change the gesture to .draggable - detectDragGestures { change, _ -> - val dragDistance = change.position - change.previousPosition - angle += dragDistance.x / (8 * knobSize) - angle = angle.coerceIn(0f, 1f) - onValueChanged((angle * 2) - 1) // Map the angle to the range from -1 to 1 - } - } - ) { - Text( // Current knob value - text = "%.1f".format(angle.mapRange(0f,1f, knob.startPoint, knob.endPoint)).toFloat().toString()+knob.measure, - color = colors?.get(0) ?: Color.DarkGray, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth(fraction = 0.45f) - // would be good to add some outline here like in commented lines - //.clip(CircleShape) - .padding(top = 4.dp, bottom = 4.dp) - //.alpha(0.5f) - .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 12.dp)) - .align(Alignment.CenterHorizontally), - //.wrapContentSize(Alignment.TopCenter, unbounded = false), - maxLines = 1, - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Medium //FontWeight.Medium - //, fontSize = 12.sp) - ) - Box( - modifier = Modifier - //.fillMaxWidth(fraction = 0.8f) - .align(Alignment.CenterHorizontally) - ) { - Canvas( - modifier = Modifier - .fillMaxSize(fraction = 0.6f) - .aspectRatio(1f)// sizeIn(minWidth = 100.dp, minHeight = 100.dp, maxWidth = 120.dp, maxHeight = 120.dp) //.fillMaxSize() //fraction = 0.8f) - ) { - val centerX = size.width / 2 - val centerY = size.height / 2 - val radius = size.minDimension / 2 - 8.dp.toPx() - - // Draw the filled knob circle - drawCircle( - color = colors?.get(1) ?: knobColor, //?: knobColor, - center = Offset(centerX, centerY), - radius = radius - ) - - // Draw the indicator circle - // THIS IS STUPID SHIT (but somehow it's working... at least partially) - val indicatorRadius = radius * 0.2f - val angleOffset = 60 // Offset the angle by 150 degrees to start at 7 and end at 5 - val angleInDegrees = (angle * 360) / 1.2f //.coerceIn(0f, 300f) // SOMETHING FISHY HERE // MY CODE // FIXED EHHEHEHE :3 - val indicatorOffsetX = (radius - indicatorRadius) * cos(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() - val indicatorOffsetY = (radius - indicatorRadius) * sin(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() - val indicatorCenterX = centerX - indicatorOffsetX - val indicatorCenterY = centerY - indicatorOffsetY // Invert the Y-axis to start from the top - - val pointerLeftX = centerX + (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) - val pointerY = centerY - (sin(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) - val pointerRightX = centerX - (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) - - val colorStops = arrayOf( - 0.0f to Color.LightGray, //DarkGray, - 0.16f to Color.Red, - 0.33f to Color.LightGray, - 0.75f to Color.LightGray //DarkGray - ) - // Left marker - drawCircle( - color = colors?.get(1) ?: Color.LightGray, - center = Offset(pointerLeftX,pointerY), - radius = 4f - ) - // Right marker - drawCircle( - color = colors?.get(1) ?: Color.LightGray, - center = Offset(pointerRightX,pointerY), - radius = 4f - ) - // Arc indicator around the Knob - drawArc( - brush = Brush.sweepGradient(colorStops = colorStops), // listOf(Color.LightGray, Color.Magenta, Color.Red), - startAngle = 120f, - sweepAngle = angleInDegrees, - useCenter = false, - style = Stroke(width = 5f, cap = StrokeCap.Round) - ) - // Indicator on the Knob - drawCircle( - color = colors?.get(0) ?: indicatorColor, - center = Offset(indicatorCenterX, indicatorCenterY), - radius = indicatorRadius * 0.5f - ) - } - } - Text( - text = knob.name, - color = colors?.get(0) ?: Color.DarkGray, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth(fraction = 0.5f) - .align(Alignment.CenterHorizontally) - .padding(top = 4.dp, bottom = 4.dp) - .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 30.dp)), - //style = TextStyle(color = Color.Black, fontSize = 12.sp, fontWeight = FontWeight.Bold), - style = MaterialTheme.typography.labelMedium //titleMedium //.merge( - /* TextStyle(color = colors?.get(1) ?: Color.DarkGray, - //fontSize = 80.sp, - drawStyle = Stroke( - miter = 10f, - width = 2f, - join = StrokeJoin.Round - ) - ) - )*/, - maxLines = 1, - //fontWeight = FontWeight.ExtraBold - ) - } -} - -// TODO - WIP - barely started -@Composable -fun PluginSlider( - slider: Slider, - modifier: Modifier = Modifier, - sliderColor: Color = Color.DarkGray, - indicatorColor: Color = Color.LightGray, - onValueChanged: (Float) -> Unit, - value: Float = 0f - //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future -) { - val startPosition : Float = (value * (slider.endValue - slider.startValue) / 360) // TODO fix calculation - var point: Float by rememberSaveable { mutableStateOf(startPosition) } - - -} /*fun getCardImageResource(stringData: String): Int? { @@ -604,40 +461,33 @@ fun PluginSlider( else -> null } }*/ - +@PreviewLightDark @Preview(showBackground = false, heightDp = 600) @Composable fun PedalboardTabletCardPreview() { PedalboardPluginCard( plugin = LocalPluginsDataProvider.allPlugins[1], - navigateToDetail = { /*pluginId -> - navigateToDetail(pluginId, PedalboardContentType.SINGLE_PANE)*/ - }, - toggleSelection = {}, //togglePluginSelection, isOpened = false, //openedPlugin?.id == plugin.id, isSelected = false //selectedPluginIds.contains(plugin.id) ) } -@Preview(showBackground = false, widthDp = 400, heightDp = 600) +@PreviewLightDark +@Preview(showBackground = false, widthDp = 400, heightDp = 700) @Composable fun PedalboardMobileCardPreview() { PedalboardPluginCard( plugin = LocalPluginsDataProvider.allPlugins[1], - navigateToDetail = { /*pluginId -> - navigateToDetail(pluginId, PedalboardContentType.SINGLE_PANE)*/ - }, - toggleSelection = {}, //togglePluginSelection, isOpened = false, //openedPlugin?.id == plugin.id, isSelected = false //selectedPluginIds.contains(plugin.id) ) } -@Preview(showBackground = false, widthDp = 200, heightDp = 200) +/*@Preview(showBackground = false, widthDp = 200, heightDp = 200) @Composable fun PedalboardSmallKnobPreview() { PluginKnob( - knob = LocalPluginsDataProvider.TestKnob.knobSet1[3], + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], modifier = Modifier.size(200.dp), knobSize = 200f, //knobName = "Gain 2", @@ -682,7 +532,7 @@ fun PedalboardBigKnobPreview() { println("Volume changed: $value") } ) -} +}*/ diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginKnob.kt b/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginKnob.kt new file mode 100644 index 0000000..da606d0 --- /dev/null +++ b/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginKnob.kt @@ -0,0 +1,924 @@ +package com.mukuro.pedalboard.ui.components.plugin + +import android.graphics.Paint +import android.graphics.Path +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mukuro.pedalboard.data.Knob +import com.mukuro.pedalboard.data.local.LocalPluginsDataProvider +import kotlin.math.cos +import kotlin.math.sin + +/** Small lerp function, cuz I found no easier way to map value from (0..1) to (min..max) + * WTF, there is no built-in lerp function? or am I stupid? (not hehe) + * TODO - refactor, maybe this additional func is overkill + */ +fun Float.mapRange( + fromMin: Float, + fromMax: Float, + toMin: Float, + toMax: Float +): Float { + if (fromMin == fromMax) { + throw IllegalArgumentException("Input range cannot have equal min and max values") + } + + val clampedValue = this.coerceIn(fromMin, fromMax) + return (clampedValue - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin +} + +@Composable +fun PluginKnob( + knob: Knob, + modifier: Modifier = Modifier, + knobSize: Float, // = 100f, + knobColor: Color = Color.DarkGray, + indicatorColor: Color = Color.LightGray, + onValueChanged: (Float) -> Unit, + value: Float = 0f, + colors: Array? = null + //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future +) { + val startPosition : Float = (value * (knob.endPoint - knob.startPoint) / 360) // TODO fix calculation + var angle: Float by rememberSaveable { mutableStateOf(startPosition) } + + Column( + modifier = modifier + .requiredSize(knobSize.dp, knobSize.dp + 24.dp) + //.requiredWidth(knobSize.dp) + //.requiredHeight(knobSize.dp) + .wrapContentSize() + .pointerInput(Unit) { // Probably a good idea is to change the gesture to .draggable + detectDragGestures { change, _ -> + val dragDistance = change.position - change.previousPosition + angle += dragDistance.x / (8 * knobSize) + angle = angle.coerceIn(0f, 1f) + onValueChanged((angle * 2) - 1) // Map the angle to the range from -1 to 1 + } + } + ) { + Text( // Current knob value + text = "%.1f".format(angle.mapRange(0f,1f, knob.startPoint, knob.endPoint)).toFloat().toString()+knob.measure, + color = colors?.get(0) ?: Color.DarkGray, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(fraction = 0.45f) + // would be good to add some outline here like in commented lines + //.clip(CircleShape) + .padding(top = 4.dp, bottom = 4.dp) + //.alpha(0.5f) + .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 12.dp)) + .align(Alignment.CenterHorizontally), + //.wrapContentSize(Alignment.TopCenter, unbounded = false), + maxLines = 1, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium //FontWeight.Medium + //, fontSize = 12.sp) + ) + Box( + modifier = Modifier + //.fillMaxWidth(fraction = 0.8f) + .align(Alignment.CenterHorizontally) + ) { + Canvas( + modifier = Modifier + .fillMaxSize(fraction = 0.6f) + .aspectRatio(1f)// sizeIn(minWidth = 100.dp, minHeight = 100.dp, maxWidth = 120.dp, maxHeight = 120.dp) //.fillMaxSize() //fraction = 0.8f) + ) { + val centerX = size.width / 2 + val centerY = size.height / 2 + val radius = size.minDimension / 2 - 8.dp.toPx() + + // Draw the filled knob circle + drawCircle( + color = colors?.get(1) ?: knobColor, //?: knobColor, + center = Offset(centerX, centerY), + radius = radius + ) + + // Draw the indicator circle + // THIS IS STUPID SHIT (but somehow it's working... at least partially) + val indicatorRadius = radius * 0.2f + val angleOffset = 60 // Offset the angle by 150 degrees to start at 7 and end at 5 + val angleInDegrees = (angle * 360) / 1.2f //.coerceIn(0f, 300f) // SOMETHING FISHY HERE // MY CODE // FIXED EHHEHEHE :3 + val indicatorOffsetX = (radius - indicatorRadius) * cos(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() + val indicatorOffsetY = (radius - indicatorRadius) * sin(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() + val indicatorCenterX = centerX - indicatorOffsetX + val indicatorCenterY = centerY - indicatorOffsetY // Invert the Y-axis to start from the top + + /** Dot marker offset + * + * Previous formula was + * centerX + (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + * but I try to use some dynamic distance instead of a const 1.28f + * + * So let's try using fraction of KnobSize + */ + val pointerLeftX = centerX + (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + val pointerY = centerY - (sin(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + val pointerRightX = centerX - (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + + val colorStops = arrayOf( + 0.0f to Color.LightGray, //DarkGray, + 0.16f to Color.Red, + 0.33f to Color.LightGray, + 0.75f to Color.LightGray //DarkGray + ) + // Left marker + drawCircle( + color = colors?.get(1) ?: Color.LightGray, + center = Offset(pointerLeftX,pointerY), + radius = 4f + ) + // Right marker + drawCircle( + color = colors?.get(1) ?: Color.LightGray, + center = Offset(pointerRightX,pointerY), + radius = 4f + ) + // Arc indicator around the Knob + drawArc( + brush = Brush.sweepGradient(colorStops = colorStops), // listOf(Color.LightGray, Color.Magenta, Color.Red), + startAngle = 120f, + sweepAngle = angleInDegrees, + useCenter = false, + style = Stroke(width = 5f, cap = StrokeCap.Round) + ) + // Indicator on the Knob + drawCircle( + color = colors?.get(0) ?: indicatorColor, + center = Offset(indicatorCenterX, indicatorCenterY), + radius = indicatorRadius * 0.5f + ) + } + } + Text( + text = knob.name, + color = colors?.get(0) ?: Color.DarkGray, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(fraction = 0.5f) + .align(Alignment.CenterHorizontally) + .padding(top = 4.dp, bottom = 4.dp) + .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 30.dp)), + //style = TextStyle(color = Color.Black, fontSize = 12.sp, fontWeight = FontWeight.Bold), + style = MaterialTheme.typography.labelMedium //titleMedium //.merge( + /* TextStyle(color = colors?.get(1) ?: Color.DarkGray, + //fontSize = 80.sp, + drawStyle = Stroke( + miter = 10f, + width = 2f, + join = StrokeJoin.Round + ) + ) + )*/, + maxLines = 1, + //fontWeight = FontWeight.ExtraBold + ) + } +} + +@Composable +fun RadialPluginKnob( + knob: Knob, + modifier: Modifier = Modifier, + knobSize: Float, // = 100f, + knobColor: Color = Color.DarkGray, + indicatorColor: Color = Color.LightGray, + onValueChanged: (Float) -> Unit, + value: Float = 0f, + colors: Array? = null + //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future +) { + val startPosition : Float = (value * (knob.endPoint - knob.startPoint) / 360) // TODO fix calculation + var angle: Float by rememberSaveable { mutableStateOf(startPosition) } + + // Creating a canvas with various attributes + + Column( + modifier = modifier + .requiredWidth(knobSize.dp) + .requiredHeight(knobSize.dp) + .wrapContentSize() + .pointerInput(Unit) { // Probably a good idea is to change the gesture to .draggable + detectDragGestures { change, _ -> + val dragDistance = change.position - change.previousPosition + angle += dragDistance.x / (8 * knobSize) + angle = angle.coerceIn(0f, 1f) + onValueChanged((angle * 2) - 1) // Map the angle to the range from -1 to 1 + } + } + ) { + Text( // Current knob value + text = "%.1f".format(angle.mapRange(0f,1f, knob.startPoint, knob.endPoint)).toFloat().toString()+knob.measure, + color = colors?.get(0) ?: Color.DarkGray, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(fraction = 0.45f) + // would be good to add some outline here like in commented lines + //.clip(CircleShape) + .padding(top = 4.dp, bottom = 4.dp) + //.alpha(0.5f) + .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 12.dp)) + .align(Alignment.CenterHorizontally), + //.wrapContentSize(Alignment.TopCenter, unbounded = false), + maxLines = 1, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium //FontWeight.Medium + //, fontSize = 12.sp) + ) + Box( + modifier = Modifier + //.fillMaxWidth(fraction = 0.8f) + .align(Alignment.CenterHorizontally) + ) { + Canvas( + modifier = Modifier + .fillMaxSize(fraction = 0.6f) + .aspectRatio(1f)// sizeIn(minWidth = 100.dp, minHeight = 100.dp, maxWidth = 120.dp, maxHeight = 120.dp) //.fillMaxSize() //fraction = 0.8f) + ) { + + // CURVED TEXT + drawIntoCanvas { + val textPadding = 0.dp.toPx() + val arcHeight = knobSize.dp.toPx() + val arcWidth = knobSize.dp.toPx() + + // Path for curved text + val path = Path().apply { + addArc(0f, textPadding, arcWidth/2, arcHeight/2, 180f, 180f) + } + + it.nativeCanvas.drawTextOnPath( + "Knob name!!", + path, + 0f - knobSize, + 0f, + Paint().apply { + textSize = 10.sp.toPx() + textAlign = Paint.Align.CENTER + + }, + + ) + } + + val centerX = size.width / 2 + val centerY = size.height / 2 + val radius = size.minDimension / 2 - 8.dp.toPx() + + // Draw the filled knob circle + drawCircle( + color = colors?.get(1) ?: knobColor, //?: knobColor, + center = Offset(centerX, centerY), + radius = radius + ) + + // Draw the indicator circle + // THIS IS STUPID SHIT (but somehow it's working... at least partially) + val indicatorRadius = radius * 0.2f + val angleOffset = 60 // Offset the angle by 150 degrees to start at 7 and end at 5 + val angleInDegrees = (angle * 360) / 1.2f //.coerceIn(0f, 300f) // SOMETHING FISHY HERE // MY CODE // FIXED EHHEHEHE :3 + val indicatorOffsetX = (radius - indicatorRadius) * cos(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() + val indicatorOffsetY = (radius - indicatorRadius) * sin(Math.toRadians(angleInDegrees.toDouble()-angleOffset.toDouble())).toFloat() + val indicatorCenterX = centerX - indicatorOffsetX + val indicatorCenterY = centerY - indicatorOffsetY // Invert the Y-axis to start from the top + + val pointerLeftX = centerX + (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + val pointerY = centerY - (sin(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + val pointerRightX = centerX - (cos(Math.toRadians(254.0)).toFloat() * (radius * 1.28f)) + + val colorStops = arrayOf( + 0.0f to Color.LightGray, //DarkGray, + 0.16f to Color.Red, + 0.33f to Color.LightGray, + 0.75f to Color.LightGray //DarkGray + ) + // Left marker + drawCircle( + color = colors?.get(1) ?: Color.LightGray, + center = Offset(pointerLeftX,pointerY), + radius = 4f + ) + // Right marker + drawCircle( + color = colors?.get(1) ?: Color.LightGray, + center = Offset(pointerRightX,pointerY), + radius = 4f + ) + // Arc indicator around the Knob + drawArc( + brush = Brush.sweepGradient(colorStops = colorStops), // listOf(Color.LightGray, Color.Magenta, Color.Red), + startAngle = 120f, + sweepAngle = angleInDegrees, + useCenter = false, + style = Stroke(width = 5f, cap = StrokeCap.Round) + ) + // Indicator on the Knob + drawCircle( + color = colors?.get(0) ?: indicatorColor, + center = Offset(indicatorCenterX, indicatorCenterY), + radius = indicatorRadius * 0.5f + ) + } + } + Text( + text = knob.name, + color = colors?.get(0) ?: Color.DarkGray, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(fraction = 0.5f) + .align(Alignment.CenterHorizontally) + .padding(top = 4.dp, bottom = 4.dp) + .background(color = (colors?.get(1) ?: knobColor), shape = RoundedCornerShape(size = 30.dp)), + //style = TextStyle(color = Color.Black, fontSize = 12.sp, fontWeight = FontWeight.Bold), + style = MaterialTheme.typography.labelMedium //titleMedium //.merge( + /* TextStyle(color = colors?.get(1) ?: Color.DarkGray, + //fontSize = 80.sp, + drawStyle = Stroke( + miter = 10f, + width = 2f, + join = StrokeJoin.Round + ) + ) + )*/, + maxLines = 1, + //fontWeight = FontWeight.ExtraBold + ) + } +} + +@Composable +fun NeuroPluginKnob( + knob: Knob, + modifier: Modifier = Modifier, + knobSize: Float, // = 100f, + knobColor: Color = Color.DarkGray, + indicatorColor: Color = Color.LightGray, + onValueChanged: (Float) -> Unit, + value: Float = 0f, + colors: Array? = null // TODO - refactor, need better way to pass color data + //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future +) { + + /** + * DONE! + * But still TODO + * + * Things to polish/improve: + * 0) Rename the knob to be the main one, probably? + * 1) implement color initialisation + * - some basics are already here + * - at the moment arc color is static and does not react to changing theme (light/dark) + * - as well as knob indicator + * 2) probably remove padding OR set proper size value. + * - right now size of the element depends on the knobSize, but the UI element itself is wider + * - probably better to use the card Flow element spacing instead of padding + * 3) Re-enable shadows/highlights where needed, it's code is commented ATM + * 4) Add constrains for the min/max knob size value? + * 5) Find out magic offset for labels canvas, lol + * 6) And clean up all the mess, of course :D + * */ + val labelColor = MaterialTheme.colorScheme.onSurface + val secondaryColor = MaterialTheme.colorScheme.onSurface + + // Values for Knob functionality + val startPosition : Float = (value * (knob.endPoint - knob.startPoint) / 100) // TODO fix calculation + var angle: Float by rememberSaveable { mutableFloatStateOf(startPosition) } + + //val gray100 = Color(0xffe5e5e5) + //val gray200 = Color(0xffd0d0d0) + + // Values for Knob handles + val handleRadiusOffset = knobSize / 2f * 1.24f // TODO - find proper coeff + val handleSize = knobSize.div(8) // TODO - find proper coeff + val handleX = handleRadiusOffset * cos(Math.toRadians(240.0)).toFloat() + val handleY = handleRadiusOffset * sin(Math.toRadians(240.0)).toFloat() + + var angleInDegrees = (angle * 360) / 1.2f + + // Values for Knob indicator + val indicatorRadiusOffset = knobSize / 2f * 0.74f // TODO - find proper coeff + val indicatorSize = knobSize.div(8) // TODO - find proper coeff + val indicatorX = indicatorRadiusOffset * cos(Math.toRadians(120f + angleInDegrees.toDouble())).toFloat() + val indicatorY = indicatorRadiusOffset * sin(Math.toRadians(120f + angleInDegrees.toDouble())).toFloat() + + + Box( // Main Knob CONTAINER + Modifier + //.weight(1f) + //.requiredSize(100.dp,100.dp) + .padding(18.dp) + .requiredSize(knobSize.dp * 1.24f,knobSize.dp * 1.24f) // TODO - recalculate size + + // + //.wrapContentSize() + //.weight(1f) +/* .background( + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f), + ) + ), + shape = CircleShape + )*/ + //.fillMaxSize() +/* .border( + width = 1.dp, + shape = RectangleShape, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = .25f), + Color.Black.copy(alpha = .25f), + ) + ) + )*/, + contentAlignment = Alignment.Center + ) { + // Main Knob circle + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center)//fraction = 0.8f) + .requiredSize(knobSize.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f), + ) + ), + shape = CircleShape + ) + .border( + width = knobSize.div(20f).dp,//6.dp, + shape = CircleShape, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = .25f), + Color.Black.copy(alpha = .25f), + ) + ) + ) + .pointerInput(Unit) { // Probably a good idea is to change the gesture to .draggable? Not sure + detectDragGestures { change, _ -> + val dragDistance = change.position - change.previousPosition + angle += dragDistance.x / (8 * knobSize) + angle = angle.coerceIn(0f, 1f) + angleInDegrees = (angle * 360) / 1.2f + onValueChanged((angle * 2) - 1) // Map the angle to the range from -1 to 1 + } + } + ) { + + // Knob Highlight +/* Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, -20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.White.copy(alpha = .05f), CircleShape) + )*/ + // Knob Shadow + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, 20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.Black.copy(alpha = .05f), CircleShape) + ) + + /** Indicator */ + Box( // Indicator + Modifier // WTF IS THIS + .offset(indicatorX.dp, indicatorY.dp) + //.offset((indicatorRadiusOffset.times(cos(Math.toRadians(angleInDegrees.toDouble()))).toFloat()).dp, (indicatorRadiusOffset.times(sin(Math.toRadians(angleInDegrees.toDouble()))).toFloat()).dp) + .align(Alignment.Center) + .size(handleSize.dp) + //.fillMaxSize(fraction = 0.1f) + .background( + color = Color.Black.copy(alpha = .75f), + shape = CircleShape + ) + /* .border( + width = 2.dp, + shape = CircleShape, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = .25f), + Color.Black.copy(alpha = .25f), + ) + ) + )*/ + ) { + /* // Indicator highlight & shadow + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, -20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.White.copy(alpha = .10f), CircleShape) + ) + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, 20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.Black.copy(alpha = .10f), CircleShape) + )*/ + } + } + + Canvas( + modifier = Modifier + .fillMaxSize() + ) { + // TODO - probably remove? + val centerX = size.width / 2 + val centerY = size.height / 2 + val radius = size.minDimension * 1.2f// / 2 + 8.dp.toPx() + + val colorStops = arrayOf( + 0.0f to Color.LightGray, //DarkGray, + 0.16f to Color.Red, + 0.33f to Color.LightGray, + 0.75f to Color.LightGray //DarkGray + ) + + val angleInDegrees = (angle * 360) / 1.2f + drawArc( + brush = Brush.sweepGradient(colorStops = colorStops), // listOf(Color.LightGray, Color.Magenta, Color.Red), + startAngle = 120f, + sweepAngle = angleInDegrees, + //size = Size(radius,radius), + useCenter = false, + style = Stroke(width = knobSize.div(8f), cap = StrokeCap.Round) + ) + } + + /** HANDLES */ + // TODO - reduce handle shadows/highlights - check on white theme + Box( // Left handle + Modifier + .offset(handleX.dp,-handleY.dp) + .align(Alignment.Center) + .size(handleSize.dp) + //.fillMaxSize(fraction = 0.1f) + .background( + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surfaceVariant, + ) + ), + shape = CircleShape + ) + .border( + width = 4.dp, + shape = CircleShape, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = .25f), + Color.Black.copy(alpha = .25f), + ) + ) + ) + ) { + // Left handle highlight & shadow + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, -20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.White.copy(alpha = .10f), CircleShape) + ) + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, 20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.Black.copy(alpha = .10f), CircleShape) + ) + } + + Box( // Right handle + Modifier + .offset(-handleX.dp,-handleY.dp) + .align(Alignment.Center) + .size(handleSize.dp) + //.fillMaxSize(fraction = 0.1f) + .background( + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surfaceVariant, + ) + ), + shape = CircleShape + ) + .border( + width = 4.dp, + shape = CircleShape, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = .25f), + Color.Black.copy(alpha = .25f), + ) + ) + ) + ) { + // Right handle highlight & shadow + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, -20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.White.copy(alpha = .10f), CircleShape) + ) + Box( + Modifier + .fillMaxSize() + .offset { IntOffset(0, 20) } + .blur(10.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .background(Color.Black.copy(alpha = .10f), CircleShape) + ) + } + + // Creating a canvas with text + Canvas(modifier = Modifier.fillMaxSize().offset(-(knobSize/16f).dp,-(knobSize/16f).dp)) //-8.dp,-8.dp)) + { + drawIntoCanvas { + //val textPadding = 10.dp.toPx() + val arcHeight = (knobSize * 1.44f).dp.toPx() + val arcWidth = (knobSize * 1.44f).dp.toPx() + + // Path for curved Name + val namePath = Path().apply { + addArc(0f, 0f /*textPadding*/, arcWidth, arcHeight, 160f, 100f) + } + it.nativeCanvas.drawTextOnPath( + //"Test name test name test name test name test name ", + knob.name, + namePath, + -10f, + -10f, + Paint().apply { + color = labelColor.toArgb() + textSize = (knobSize/8f).sp.toPx() + textAlign = Paint.Align.CENTER + //textSkewX = -0.15f + //textScaleX = 1.1f + isFakeBoldText = true + //letterSpacing = 1.1f + } + ) + + // Path for curved value + val valuePath = Path().apply { + addArc(0f, 0f /*textPadding*/, arcWidth, arcHeight, 80f, -90f) + } + it.nativeCanvas.drawTextOnPath( + //"Test name test name test name test name test name ",// + "%.1f".format(angle.mapRange(0f,1f, knob.startPoint, knob.endPoint)).toFloat().toString()+knob.measure, + valuePath, + 10f, + 10f, + Paint().apply { + color = labelColor.toArgb() + textSize = (knobSize/8f).sp.toPx() + textAlign = Paint.Align.CENTER + //textSkewX = -0.15f + //textScaleX = 1.1f + //isFakeBoldText = true + } + ) + + } + + } + } + +} + + +@Preview("Size 80", "Regular", showBackground = true, widthDp = 250, heightDp = 250) +@Composable +fun NeuroKnobPreview() { + NeuroPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 80f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + }, + value = 1f, + ) +} + +@Preview("Size 100", "Regular", showBackground = true, widthDp = 250, heightDp = 250) +@Composable +fun NeuroKnobPreviewMed() { + NeuroPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 100f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + }, + value = 1f, + ) +} + +@Preview("Size 100", "Regular", showBackground = true, widthDp = 250, heightDp = 250) +@Composable +fun NeuroKnobPreviewLarge() { + NeuroPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 150f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + }, + value = 0.8f, + ) +} + +@Preview("Size 250", "Regular", showBackground = true, widthDp = 250, heightDp = 250) +@Composable +fun NeuroKnobPreviewExtraLarge() { + NeuroPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 650f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + }, + value = 0.8f, + ) +} + +//@PreviewFontScale +//@PreviewLightDark +//@PreviewDynamicColors +@Preview("Size 50", "Regular", showBackground = true, widthDp = 250, heightDp = 250) +@Composable +fun SmallKnob() { + PluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 50f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + }, + value = 0f + ) +} + +@Preview("Size 100", "Regular", showBackground = true, widthDp = 150, heightDp = 200) +@Composable +fun MediumKnob() { + PluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 100f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + } + ) +} + +@Preview("Size 150", "Regular", showBackground = true, widthDp = 150, heightDp = 200) +@Composable +fun LargeKnob() { + PluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 150f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + } + ) +} + +@Preview("Size 80", "Radial", showBackground = false, widthDp = 150, heightDp = 200) +@Composable +fun RadialSmall() { + RadialPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 80f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + } + ) +} + +@Preview("Size 170", "Radial", showBackground = false, widthDp = 150, heightDp = 200) +@Composable +fun RadialMedium() { + RadialPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 170f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + } + ) +} + +@Preview("Size 200", "Radial", showBackground = false, widthDp = 150, heightDp = 200) +@Composable +fun RadialLarge() { + RadialPluginKnob( + knob = LocalPluginsDataProvider.TestKnob.knobSet1[2], + modifier = Modifier, + knobSize = 200f, + //knobName = "Gain 2", + knobColor = Color.LightGray, + indicatorColor = Color.DarkGray, + onValueChanged = { value -> + // Placeholder code for handling volume change + println("Volume changed: $value") + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginSlider.kt b/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginSlider.kt new file mode 100644 index 0000000..da1314c --- /dev/null +++ b/app/src/main/java/com/mukuro/pedalboard/ui/components/plugin/PluginSlider.kt @@ -0,0 +1,262 @@ +package com.mukuro.pedalboard.ui.components.plugin + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults.colors +import androidx.compose.material3.SliderState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mukuro.pedalboard.data.Slider + +// TODO - WIP - barely started +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PluginSlider( + slider: Slider, + modifier: Modifier = Modifier, + sliderColor: Color = Color.DarkGray, + indicatorColor: Color = Color.LightGray, + onValueChanged: (Float) -> Unit, + value: Float = 0f // TODO - rename? + //content: @Composable () -> Unit // TODO - probably remove, don't think it may be needed here in the future +) { + val startPosition : Float = (value * (slider.endValue - slider.startValue) / 360) // TODO fix calculation + var point: Float by rememberSaveable { mutableStateOf(startPosition) } + + val sliderState = SliderState( + value = startPosition, + valueRange = slider.startValue..slider.endValue + ) + + androidx.compose.material3.Slider( + modifier = Modifier.fillMaxWidth(), + state = sliderState + ) + +} + +// TODO - from here - unused ATM code for track & thumb + +/** + * + * Copied from older material 3 library source code + * + */ +@Composable +@ExperimentalMaterial3Api +fun CustomTrack( + sliderState: SliderState, + modifier: Modifier = Modifier, + colors: SliderColors = colors(), + enabled: Boolean = true +) { + val inactiveTrackColor = colors.trackColor(enabled, active = false) + val activeTrackColor = colors.trackColor(enabled, active = true) + val inactiveTickColor = colors.tickColor(enabled, active = false) + val activeTickColor = colors.tickColor(enabled, active = true) + Canvas( + modifier + .fillMaxWidth() + .height(TrackHeight) + ) { + drawTrack( + sliderState.tickFractions, + 0f, + sliderState.coercedValueAsFraction, + inactiveTrackColor, + activeTrackColor, + inactiveTickColor, + activeTickColor + ) + } +} + + +private fun DrawScope.drawTrack( + tickFractions: FloatArray, + activeRangeStart: Float, + activeRangeEnd: Float, + inactiveTrackColor: Color, + activeTrackColor: Color, + inactiveTickColor: Color, + activeTickColor: Color +) { + val isRtl = layoutDirection == LayoutDirection.Rtl + val sliderLeft = Offset(0f, center.y) + val sliderRight = Offset(size.width, center.y) + val sliderStart = if (isRtl) sliderRight else sliderLeft + val sliderEnd = if (isRtl) sliderLeft else sliderRight + val tickSize = TickSize.toPx() + val trackStrokeWidth = TrackHeight.toPx() + drawLine( + inactiveTrackColor, + sliderStart, + sliderEnd, + trackStrokeWidth, + StrokeCap.Round + ) + val sliderValueEnd = Offset( + sliderStart.x + + (sliderEnd.x - sliderStart.x) * activeRangeEnd, + center.y + ) + + val sliderValueStart = Offset( + sliderStart.x + + (sliderEnd.x - sliderStart.x) * activeRangeStart, + center.y + ) + + drawLine( + activeTrackColor, + sliderValueStart, + sliderValueEnd, + trackStrokeWidth, + StrokeCap.Round + ) + + for (tick in tickFractions) { + val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart + drawCircle( + color = if (outsideFraction) inactiveTickColor else activeTickColor, + center = Offset( + androidx.compose.ui.geometry.lerp(sliderStart, sliderEnd, tick).x, + center.y + ), + radius = tickSize / 2f + ) + } +} + + +@Composable +fun CustomThumb( + interactionSource: MutableInteractionSource, + modifier: Modifier = Modifier, + colors: SliderColors = colors(), + enabled: Boolean = true, + thumbSize: DpSize = ThumbSize +) { + val interactions = remember { mutableStateListOf() } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> interactions.add(interaction) + is PressInteraction.Release -> interactions.remove(interaction.press) + is PressInteraction.Cancel -> interactions.remove(interaction.press) + is DragInteraction.Start -> interactions.add(interaction) + is DragInteraction.Stop -> interactions.remove(interaction.start) + is DragInteraction.Cancel -> interactions.remove(interaction.start) + } + } + } + + val elevation = if (interactions.isNotEmpty()) { + ThumbPressedElevation + } else { + ThumbDefaultElevation + } + val shape = CircleShape + + @Suppress("DEPRECATION_ERROR") + (Spacer( + modifier + .size(thumbSize) + .indication( + interactionSource = interactionSource, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = StateLayerSize / 2 + ) + ) + .hoverable(interactionSource = interactionSource) + .shadow(if (enabled) elevation else 0.dp, shape, clip = false) + .background(colors.thumbColor(enabled), shape) + )) +} + +@Stable +internal fun SliderColors.thumbColor(enabled: Boolean): Color = + if (enabled) thumbColor else disabledThumbColor + +@Stable +internal fun SliderColors.trackColor(enabled: Boolean, active: Boolean): Color = + if (enabled) { + if (active) activeTrackColor else inactiveTrackColor + } else { + if (active) disabledActiveTrackColor else disabledInactiveTrackColor + } + +@Stable +internal fun SliderColors.tickColor(enabled: Boolean, active: Boolean): Color = + if (enabled) { + if (active) activeTickColor else inactiveTickColor + } else { + if (active) disabledActiveTickColor else disabledInactiveTickColor + } + +@OptIn(ExperimentalMaterial3Api::class) +internal val SliderState.coercedValueAsFraction + get() = calcFraction( + valueRange.start, + valueRange.endInclusive, + value.coerceIn(valueRange.start, valueRange.endInclusive) + ) + +private fun calcFraction(a: Float, b: Float, pos: Float) = + (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) + +@OptIn(ExperimentalMaterial3Api::class) +internal val SliderState.tickFractions: FloatArray + get() = stepsToTickFractions(steps) + + +private fun stepsToTickFractions(steps: Int): FloatArray { + return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) } +} + +private val HandleHeight = 20.0.dp +private val HandleWidth = 20.0.dp + +private val StateLayerSize = 40.0.dp +internal val ThumbWidth = HandleWidth +private val ThumbHeight = HandleHeight +private val ThumbSize = DpSize(ThumbWidth, ThumbHeight) +private val ThumbDefaultElevation = 1.dp +private val ThumbPressedElevation = 6.dp +private val TickMarksContainerSize = 2.0.dp +private val TickSize = TickMarksContainerSize + +// Internal to be referred to in tests +private val InactiveTrackHeight = 4.0.dp +private val TrackHeight = InactiveTrackHeight \ No newline at end of file diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationActions.kt b/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationActions.kt index 31f9a43..c51a91b 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationActions.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationActions.kt @@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowForward import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.filled.Moped import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Star @@ -19,6 +20,7 @@ object PedalboardRoute { const val PRESETS = "Presets" const val RECORDED = "Recorded" const val DRUMS = "Drums" + const val TEST = "TEST" } data class PedalboardTopLevelDestination( @@ -82,6 +84,12 @@ val TOP_LEVEL_DESTINATIONS = listOf( selectedIcon = Icons.Default.Star, unselectedIcon = Icons.Default.Star, iconTextId = R.string.tab_drums + ), + PedalboardTopLevelDestination( + route = PedalboardRoute.TEST, + selectedIcon = Icons.Default.Moped, + unselectedIcon = Icons.Default.Moped, + iconTextId = R.string.test ) ) \ No newline at end of file diff --git a/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationComponents.kt b/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationComponents.kt index 756ea5a..3556070 100644 --- a/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationComponents.kt +++ b/app/src/main/java/com/mukuro/pedalboard/ui/navigation/PedalboardNavigationComponents.kt @@ -11,6 +11,7 @@ import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +26,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ChevronLeft @@ -54,7 +57,7 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Slider +//import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderState import androidx.compose.material3.Switch @@ -65,6 +68,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -85,10 +89,13 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.sp import com.mukuro.pedalboard.R +import com.mukuro.pedalboard.ui.components.CustomSlider +//import com.mukuro.pedalboard.ui.components.Slider import com.mukuro.pedalboard.ui.utils.PedalboardNavigationContentPosition import kotlinx.coroutines.launch @@ -295,6 +302,11 @@ fun ModalNavigationDrawerContent( var expandedOutput by remember { mutableStateOf(false) } var selectedOutput by remember { mutableStateOf(inputOptions[0]) } + // TEST values for sliders + var value by remember { mutableFloatStateOf(.5f) } + var sliderPosition by remember { mutableStateOf(0f) } + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + ModalDrawerSheet { // TODO remove custom nav drawer content positioning when NavDrawer component supports it. ticket : b/232495216 Layout( @@ -419,7 +431,7 @@ fun ModalNavigationDrawerContent( ) { LinearProgressIndicator( progress = { currentInput }, - color = Color(0xFF5EC281), //ProgressIndicatorDefaults.linearColor, + color = Color(0xff13b64d), //ProgressIndicatorDefaults.linearColor, trackColor = MaterialTheme.colorScheme.surfaceBright, modifier = Modifier.fillMaxWidth(), strokeCap = StrokeCap.Round @@ -432,11 +444,20 @@ fun ModalNavigationDrawerContent( .fillMaxWidth(), contentAlignment = Alignment.Center ) { - Slider( + +/* Slider( modifier = Modifier.fillMaxWidth(), state = SliderState(1f), - colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceBright) - ) + colors = SliderDefaults + .colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceBright), + // TODO - remove this part, default thumb is not customizable in M3 + thumb = { + SliderDefaults.Thumb( + thumbSize = DpSize(width = 16.dp, height = 16.dp), + interactionSource = remember { MutableInteractionSource() } + ) + } + )*/ } } @@ -479,11 +500,11 @@ fun ModalNavigationDrawerContent( .fillMaxWidth(), contentAlignment = Alignment.Center ) { - Slider( +/* Slider( modifier = Modifier.fillMaxWidth(), state = SliderState(1f), colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceBright) - ) + )*/ } } @@ -950,6 +971,9 @@ fun DismissibleNavigationDrawerContent( // TODO - remove obsolete stuff var expandedOutput by remember { mutableStateOf(false) } var selectedOutput by remember { mutableStateOf(inputOptions[0]) } + // Experimental stuff for Sliders + var value by remember { mutableFloatStateOf(.5f) } + DismissibleDrawerSheet() { //// Want to make it smaller, but UI gets fucked then, fix it then add >>> modifier = Modifier.requiredWidth(300.dp) // TODO remove custom nav drawer content positioning when NavDrawer component supports it. ticket : b/232495216 @@ -1089,7 +1113,7 @@ fun DismissibleNavigationDrawerContent( // TODO - remove obsolete stuff .fillMaxWidth(), contentAlignment = Alignment.Center ) { - Slider( + CustomSlider( modifier = Modifier.fillMaxWidth(), state = SliderState(1f), colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceBright) @@ -1136,7 +1160,7 @@ fun DismissibleNavigationDrawerContent( // TODO - remove obsolete stuff .fillMaxWidth(), contentAlignment = Alignment.Center ) { - Slider( + CustomSlider( modifier = Modifier.fillMaxWidth(), state = SliderState(1f), colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.surfaceBright) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8a7bc6..098943b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Presets Recorded Drums + Test // to modify further Profile diff --git a/build.gradle b/build.gradle index 5dc8e32..19b84af 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.5.2' apply false - id 'com.android.library' version '8.5.2' apply false - id 'org.jetbrains.kotlin.android' version '2.0.0' apply false - id 'org.jetbrains.kotlin.plugin.compose' version '2.0.0' apply false + id 'com.android.application' version '8.7.1' apply false + id 'com.android.library' version '8.7.1' apply false + id 'org.jetbrains.kotlin.android' version '2.0.20' apply false + id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' apply false } //apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index a03a504..c5f5214 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,4 +28,4 @@ org.gradle.parallel=true # Enable R8 full mode. android.enableR8.fullMode=true -org.gradle.unsafe.configuration-cache=true \ No newline at end of file +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ce8299..0985fc1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun May 21 00:35:11 EEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 09bfce1..dc5bbff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,13 +12,13 @@ String snapshotVersion = System.getenv("COMPOSE_SNAPSHOT_ID") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - snapshotVersion?.let { +/* snapshotVersion?.let { println("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } - } + }*/ // - maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } + //maven { url = uri("https://androidx.dev/snapshots/builds/$it/artifacts/repository/") } google() mavenCentral()