package cc.unitmesh.devins.ui.base import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import kotlin.math.abs import kotlin.math.roundToInt /** * A high-performance resizable split pane that divides two composables horizontally * Optimized for smooth dragging and excellent visual feedback * * @param modifier The modifier to apply to this layout * @param initialSplitRatio The initial split ratio (0.0 to 1.0) for the first pane * @param minRatio The minimum split ratio for the first pane * @param maxRatio The maximum split ratio for the first pane * @param dividerWidth The width of the divider in dp * @param saveKey Optional key for saving/restoring split ratio across app sessions. If null, state is not persisted. * @param first The first composable (left side) * @param second The second composable (right side) */ @Composable fun ResizableSplitPane( modifier: Modifier = Modifier, initialSplitRatio: Float = 0.5f, minRatio: Float = 0.2f, maxRatio: Float = 0.8f, dividerWidth: Int = 4, saveKey: String? = null, first: @Composable () -> Unit, second: @Composable () -> Unit ) { // Use rememberSaveable when saveKey is provided for persistent state var splitRatio by if (saveKey != null) { rememberSaveable(key = saveKey) { mutableStateOf(initialSplitRatio.coerceIn(minRatio, maxRatio)) } } else { remember { mutableStateOf(initialSplitRatio.coerceIn(minRatio, maxRatio)) } } var isDragging by remember { mutableStateOf(false) } var containerWidth by remember { mutableStateOf(0) } // Track hover state for smooth animations val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() // Smooth opacity animation for hover effect val dividerAlpha by animateFloatAsState( targetValue = when { isDragging -> 1f isHovered -> 0.8f else -> 0.4f }, animationSpec = tween(durationMillis = 150), label = "dividerAlpha" ) // Smooth scale animation for better visual feedback val dividerScale by animateFloatAsState( targetValue = when { isDragging -> 1.2f isHovered -> 1.1f else -> 1f }, animationSpec = tween(durationMillis = 150), label = "dividerScale" ) Layout( modifier = modifier, content = { // First pane Box(modifier = Modifier.fillMaxHeight()) { first() } // Enhanced divider with visual feedback Box( modifier = Modifier .width(dividerWidth.dp) .fillMaxHeight() .hoverable(interactionSource) .pointerHoverIcon(PointerIcon.Crosshair) // Better cursor for resizing ) { // Background layer with subtle gradient effect Spacer( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) ) // Highlighted center strip for grabbing Spacer( modifier = Modifier .width((dividerWidth * dividerScale).dp) .fillMaxHeight() .alpha(dividerAlpha) .background( when { isDragging -> MaterialTheme.colorScheme.primary isHovered -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) else -> MaterialTheme.colorScheme.outlineVariant } ) .pointerInput(containerWidth) { detectDragGestures( onDragStart = { isDragging = true }, onDragEnd = { isDragging = false }, onDragCancel = { isDragging = false } ) { change, dragAmount -> change.consume() // Calculate delta based on total container width for accurate dragging if (containerWidth > 0) { val delta = dragAmount.x / containerWidth // Optimize: Only update if change is meaningful (reduces recomposition) val newRatio = (splitRatio + delta).coerceIn(minRatio, maxRatio) if (abs(newRatio - splitRatio) > 0.001f) { splitRatio = newRatio } } } } ) } // Second pane Box(modifier = Modifier.fillMaxHeight()) { second() } } ) { measurables, constraints -> // Store container width for accurate drag calculations containerWidth = constraints.maxWidth // Pre-calculate dimensions once for better performance val dividerWidthPx = (dividerWidth.dp).roundToPx() val availableWidth = constraints.maxWidth - dividerWidthPx // Calculate pane widths val firstWidth = (availableWidth * splitRatio).roundToInt().coerceAtLeast(0) val secondWidth = (availableWidth - firstWidth).coerceAtLeast(0) // Measure all children with fixed constraints val firstPlaceable = measurables[0].measure( Constraints.fixed(firstWidth, constraints.maxHeight) ) val dividerPlaceable = measurables[1].measure( Constraints.fixed(dividerWidthPx, constraints.maxHeight) ) val secondPlaceable = measurables[2].measure( Constraints.fixed(secondWidth, constraints.maxHeight) ) // Place children in layout layout(constraints.maxWidth, constraints.maxHeight) { firstPlaceable.placeRelative(0, 0) dividerPlaceable.placeRelative(firstWidth, 0) secondPlaceable.placeRelative(firstWidth + dividerWidthPx, 0) } } }