LAP2
4 years ago
committed by
GitHub
13 changed files with 762 additions and 2 deletions
@ -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")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -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") {} |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
) |
@ -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 |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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 |
||||||
|
) |
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
) |
||||||
|
|
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue