Browse Source

SplitPane improvements and fixes. (#2886)

* SplitPane improvements and fixes.

- Allow SplitPane to receive exact incoming size constraints
- Correctly abide by constraint on minimum size for "second part"
- Simplify layout code
pull/2920/head
Alexander Maryanovsky 2 years ago committed by GitHub
parent
commit
36616f877c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt
  2. 4
      components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt
  3. 2
      components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt
  4. 93
      components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt

14
components/SplitPane/demo/src/jvmMain/kotlin/org/jetbrains/compose/splitpane/demo/Main.kt

@ -1,13 +1,9 @@
package org.jetbrains.compose.splitpane.demo
import androidx.compose.desktop.DesktopTheme
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.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
@ -15,13 +11,9 @@ import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.VerticalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState
import org.jetbrains.compose.splitpane.*
import java.awt.Cursor
@OptIn(ExperimentalComposeUiApi::class)
private fun Modifier.cursorForHorizontalResize(): Modifier =
pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))

4
components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt

@ -4,7 +4,6 @@ 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
@ -86,7 +85,7 @@ internal class HandleScopeImpl(
) : HandleScope {
override fun Modifier.markAsHandle(): Modifier = this.pointerInput(containerScope.splitPaneState) {
detectDragGestures { change, _ ->
change.consumeAllChanges()
change.consume()
containerScope.splitPaneState.dispatchRawMovement(
if (containerScope.isHorizontal) change.position.x else change.position.y
)
@ -171,7 +170,6 @@ internal class SplitPaneScopeImpl(
*
* @param initialPositionPercentage the initial value for [SplitPaneState.positionPercentage]
* @param moveEnabled the initial value for [SplitPaneState.moveEnabled]
* @param interactionState the initial value for [SplitPaneState.interactionState]
* */
@ExperimentalSplitPaneApi
@Composable

2
components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneState.kt

@ -24,7 +24,7 @@ class SplitPaneState(
val movableArea = maxPosition - minPosition
if (movableArea > 0) {
positionPercentage =
((movableArea * positionPercentage) + delta).coerceIn(minPosition, maxPosition) / movableArea
((movableArea * positionPercentage) + delta).coerceIn(0f, movableArea) / movableArea
}
}

93
components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt

@ -9,8 +9,10 @@ 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
private fun Constraints.withUnconstrainedWidth() = copy(minWidth = 0, maxWidth = Constraints.Infinity)
private fun Constraints.withUnconstrainedHeight() = copy(minHeight = 0, maxHeight = Constraints.Infinity)
@OptIn(ExperimentalSplitPaneApi::class)
@Composable
@ -51,101 +53,84 @@ internal actual fun SplitPane(
val secondMinSizePx = secondPlaceableMinimalSize.value * density
with(splitPaneState) {
val constrainedMin = constraints.minByDirection(isHorizontal) + firstMinSizePx
val constrainedMax =
(constraints.maxByDirection(isHorizontal).toFloat() - secondMinSizePx).let {
if (it <= 0 || it <= constrainedMin) {
constraints.maxByDirection(isHorizontal).toFloat()
} else {
it
}
}
val firstMeasurable = measurables[0]
val splitterMeasurable = measurables[1]
val secondMeasurable = measurables[2]
val handleMeasurable = measurables[3]
if (minPosition != constrainedMin) {
maxPosition = constrainedMin
}
// Need the size of the splitter to determine the min/max position
// Constrain the splitter only on the "other" axis
val splitterConstraints =
if (isHorizontal)
constraints.withUnconstrainedWidth()
else
constraints.withUnconstrainedHeight()
val splitterPlaceable = splitterMeasurable.measure(splitterConstraints)
val splitterSize = splitterPlaceable.valueByDirection(isHorizontal)
if (maxPosition != constrainedMax) {
maxPosition =
if (firstMinSizePx + secondMinSizePx < constraints.maxByDirection(isHorizontal)) {
constrainedMax
} else {
minPosition
}
}
@Suppress("UnnecessaryVariable")
val constrainedMin = firstMinSizePx
val maxConstraintOnMainAxis = constraints.maxByDirection(isHorizontal)
val constrainedMax = (maxConstraintOnMainAxis - secondMinSizePx - splitterSize)
.coerceAtLeast(constrainedMin)
val constrainedPosition =
(constraints.maxByDirection(isHorizontal) - (firstMinSizePx + secondMinSizePx)).let {
if (it > 0f) {
(it * positionPercentage).coerceIn(constrainedMin, constrainedMax).roundToInt()
} else {
constrainedMin.roundToInt()
}
}
minPosition = constrainedMin
maxPosition = constrainedMax
val position = (constrainedMin * (1-positionPercentage) + constrainedMax * positionPercentage)
.roundToInt()
val firstPlaceable = measurables[0].measure(
val firstPlaceable = firstMeasurable.measure(
if (isHorizontal) {
constraints.copy(
minWidth = 0,
maxWidth = constrainedPosition
maxWidth = position
)
} else {
constraints.copy(
minHeight = 0,
maxHeight = constrainedPosition
maxHeight = position
)
}
)
val splitterPlaceable = measurables[1].measure(constraints)
val splitterSize = splitterPlaceable.valueByDirection(isHorizontal)
val secondPlaceablePosition = constrainedPosition + splitterSize
val secondPlaceableSize =
(constraints.maxByDirection(isHorizontal) - secondPlaceablePosition).coerceIn(
0,
if (secondPlaceablePosition < constraints.maxByDirection(isHorizontal)) {
constraints.maxByDirection(isHorizontal) - secondPlaceablePosition
} else {
constraints.maxByDirection(isHorizontal)
}
)
val secondPlaceablePosition = position + splitterSize
val secondAvailableSize = (maxConstraintOnMainAxis - secondPlaceablePosition).coerceAtLeast(0)
val secondPlaceable = measurables[2].measure(
val secondPlaceable = secondMeasurable.measure(
if (isHorizontal) {
constraints.copy(
minWidth = 0,
maxWidth = secondPlaceableSize
maxWidth = secondAvailableSize
)
} else {
constraints.copy(
minHeight = 0,
maxHeight = secondPlaceableSize
maxHeight = secondAvailableSize
)
}
)
val handlePlaceable = measurables[3].measure(constraints)
val handlePlaceable = handleMeasurable.measure(splitterConstraints)
val handleSize = handlePlaceable.valueByDirection(isHorizontal)
// TODO support RTL
val handlePosition = when (splitter.alignment) {
SplitterHandleAlignment.BEFORE -> constrainedPosition + splitterSize - handleSize
SplitterHandleAlignment.ABOVE -> constrainedPosition + (splitterSize - handleSize) / 2
SplitterHandleAlignment.AFTER -> constrainedPosition
SplitterHandleAlignment.BEFORE -> position + splitterSize - handleSize
SplitterHandleAlignment.ABOVE -> position + (splitterSize - handleSize) / 2
SplitterHandleAlignment.AFTER -> position
}
layout(constraints.maxWidth, constraints.maxHeight) {
firstPlaceable.place(0, 0)
if (isHorizontal) {
secondPlaceable.place(secondPlaceablePosition, 0)
splitterPlaceable.place(constrainedPosition, 0)
splitterPlaceable.place(position, 0)
if (moveEnabled) {
handlePlaceable.place(handlePosition, 0)
}
} else {
secondPlaceable.place(0, secondPlaceablePosition)
splitterPlaceable.place(0, constrainedPosition)
splitterPlaceable.place(0, position)
if (moveEnabled) {
handlePlaceable.place(0, handlePosition)
}

Loading…
Cancel
Save