From 901406269a9c01480c14bc4b7099a9d83d250146 Mon Sep 17 00:00:00 2001 From: LAP2 Date: Sun, 21 Feb 2021 00:53:50 +0400 Subject: [PATCH] Add split pane (#355) Components. SplitPane --- README.md | 1 + components/SplitPane/build.gradle.kts | 21 ++ components/SplitPane/common/build.gradle.kts | 21 ++ .../jetbrains/compose/splitpane/SplitPane.kt | 113 +++++++++++ .../compose/splitpane/SplitPaneDSL.kt | 184 ++++++++++++++++++ .../compose/splitpane/SplitPaneState.kt | 45 +++++ .../jetbrains/compose/splitpane/Splitter.kt | 15 ++ .../compose/splitpane/DesktopSplitPane.kt | 140 +++++++++++++ .../compose/splitpane/DesktopSplitter.kt | 99 ++++++++++ components/SplitPane/desktop/build.gradle.kts | 24 +++ .../jetbrains/compose/splitpane/demo/Main.kt | 95 +++++++++ components/build.gradle.kts | 4 +- components/settings.gradle.kts | 2 + 13 files changed, 762 insertions(+), 2 deletions(-) create mode 100644 components/SplitPane/build.gradle.kts create mode 100644 components/SplitPane/common/build.gradle.kts create mode 100644 components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt create mode 100644 components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt create mode 100644 components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt create mode 100644 components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt create mode 100644 components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt create mode 100644 components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt create mode 100644 components/SplitPane/desktop/build.gradle.kts create mode 100644 components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt diff --git a/README.md b/README.md index 8cb0ed8198..9aa01bf57e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ at https://android.googlesource.com/platform/frameworks/support. * [Navigation](tutorials/Navigation) * [components](components) - custom components of Compose for Desktop * [Video Player](components/VideoPlayer) + * [Split Pane](components/SplitPane) ## Getting latest version of Compose for Desktop ## diff --git a/components/SplitPane/build.gradle.kts b/components/SplitPane/build.gradle.kts new file mode 100644 index 0000000000..b309717036 --- /dev/null +++ b/components/SplitPane/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm { + withJava() + } + sourceSets { + named("jvmMain") { + dependencies { + implementation(compose.desktop.currentOs) + implementation(project("common")) + } + } + } +} + diff --git a/components/SplitPane/common/build.gradle.kts b/components/SplitPane/common/build.gradle.kts new file mode 100644 index 0000000000..1fc0cf5571 --- /dev/null +++ b/components/SplitPane/common/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm("desktop") + + sourceSets { + named("commonMain") { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + } + } + named("desktopMain") {} + } +} \ 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 new file mode 100644 index 0000000000..fc6d3a77cb --- /dev/null +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPane.kt @@ -0,0 +1,113 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal data class MinimalSizes( + val firstPlaceableMinimalSize: Dp, + val secondPlaceableMinimalSize: Dp +) + +/** + * Pane that place it parts **vertically** from top to bottom and allows to change items **heights**. + * The [content] block defines DSL which allow you to configure top ([SplitPaneScope.first]), + * bottom ([SplitPaneScope.second]). + * + * @param modifier the modifier to apply to this layout + * @param splitPaneState the state object to be used to control or observe the split pane state + * @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. + */ +@Composable +fun VerticalSplitPane( + modifier: Modifier = Modifier, + splitPaneState: SplitPaneState = rememberSplitPaneState(), + content: SplitPaneScope.() -> Unit +) { + with(SplitPaneScopeImpl(isHorizontal = false, splitPaneState).apply(content)) { + if (firstPlaceableContent != null && secondPlaceableContent != null) { + SplitPane( + modifier = modifier, + isHorizontal = false, + splitPaneState = splitPaneState, + minimalSizesConfiguration = minimalSizes, + first = firstPlaceableContent!!, + second = secondPlaceableContent!!, + splitter = splitter + ) + } else { + firstPlaceableContent?.invoke() + secondPlaceableContent?.invoke() + } + } +} + +/** + * Pane that place it parts **horizontally** from left to right and allows to change items **width**. + * The [content] block defines DSL which allow you to configure left ([SplitPaneScope.first]), + * right ([SplitPaneScope.second]) parts of split pane. + * + * @param modifier the modifier to apply to this layout + * @param splitPaneState the state object to be used to control or observe the split pane state + * @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. + */ +@Composable +fun HorizontalSplitPane( + modifier: Modifier = Modifier, + splitPaneState: SplitPaneState = rememberSplitPaneState(), + content: SplitPaneScope.() -> Unit +) { + with(SplitPaneScopeImpl(isHorizontal = true, splitPaneState).apply(content)) { + if (firstPlaceableContent != null && secondPlaceableContent != null) { + SplitPane( + modifier = modifier, + isHorizontal = true, + splitPaneState = splitPaneState, + minimalSizesConfiguration = minimalSizes, + first = firstPlaceableContent!!, + second = secondPlaceableContent!!, + splitter = splitter + ) + } else { + firstPlaceableContent?.invoke() + secondPlaceableContent?.invoke() + } + } + +} + +/** + * Internal implementation of default splitter + * + * @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 + */ +internal expect fun defaultSplitter( + isHorizontal: Boolean, + splitPaneState: SplitPaneState +): Splitter + +/** + * Internal implementation of split pane that used in all public composable functions + * + * @param modifier the modifier to apply to this layout + * @param isHorizontal describes is it horizontal of vertical split pane + * @param splitPaneState the state object to be used to control or observe the split pane state + * @param minimalSizesConfiguration data class ([MinimalSizes]) that provides minimal size for split pane parts + * @param first first part of split pane, left or top according to [isHorizontal] + * @param second second part of split pane, right or bottom according to [isHorizontal] + * @param splitter separator composable, by default [Splitter] is used + * */ +@Composable +internal expect fun SplitPane( + modifier: Modifier = Modifier, + isHorizontal: Boolean = true, + splitPaneState: SplitPaneState, + minimalSizesConfiguration: MinimalSizes = MinimalSizes(0.dp, 0.dp), + first: @Composable () -> Unit, + second: @Composable () -> Unit, + splitter: Splitter +) \ No newline at end of file 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 new file mode 100644 index 0000000000..e5a8501b43 --- /dev/null +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt @@ -0,0 +1,184 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.foundation.InteractionState +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.consumeAllChanges +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** Receiver scope which is used by [HorizontalSplitPane] and [VerticalSplitPane] */ +interface SplitPaneScope { + + /** + * Set up first composable item if SplitPane, for [HorizontalSplitPane] it will be + * Right part, for [VerticalSplitPane] it will be Top part + * @param minSize a minimal size of composable item. + * For [HorizontalSplitPane] it will be minimal width, for [VerticalSplitPane] it wil be minimal Heights. + * In this context minimal mean that this composable item could not be smaller than specified value. + * @param content composable item content. + * */ + fun first( + minSize: Dp = 0.dp, + content: @Composable () -> Unit + ) + + /** + * Set up second composable item if SplitPane. + * For [HorizontalSplitPane] it will be Left part, for [VerticalSplitPane] it will be Bottom part + * @param minSize a minimal size of composable item. + * For [HorizontalSplitPane] it will be minimal width, for [VerticalSplitPane] it wil be minimal Heights. + * In this context minimal mean that this composable item could not be smaller than specified value. + * @param content composable item content. + * */ + fun second( + minSize: Dp = 0.dp, + content: @Composable () -> Unit + ) + + fun splitter(block: SplitterScope.() -> Unit) + +} + +/** Receiver scope which is used by [SplitterScope] */ +interface HandleScope { + /** allow mark composable as movable handle */ + fun Modifier.markAsHandle(): Modifier +} + +/** Receiver scope which is used by [SplitPaneScope] */ +interface SplitterScope { + /** + * Set up visible part of splitter. This part will be measured and placed between split pane + * parts (first and second) + * + * @param content composable item content + * */ + fun visiblePart(content: @Composable () -> Unit) + + /** + * Set up handle part, this part of splitter would be measured and placed above [visiblePart] content. + * Size of handle will have no effect on split pane parts (first and second) sizes. + * + * @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) + * * and [SplitterHandleAlign.AFTER] if you place handle after [visiblePart]. + * + * @param content composable item content provider. Uses [HandleScope] to allow mark any provided composable part + * as handle. + * [content] will be placed only if [SplitPaneState.moveEnabled] is true + */ + fun handle( + alignment: SplitterHandleAlign = SplitterHandleAlign.ABOVE, + content: HandleScope.() -> @Composable () -> Unit + ) +} + +internal class HandleScopeImpl( + private val containerScope: SplitPaneScopeImpl +) : HandleScope { + override fun Modifier.markAsHandle(): Modifier = this.pointerInput(containerScope.splitPaneState) { + detectDragGestures { change, _ -> + change.consumeAllChanges() + containerScope.splitPaneState.dispatchRawMovement( + if (containerScope.isHorizontal) change.position.x else change.position.y + ) + } + } +} + +internal class SplitterScopeImpl( + private val containerScope: SplitPaneScopeImpl +) : SplitterScope { + + override fun visiblePart(content: @Composable () -> Unit) { + containerScope.visiblePart = content + } + + override fun handle( + alignment: SplitterHandleAlign, + content: HandleScope.() -> @Composable () -> Unit + ) { + containerScope.handle = HandleScopeImpl(containerScope).content() + containerScope.alignment = alignment + } +} + +private typealias ComposableSlot = @Composable () -> Unit + +internal class SplitPaneScopeImpl( + internal val isHorizontal: Boolean, + internal val splitPaneState: SplitPaneState +) : SplitPaneScope { + + private var firstPlaceableMinimalSize: Dp = 0.dp + private var secondPlaceableMinimalSize: Dp = 0.dp + + internal val minimalSizes: MinimalSizes + get() = MinimalSizes(firstPlaceableMinimalSize, secondPlaceableMinimalSize) + + internal var firstPlaceableContent: ComposableSlot? = null + private set + internal var secondPlaceableContent: ComposableSlot? = null + private set + + internal lateinit var visiblePart: ComposableSlot + internal lateinit var handle: ComposableSlot + internal var alignment: SplitterHandleAlign = SplitterHandleAlign.ABOVE + internal val splitter + get() = + if (this::visiblePart.isInitialized && this::handle.isInitialized) { + Splitter(visiblePart, handle, alignment) + } else { + defaultSplitter(isHorizontal, splitPaneState) + } + + override fun first( + minSize: Dp, + content: @Composable () -> Unit + ) { + firstPlaceableMinimalSize = minSize + firstPlaceableContent = content + } + + override fun second( + minSize: Dp, + content: @Composable () -> Unit + ) { + secondPlaceableMinimalSize = minSize + secondPlaceableContent = content + } + + override fun splitter(block: SplitterScope.() -> Unit) { + SplitterScopeImpl(this).block() + } +} + +/** + * creates a [SplitPaneState] and remembers it across composition + * + * Changes to the provided initial values will **not** result in the state being recreated or + * changed in any way if it has already been created. + * + * @param initialPositionPercentage the initial value for [SplitPaneState.positionPercentage] + * @param moveEnabled the initial value for [SplitPaneState.moveEnabled] + * @param interactionState the initial value for [SplitPaneState.interactionState] + * */ +@Composable +fun rememberSplitPaneState( + initialPositionPercentage: Float = 0f, + moveEnabled: Boolean = true, + interactionState: InteractionState = remember { InteractionState() } +): SplitPaneState { + return remember { + SplitPaneState( + moveEnabled = moveEnabled, + initialPositionPercentage = initialPositionPercentage, + interactionState = interactionState + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..fe2b4b7b80 --- /dev/null +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt @@ -0,0 +1,45 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.foundation.Interaction +import androidx.compose.foundation.InteractionState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.structuralEqualityPolicy + + +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 + } + + private val _positionPercentage = mutableStateOf(initialPositionPercentage, structuralEqualityPolicy()) + + var positionPercentage: Float + get() = _positionPercentage.value + internal set(newPosition) { + _positionPercentage.value = newPosition + } + + internal var minPosition: Float = 0f + + internal var maxPosition: Float = Float.POSITIVE_INFINITY + + fun dispatchRawMovement(delta: Float) { + interactionState.addInteraction(Interaction.Dragged) + val movableArea = maxPosition - minPosition + if (movableArea > 0) { + positionPercentage = + ((movableArea * positionPercentage) + delta).coerceIn(minPosition, maxPosition) / movableArea + } + interactionState.removeInteraction(Interaction.Dragged) + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cb34c94277 --- /dev/null +++ b/components/SplitPane/common/src/commonMain/kotlin/org/jetbrains/compose/splitpane/Splitter.kt @@ -0,0 +1,15 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.runtime.Composable + +enum class SplitterHandleAlign { + BEFORE, + ABOVE, + AFTER +} + +internal data class Splitter( + val measuredPart: @Composable () -> Unit, + val handlePart: @Composable () -> Unit = measuredPart, + val align: SplitterHandleAlign = SplitterHandleAlign.ABOVE +) 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 new file mode 100644 index 0000000000..13738b2805 --- /dev/null +++ b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt @@ -0,0 +1,140 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Constraints +import kotlin.math.roundToInt + +private fun Constraints.maxByDirection(isHorizontal: Boolean): Int = if (isHorizontal) maxWidth else maxHeight +private fun Constraints.minByDirection(isHorizontal: Boolean): Int = if (isHorizontal) minWidth else minHeight +private fun Placeable.valueByDirection(isHorizontal: Boolean): Int = if (isHorizontal) width else height + +@Composable +internal actual fun SplitPane( + modifier: Modifier, + isHorizontal: Boolean, + splitPaneState: SplitPaneState, + minimalSizesConfiguration: MinimalSizes, + first: @Composable () -> Unit, + second: @Composable () -> Unit, + splitter: Splitter +) { + Layout( + { + first() + splitter.measuredPart() + second() + splitter.handlePart() + }, + modifier, + ) { measurables, constraints -> + with(minimalSizesConfiguration) { + with(splitPaneState) { + + val constrainedMin = constraints.minByDirection(isHorizontal) + firstPlaceableMinimalSize.value + + val constrainedMax = + (constraints.maxByDirection(isHorizontal).toFloat() - secondPlaceableMinimalSize.value).let { + if (it <= 0 || it <= constrainedMin) { + constraints.maxByDirection(isHorizontal).toFloat() + } else { + it + } + } + + if (minPosition != constrainedMin) { + maxPosition = constrainedMin + } + + if (maxPosition != constrainedMax) { + maxPosition = + if ((firstPlaceableMinimalSize + secondPlaceableMinimalSize).value < constraints.maxByDirection( + isHorizontal + ) + ) { + constrainedMax + } else { + minPosition + } + } + + val constrainedPosition = + (constraints.maxByDirection(isHorizontal) - (firstPlaceableMinimalSize + secondPlaceableMinimalSize).value).let { + if (it > 0f) { + (it * positionPercentage).coerceIn(constrainedMin, constrainedMax).roundToInt() + } else { + constrainedMin.roundToInt() + } + } + + + val firstPlaceable = measurables[0].measure( + if (isHorizontal) { + constraints.copy( + minWidth = 0, + maxWidth = constrainedPosition + ) + } else { + constraints.copy( + minHeight = 0, + maxHeight = constrainedPosition + ) + } + ) + + val splitterPlaceable = measurables[1].measure(constraints) + val secondPlaceablePosition = constrainedPosition + splitterPlaceable.valueByDirection(isHorizontal) + + val secondPlaceableSize = + (constraints.maxByDirection(isHorizontal) - secondPlaceablePosition).coerceIn( + 0, + if (secondPlaceablePosition < constraints.maxByDirection(isHorizontal)) { + constraints.maxByDirection(isHorizontal) - secondPlaceablePosition + } else { + constraints.maxByDirection(isHorizontal) + } + ) + + val secondPlaceable = measurables[2].measure( + if (isHorizontal) { + constraints.copy( + minWidth = 0, + maxWidth = secondPlaceableSize + ) + } else { + constraints.copy( + minHeight = 0, + maxHeight = secondPlaceableSize + ) + } + ) + + val handlePlaceable = measurables[3].measure(constraints) + val handlePosition = when (splitter.align) { + SplitterHandleAlign.BEFORE -> constrainedPosition - handlePlaceable.valueByDirection(isHorizontal) + SplitterHandleAlign.ABOVE -> constrainedPosition - (handlePlaceable.valueByDirection(isHorizontal) / 2) + SplitterHandleAlign.AFTER -> constrainedPosition + handlePlaceable.valueByDirection(isHorizontal) + } + + layout(constraints.maxWidth, constraints.maxHeight) { + firstPlaceable.place(0, 0) + if (isHorizontal) { + secondPlaceable.place(secondPlaceablePosition, 0) + splitterPlaceable.place(constrainedPosition, 0) + if (moveEnabled) { + handlePlaceable.place(handlePosition, 0) + } + } else { + secondPlaceable.place(0, secondPlaceablePosition) + splitterPlaceable.place(0, constrainedPosition) + if (moveEnabled) { + handlePlaceable.place(0, handlePosition) + } + } + } + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..0739ecd9f8 --- /dev/null +++ b/components/SplitPane/common/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt @@ -0,0 +1,99 @@ +package org.jetbrains.compose.splitpane + +import androidx.compose.desktop.LocalAppWindow +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.consumeAllChanges +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerMoveFilter +import androidx.compose.ui.unit.dp +import java.awt.Cursor + +private fun Modifier.cursorForHorizontalResize( + isHorizontal: Boolean +): Modifier = composed { + var isHover by remember { mutableStateOf(false) } + + if (isHover) { + LocalAppWindow.current.window.cursor = Cursor( + if (isHorizontal) Cursor.E_RESIZE_CURSOR else Cursor.S_RESIZE_CURSOR + ) + } else { + LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() + } + pointerMoveFilter( + onEnter = { isHover = true; true }, + onExit = { isHover = false; true } + ) +} + +@Composable +private fun DesktopSplitPaneSeparator( + isHorizontal: Boolean, + color: Color = MaterialTheme.colors.background +) = Box( + Modifier + .run { + if (isHorizontal) { + this.width(1.dp) + .fillMaxHeight() + } else { + this.height(1.dp) + .fillMaxWidth() + } + } + .background(color) +) + +@Composable +private fun DesctopHandle( + isHorizontal: Boolean, + splitPaneState: SplitPaneState +) = Box( + Modifier + .pointerInput(splitPaneState) { + detectDragGestures { change, _ -> + change.consumeAllChanges() + splitPaneState.dispatchRawMovement( + if (isHorizontal) change.position.x else change.position.y + ) + } + } + .cursorForHorizontalResize(isHorizontal) + .run { + if (isHorizontal) { + this.width(8.dp) + .fillMaxHeight() + } else { + this.height(8.dp) + .fillMaxWidth() + } + } +) + +internal actual fun defaultSplitter( + isHorizontal: Boolean, + splitPaneState: SplitPaneState +): Splitter = Splitter( + measuredPart = { + DesktopSplitPaneSeparator(isHorizontal) + }, + handlePart = { + DesctopHandle(isHorizontal, splitPaneState) + } +) + diff --git a/components/SplitPane/desktop/build.gradle.kts b/components/SplitPane/desktop/build.gradle.kts new file mode 100644 index 0000000000..7df9e72c06 --- /dev/null +++ b/components/SplitPane/desktop/build.gradle.kts @@ -0,0 +1,24 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm {} + sourceSets { + named("jvmMain") { + dependencies { + implementation(compose.desktop.currentOs) + implementation(project(":SplitPane:common")) + } + } + } +} + +compose.desktop { + application { + mainClass = "org.jetbrains.compose.splitpane.demo.MainKt" + } +} \ 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 new file mode 100644 index 0000000000..a0fd4c6af2 --- /dev/null +++ b/components/SplitPane/desktop/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt @@ -0,0 +1,95 @@ +package org.jetbrains.compose.splitpane.demo + +import androidx.compose.desktop.DesktopTheme +import androidx.compose.desktop.LocalAppWindow +import androidx.compose.desktop.Window +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +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.HorizontalSplitPane +import org.jetbrains.compose.splitpane.VerticalSplitPane +import org.jetbrains.compose.splitpane.rememberSplitPaneState +import java.awt.Cursor + +private fun Modifier.cursorForHorizontalResize( +): Modifier = composed { + var isHover by remember { mutableStateOf(false) } + + if (isHover) { + LocalAppWindow.current.window.cursor = Cursor(Cursor.E_RESIZE_CURSOR) + } else { + LocalAppWindow.current.window.cursor = Cursor.getDefaultCursor() + } + + pointerMoveFilter( + onEnter = { isHover = true; true }, + onExit = { isHover = false; true } + ) +} + +fun main() = Window( + "SplitPane demo" +) { + MaterialTheme { + DesktopTheme { + val splitterState = rememberSplitPaneState() + val hSplitterState = rememberSplitPaneState() + HorizontalSplitPane( + splitPaneState = splitterState + ) { + first(20.dp) { + Box(Modifier.background(Color.Red).fillMaxSize()) + } + second(50.dp) { + VerticalSplitPane(splitPaneState = hSplitterState) { + first(50.dp) { + Box(Modifier.background(Color.Blue).fillMaxSize()) + } + second(20.dp) { + Box(Modifier.background(Color.Green).fillMaxSize()) + } + } + } + splitter { + visiblePart { + Box( + Modifier + .width(1.dp) + .fillMaxHeight() + .background(MaterialTheme.colors.background) + ) + } + handle { + { + Box( + Modifier + .markAsHandle() + .cursorForHorizontalResize() + .background(SolidColor(Color.Gray), alpha = 0.50f) + .width(8.dp) + .fillMaxHeight() + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/components/build.gradle.kts b/components/build.gradle.kts index 2c4c10bc87..e667db4be6 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -1,6 +1,6 @@ buildscript { // __LATEST_COMPOSE_RELEASE_VERSION__ - val composeVersion = System.getenv("COMPOSE_RELEASE_VERSION") ?: "0.3.0-build135" + val composeVersion = System.getenv("COMPOSE_RELEASE_VERSION") ?: "0.3.0-build150" repositories { google() @@ -11,7 +11,7 @@ buildscript { dependencies { classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion") // __KOTLIN_COMPOSE_VERSION__ - classpath(kotlin("gradle-plugin", version = "1.4.21")) + classpath(kotlin("gradle-plugin", version = "1.4.30")) } } diff --git a/components/settings.gradle.kts b/components/settings.gradle.kts index cb16b44ff6..473b5f6a63 100644 --- a/components/settings.gradle.kts +++ b/components/settings.gradle.kts @@ -1,2 +1,4 @@ include(":VideoPlayer:common") include(":VideoPlayer:desktop") +include(":SplitPane:common") +include(":SplitPane:desktop") \ No newline at end of file