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