Skip to content

Commit

Permalink
Fix SplitLayout divider positioning error (#667)
Browse files Browse the repository at this point in the history
* add initial splitLayout fraction to ReleasesSampleCompose

Signed-off-by: Ivan Morgillo <[email protected]>

* create a basic SpitLayout example in IDE sample

Signed-off-by: Ivan Morgillo <[email protected]>

* force divider position to respect initialFraction

Signed-off-by: Ivan Morgillo <[email protected]>

* constrain minimum size for SplitLayout on window resize

Signed-off-by: Ivan Morgillo <[email protected]>

* make the formatter happy

* make the formatter happy

Signed-off-by: morgillo <[email protected]>

* fix #589

Signed-off-by: Ivan Morgillo <[email protected]>

* remove SplitLayoutSandbox

Signed-off-by: Ivan Morgillo <[email protected]>

* refactor roundToPx in SplitLayout

reference #667 (comment)

Signed-off-by: Ivan Morgillo <[email protected]>

* remove redundant constrain

reference #667 (comment)

Signed-off-by: Ivan Morgillo <[email protected]>

* make the formatter happy

Signed-off-by: morgillo <[email protected]>

* fix jumpy divider in SplitLayout

Signed-off-by: morgillo <[email protected]>

* fix coercion exception in SplitLayout draggableState

Signed-off-by: Ivan Morgillo <[email protected]>

* make formatter happy

* fix klint warning in SplitLayoutImpl

Signed-off-by: Ivan Morgillo <[email protected]>

* iterate on jumpy divider

Signed-off-by: Ivan Morgillo <[email protected]>

* make formatter happy

* Iterate on draggableState doc

---------

Signed-off-by: Ivan Morgillo <[email protected]>
Signed-off-by: morgillo <[email protected]>
Co-authored-by: morgillo <[email protected]>
  • Loading branch information
hamen and morgillo authored Nov 6, 2024
1 parent 25e5fdb commit bf911c3
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import org.jetbrains.jewel.ui.component.TextField
import org.jetbrains.jewel.ui.component.Typography
import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer
import org.jetbrains.jewel.ui.component.items
import org.jetbrains.jewel.ui.component.rememberSplitLayoutState
import org.jetbrains.jewel.ui.component.scrollbarContentSafePadding
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider
Expand All @@ -105,6 +106,7 @@ fun ReleasesSampleCompose(project: Project) {
modifier = Modifier.fillMaxSize(),
firstPaneMinWidth = 300.dp,
secondPaneMinWidth = 300.dp,
state = rememberSplitLayoutState(.3f),
)
}

Expand Down
113 changes: 77 additions & 36 deletions ui/src/main/kotlin/org/jetbrains/jewel/ui/component/SplitLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
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.setValue
Expand Down Expand Up @@ -175,18 +176,26 @@ private fun SplitLayoutImpl(
var isDragging by remember { mutableStateOf(false) }
val resizePointerIcon = if (strategy.isHorizontal()) HorizontalResizePointerIcon else VerticalResizePointerIcon

var dragOffset by remember { mutableStateOf(0f) }
var dragOffset by remember { mutableFloatStateOf(0f) }

val draggableState = rememberDraggableState { delta ->
state.layoutCoordinates?.let { coordinates ->
val size = if (strategy.isHorizontal()) coordinates.size.width else coordinates.size.height
val minFirstPositionPx = with(density) { firstPaneMinWidth.toPx() }
val minSecondPositionPx = with(density) { secondPaneMinWidth.toPx() }

dragOffset += delta
val position = size * state.dividerPosition + dragOffset
val newPosition = position.coerceIn(minFirstPositionPx, size - minSecondPositionPx)
state.dividerPosition = newPosition / size
/**
* Ensures that the divider in the split layout can be dragged, but it respects the minimum sizes of both
* panes and adjusts the layout accordingly. The position is calculated, constrained, and then applied to
* the dividerPosition in the state, ensuring a smooth and safe user experience during dragging
* interactions.
*/
if (minFirstPositionPx + minSecondPositionPx <= size) {
dragOffset += delta
val position = size * state.dividerPosition + dragOffset
val newPosition = position.coerceIn(minFirstPositionPx, size - minSecondPositionPx)
state.dividerPosition = newPosition / size
}
}
}

Expand Down Expand Up @@ -283,12 +292,9 @@ private fun MeasureScope.doLayout(
val splitResult = strategy.calculateSplitResult(density = density, layoutDirection = layoutDirection, state = state)

val gapOrientation = splitResult.gapOrientation
val gapBounds = splitResult.gapBounds

val dividerWidth = with(density) { dividerStyle.metrics.thickness.roundToPx() }
val handleWidth = with(density) { draggableWidth.roundToPx() }
val minFirstPaneSizePx = with(density) { firstPaneMinWidth.roundToPx() }
val minSecondPaneSizePx = with(density) { secondPaneMinWidth.roundToPx() }

// The visual divider itself. It's a thin line that separates the two panes
val dividerPlaceable =
Expand Down Expand Up @@ -346,47 +352,54 @@ private fun MeasureScope.doLayout(
(constraints.maxHeight - dividerWidth).coerceAtLeast(1)
}

val (adjustedFirstSize, adjustedSecondSize) =
calculateAdjustedSizes(availableSpace, minFirstPaneSizePx, minSecondPaneSizePx)
val minFirstPaneSizePx = with(density) { firstPaneMinWidth.roundToPx() }
val minSecondPaneSizePx = with(density) { secondPaneMinWidth.roundToPx() }

val firstGap =
when (gapOrientation) {
Orientation.Vertical -> gapBounds.left
Orientation.Horizontal -> gapBounds.top
}
// Calculate initial sizes based on divider position
val initialFirstSize = (availableSpace * state.dividerPosition).roundToInt()
val initialSecondSize = availableSpace - initialFirstSize

val firstSize: Int = firstGap.roundToInt().coerceIn(adjustedFirstSize, availableSpace - adjustedSecondSize)
val (adjustedFirstSize, adjustedSecondSize) =
calculateAdjustedSizes(
availableSpace,
initialFirstSize,
initialSecondSize,
minFirstPaneSizePx,
minSecondPaneSizePx,
)

val secondSize = availableSpace - firstSize
// Update state.dividerPosition to match adjusted sizes
state.dividerPosition = adjustedFirstSize.toFloat() / availableSpace.toFloat()

// Use the adjusted sizes directly for constraints
val firstConstraints =
when (gapOrientation) {
Orientation.Vertical -> constraints.copy(minWidth = adjustedFirstSize, maxWidth = firstSize)
Orientation.Horizontal -> constraints.copy(minHeight = adjustedFirstSize, maxHeight = firstSize)
Orientation.Vertical -> constraints.copy(minWidth = adjustedFirstSize, maxWidth = adjustedFirstSize)
Orientation.Horizontal -> constraints.copy(minHeight = adjustedFirstSize, maxHeight = adjustedFirstSize)
}

val secondConstraints =
when (gapOrientation) {
Orientation.Vertical -> constraints.copy(minWidth = adjustedSecondSize, maxWidth = secondSize)
Orientation.Horizontal -> constraints.copy(minHeight = adjustedSecondSize, maxHeight = secondSize)
Orientation.Vertical -> constraints.copy(minWidth = adjustedSecondSize, maxWidth = adjustedSecondSize)
Orientation.Horizontal -> constraints.copy(minHeight = adjustedSecondSize, maxHeight = adjustedSecondSize)
}

val firstPlaceable = firstMeasurable.measure(firstConstraints)
val secondPlaceable = secondMeasurable.measure(secondConstraints)

return layout(constraints.maxWidth, constraints.maxHeight) {
firstPlaceable.placeRelative(0, 0)
when (gapOrientation) {
Orientation.Vertical -> {
dividerPlaceable.placeRelative(firstSize, 0)
dividerHandlePlaceable.placeRelative(firstSize - handleWidth / 2, 0)
secondPlaceable.placeRelative(firstSize + dividerWidth, 0)
firstPlaceable.placeRelative(0, 0)
dividerPlaceable.placeRelative(adjustedFirstSize, 0)
dividerHandlePlaceable.placeRelative(adjustedFirstSize - handleWidth / 2, 0)
secondPlaceable.placeRelative(adjustedFirstSize + dividerWidth, 0)
}

Orientation.Horizontal -> {
dividerPlaceable.placeRelative(0, firstSize)
dividerHandlePlaceable.placeRelative(0, firstSize - handleWidth / 2)
secondPlaceable.placeRelative(0, firstSize + dividerWidth)
firstPlaceable.placeRelative(0, 0)
dividerPlaceable.placeRelative(0, adjustedFirstSize)
dividerHandlePlaceable.placeRelative(0, adjustedFirstSize - handleWidth / 2)
secondPlaceable.placeRelative(0, adjustedFirstSize + dividerWidth)
}
}
}
Expand Down Expand Up @@ -458,13 +471,41 @@ private fun verticalTwoPaneStrategy(gapHeight: Dp = 0.dp): SplitLayoutStrategy =
override fun isHorizontal(): Boolean = false
}

private fun calculateAdjustedSizes(availableSpace: Int, minFirstPaneSize: Int, minSecondPaneSize: Int): Pair<Int, Int> {
val totalMinSize = minFirstPaneSize + minSecondPaneSize
if (availableSpace >= totalMinSize) {
return minFirstPaneSize to minSecondPaneSize
private fun calculateAdjustedSizes(
availableSpace: Int,
initialFirstSize: Int,
initialSecondSize: Int,
minFirstPaneSizePx: Int,
minSecondPaneSizePx: Int,
): Pair<Int, Int> {
val totalMinSize = minFirstPaneSizePx + minSecondPaneSizePx

if (availableSpace <= totalMinSize) {
// Distribute space proportionally based on minimum sizes
val firstRatio = minFirstPaneSizePx.toFloat() / totalMinSize
val adjustedFirstSize = (availableSpace * firstRatio).roundToInt()
val adjustedSecondSize = availableSpace - adjustedFirstSize
return adjustedFirstSize to adjustedSecondSize
}

var adjustedFirstSize = initialFirstSize
var adjustedSecondSize = initialSecondSize

// Adjust first pane size if it's below minimum
if (adjustedFirstSize < minFirstPaneSizePx) {
adjustedFirstSize = minFirstPaneSizePx
adjustedSecondSize = availableSpace - adjustedFirstSize
}

// Adjust second pane size if it's below minimum
if (adjustedSecondSize < minSecondPaneSizePx) {
adjustedSecondSize = minSecondPaneSizePx
adjustedFirstSize = availableSpace - adjustedSecondSize
}

val ratio = minFirstPaneSize.toFloat() / totalMinSize
val adjustedFirstSize = (availableSpace * ratio).roundToInt()
return adjustedFirstSize to availableSpace - adjustedFirstSize
// Ensure sizes are within constraints
adjustedFirstSize = adjustedFirstSize.coerceIn(minFirstPaneSizePx, availableSpace - minSecondPaneSizePx)
adjustedSecondSize = adjustedSecondSize.coerceIn(minSecondPaneSizePx, availableSpace - adjustedFirstSize)

return adjustedFirstSize to adjustedSecondSize
}

0 comments on commit bf911c3

Please sign in to comment.