From 1b634255e861790a67a6801c77b025a342b3432f Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Sun, 21 Feb 2021 00:03:04 +0300 Subject: [PATCH] Split pane. Refactoring (#408) --- components/.gitignore | 15 ++++++++++++ components/SplitPane/common/build.gradle.kts | 6 +++++ .../splitpane/ExperimentalSplitPaneApi.kt | 6 +++++ .../jetbrains/compose/splitpane/SplitPane.kt | 3 +++ .../compose/splitpane/SplitPaneDSL.kt | 15 ++++++++---- .../compose/splitpane/SplitPaneState.kt | 23 ++++++------------ .../jetbrains/compose/splitpane/Splitter.kt | 2 ++ .../compose/splitpane/DesktopSplitPane.kt | 17 ++++++------- .../compose/splitpane/DesktopSplitter.kt | 6 +++-- components/SplitPane/desktop/build.gradle.kts | 5 ++++ .../jetbrains/compose/splitpane/demo/Main.kt | 24 ++++++++----------- 11 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 components/.gitignore create mode 100644 components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/ExperimentalSplitPaneApi.kt diff --git a/components/.gitignore b/components/.gitignore new file mode 100644 index 0000000000..ba8435b9c5 --- /dev/null +++ b/components/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build/ +/captures +.externalNativeBuild +.cxx \ No newline at end of file diff --git a/components/SplitPane/common/build.gradle.kts b/components/SplitPane/common/build.gradle.kts index 1fc0cf5571..d9a373bd93 100644 --- a/components/SplitPane/common/build.gradle.kts +++ b/components/SplitPane/common/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.compose +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("multiplatform") @@ -18,4 +19,9 @@ kotlin { } named("desktopMain") {} } +} + +// TODO it seems that argument isn't applied to the common sourceSet. Figure out why +tasks.withType().configureEach { + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } \ No newline at end of file diff --git a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/ExperimentalSplitPaneApi.kt b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/ExperimentalSplitPaneApi.kt new file mode 100644 index 0000000000..dc20d314bf --- /dev/null +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/ExperimentalSplitPaneApi.kt @@ -0,0 +1,6 @@ +package org.jetbrains.compose.splitpane + +@MustBeDocumented +@Retention(value = AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +annotation class ExperimentalSplitPaneApi \ No newline at end of file diff --git a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt index fc6d3a77cb..cdbce9d31b 100644 --- a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt @@ -20,6 +20,7 @@ internal data class MinimalSizes( * @param content a block which describes the content. Inside this block you can use methods like * [SplitPaneScope.first], [SplitPaneScope.second], to describe parts of split pane. */ +@ExperimentalSplitPaneApi @Composable fun VerticalSplitPane( modifier: Modifier = Modifier, @@ -54,6 +55,7 @@ fun VerticalSplitPane( * @param content a block which describes the content. Inside this block you can use methods like * [SplitPaneScope.first], [SplitPaneScope.second], to describe parts of split pane. */ +@ExperimentalSplitPaneApi @Composable fun HorizontalSplitPane( modifier: Modifier = Modifier, @@ -85,6 +87,7 @@ fun HorizontalSplitPane( * @param isHorizontal describes is it horizontal or vertical split pane * @param splitPaneState the state object to be used to control or observe the split pane state */ +@OptIn(ExperimentalSplitPaneApi::class) internal expect fun defaultSplitter( isHorizontal: Boolean, splitPaneState: SplitPaneState diff --git a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt index e5a8501b43..f5a1d98fd0 100644 --- a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** Receiver scope which is used by [HorizontalSplitPane] and [VerticalSplitPane] */ +@ExperimentalSplitPaneApi interface SplitPaneScope { /** @@ -44,12 +45,14 @@ interface SplitPaneScope { } /** Receiver scope which is used by [SplitterScope] */ +@ExperimentalSplitPaneApi interface HandleScope { /** allow mark composable as movable handle */ fun Modifier.markAsHandle(): Modifier } /** Receiver scope which is used by [SplitPaneScope] */ +@ExperimentalSplitPaneApi interface SplitterScope { /** * Set up visible part of splitter. This part will be measured and placed between split pane @@ -65,7 +68,7 @@ interface SplitterScope { * * @param alignment alignment of handle according to [visiblePart] could be: * * [SplitterHandleAlign.BEFORE] if you place handle before [visiblePart], - * * [SplitterHandleAlign.ABOVE] if you place handle above [visiblePart] (will be centred) + * * [SplitterHandleAlign.ABOVE] if you place handle above [visiblePart] (will be centered) * * and [SplitterHandleAlign.AFTER] if you place handle after [visiblePart]. * * @param content composable item content provider. Uses [HandleScope] to allow mark any provided composable part @@ -74,10 +77,11 @@ interface SplitterScope { */ fun handle( alignment: SplitterHandleAlign = SplitterHandleAlign.ABOVE, - content: HandleScope.() -> @Composable () -> Unit + content: @Composable HandleScope.() -> Unit ) } +@OptIn(ExperimentalSplitPaneApi::class) internal class HandleScopeImpl( private val containerScope: SplitPaneScopeImpl ) : HandleScope { @@ -91,6 +95,7 @@ internal class HandleScopeImpl( } } +@OptIn(ExperimentalSplitPaneApi::class) internal class SplitterScopeImpl( private val containerScope: SplitPaneScopeImpl ) : SplitterScope { @@ -101,15 +106,16 @@ internal class SplitterScopeImpl( override fun handle( alignment: SplitterHandleAlign, - content: HandleScope.() -> @Composable () -> Unit + content: @Composable HandleScope.() -> Unit ) { - containerScope.handle = HandleScopeImpl(containerScope).content() + containerScope.handle = { HandleScopeImpl(containerScope).content() } containerScope.alignment = alignment } } private typealias ComposableSlot = @Composable () -> Unit +@OptIn(ExperimentalSplitPaneApi::class) internal class SplitPaneScopeImpl( internal val isHorizontal: Boolean, internal val splitPaneState: SplitPaneState @@ -168,6 +174,7 @@ internal class SplitPaneScopeImpl( * @param moveEnabled the initial value for [SplitPaneState.moveEnabled] * @param interactionState the initial value for [SplitPaneState.interactionState] * */ +@ExperimentalSplitPaneApi @Composable fun rememberSplitPaneState( initialPositionPercentage: Float = 0f, diff --git a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt index fe2b4b7b80..772d91a982 100644 --- a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt @@ -2,31 +2,22 @@ package org.jetbrains.compose.splitpane import androidx.compose.foundation.Interaction import androidx.compose.foundation.InteractionState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.structuralEqualityPolicy - +import androidx.compose.runtime.setValue +@ExperimentalSplitPaneApi class SplitPaneState( initialPositionPercentage: Float, moveEnabled: Boolean, private val interactionState: InteractionState ) { - private var _moveEnabled = mutableStateOf(moveEnabled, structuralEqualityPolicy()) - - var moveEnabled: Boolean - get() = _moveEnabled.value - set(newValue) { - _moveEnabled.value = newValue - } + var moveEnabled by mutableStateOf(moveEnabled) + internal set - private val _positionPercentage = mutableStateOf(initialPositionPercentage, structuralEqualityPolicy()) - - var positionPercentage: Float - get() = _positionPercentage.value - internal set(newPosition) { - _positionPercentage.value = newPosition - } + var positionPercentage by mutableStateOf(initialPositionPercentage) + internal set internal var minPosition: Float = 0f diff --git a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt index cb34c94277..a16b69d94b 100644 --- a/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt @@ -2,12 +2,14 @@ package org.jetbrains.compose.splitpane import androidx.compose.runtime.Composable +@ExperimentalSplitPaneApi enum class SplitterHandleAlign { BEFORE, ABOVE, AFTER } +@OptIn(ExperimentalSplitPaneApi::class) internal data class Splitter( val measuredPart: @Composable () -> Unit, val handlePart: @Composable () -> Unit = measuredPart, diff --git a/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt index 13738b2805..ceaebcf3dc 100644 --- a/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt +++ b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt @@ -11,6 +11,7 @@ private fun Constraints.maxByDirection(isHorizontal: Boolean): Int = if (isHoriz private fun Constraints.minByDirection(isHorizontal: Boolean): Int = if (isHorizontal) minWidth else minHeight private fun Placeable.valueByDirection(isHorizontal: Boolean): Int = if (isHorizontal) width else height +@OptIn(ExperimentalSplitPaneApi::class) @Composable internal actual fun SplitPane( modifier: Modifier, @@ -50,10 +51,7 @@ internal actual fun SplitPane( if (maxPosition != constrainedMax) { maxPosition = - if ((firstPlaceableMinimalSize + secondPlaceableMinimalSize).value < constraints.maxByDirection( - isHorizontal - ) - ) { + if ((firstPlaceableMinimalSize + secondPlaceableMinimalSize).value < constraints.maxByDirection(isHorizontal)) { constrainedMax } else { minPosition @@ -85,7 +83,8 @@ internal actual fun SplitPane( ) val splitterPlaceable = measurables[1].measure(constraints) - val secondPlaceablePosition = constrainedPosition + splitterPlaceable.valueByDirection(isHorizontal) + val splitterSize = splitterPlaceable.valueByDirection(isHorizontal) + val secondPlaceablePosition = constrainedPosition + splitterSize val secondPlaceableSize = (constraints.maxByDirection(isHorizontal) - secondPlaceablePosition).coerceIn( @@ -112,10 +111,12 @@ internal actual fun SplitPane( ) val handlePlaceable = measurables[3].measure(constraints) + val handleSize = handlePlaceable.valueByDirection(isHorizontal) + // TODO support RTL val handlePosition = when (splitter.align) { - SplitterHandleAlign.BEFORE -> constrainedPosition - handlePlaceable.valueByDirection(isHorizontal) - SplitterHandleAlign.ABOVE -> constrainedPosition - (handlePlaceable.valueByDirection(isHorizontal) / 2) - SplitterHandleAlign.AFTER -> constrainedPosition + handlePlaceable.valueByDirection(isHorizontal) + SplitterHandleAlign.BEFORE -> constrainedPosition - handleSize + SplitterHandleAlign.ABOVE -> constrainedPosition + (splitterSize - handleSize) / 2 + SplitterHandleAlign.AFTER -> constrainedPosition + splitterSize + handleSize } layout(constraints.maxWidth, constraints.maxHeight) { diff --git a/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt index 0739ecd9f8..ac73a834d1 100644 --- a/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt +++ b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt @@ -59,8 +59,9 @@ private fun DesktopSplitPaneSeparator( .background(color) ) +@OptIn(ExperimentalSplitPaneApi::class) @Composable -private fun DesctopHandle( +private fun DesktopHandle( isHorizontal: Boolean, splitPaneState: SplitPaneState ) = Box( @@ -85,6 +86,7 @@ private fun DesctopHandle( } ) +@OptIn(ExperimentalSplitPaneApi::class) internal actual fun defaultSplitter( isHorizontal: Boolean, splitPaneState: SplitPaneState @@ -93,7 +95,7 @@ internal actual fun defaultSplitter( DesktopSplitPaneSeparator(isHorizontal) }, handlePart = { - DesctopHandle(isHorizontal, splitPaneState) + DesktopHandle(isHorizontal, splitPaneState) } ) diff --git a/components/SplitPane/desktop/build.gradle.kts b/components/SplitPane/desktop/build.gradle.kts index 7df9e72c06..565fbfc0d0 100644 --- a/components/SplitPane/desktop/build.gradle.kts +++ b/components/SplitPane/desktop/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.compose +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("multiplatform") @@ -21,4 +22,8 @@ compose.desktop { application { mainClass = "org.jetbrains.compose.splitpane.demo.MainKt" } +} + +tasks.withType().configureEach { + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } \ No newline at end of file diff --git a/components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt b/components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt index a0fd4c6af2..c5afd70c3c 100644 --- a/components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt +++ b/components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt @@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -20,9 +18,8 @@ import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.pointerMoveFilter -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.unit.dp +import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.VerticalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -44,6 +41,7 @@ private fun Modifier.cursorForHorizontalResize( ) } +@OptIn(ExperimentalSplitPaneApi::class) fun main() = Window( "SplitPane demo" ) { @@ -77,16 +75,14 @@ fun main() = Window( ) } handle { - { - Box( - Modifier - .markAsHandle() - .cursorForHorizontalResize() - .background(SolidColor(Color.Gray), alpha = 0.50f) - .width(8.dp) - .fillMaxHeight() - ) - } + Box( + Modifier + .markAsHandle() + .cursorForHorizontalResize() + .background(SolidColor(Color.Gray), alpha = 0.50f) + .width(9.dp) + .fillMaxHeight() + ) } } }