mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
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
This commit is contained in:
committed by
GitHub
parent
35f0f72253
commit
36616f877c
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
if (maxPosition != constrainedMax) {
|
||||
maxPosition =
|
||||
if (firstMinSizePx + secondMinSizePx < constraints.maxByDirection(isHorizontal)) {
|
||||
constrainedMax
|
||||
} else {
|
||||
minPosition
|
||||
}
|
||||
}
|
||||
|
||||
val constrainedPosition =
|
||||
(constraints.maxByDirection(isHorizontal) - (firstMinSizePx + secondMinSizePx)).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)
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
)
|
||||
@Suppress("UnnecessaryVariable")
|
||||
val constrainedMin = firstMinSizePx
|
||||
val maxConstraintOnMainAxis = constraints.maxByDirection(isHorizontal)
|
||||
val constrainedMax = (maxConstraintOnMainAxis - secondMinSizePx - splitterSize)
|
||||
.coerceAtLeast(constrainedMin)
|
||||
|
||||
val secondPlaceable = measurables[2].measure(
|
||||
minPosition = constrainedMin
|
||||
maxPosition = constrainedMax
|
||||
|
||||
val position = (constrainedMin * (1-positionPercentage) + constrainedMax * positionPercentage)
|
||||
.roundToInt()
|
||||
|
||||
val firstPlaceable = firstMeasurable.measure(
|
||||
if (isHorizontal) {
|
||||
constraints.copy(
|
||||
minWidth = 0,
|
||||
maxWidth = secondPlaceableSize
|
||||
maxWidth = position
|
||||
)
|
||||
} else {
|
||||
constraints.copy(
|
||||
minHeight = 0,
|
||||
maxHeight = secondPlaceableSize
|
||||
maxHeight = position
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val handlePlaceable = measurables[3].measure(constraints)
|
||||
val secondPlaceablePosition = position + splitterSize
|
||||
val secondAvailableSize = (maxConstraintOnMainAxis - secondPlaceablePosition).coerceAtLeast(0)
|
||||
|
||||
val secondPlaceable = secondMeasurable.measure(
|
||||
if (isHorizontal) {
|
||||
constraints.copy(
|
||||
minWidth = 0,
|
||||
maxWidth = secondAvailableSize
|
||||
)
|
||||
} else {
|
||||
constraints.copy(
|
||||
minHeight = 0,
|
||||
maxHeight = secondAvailableSize
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user