Skip to content

Commit

Permalink
Support draggable rows and columns, fix scroll animation
Browse files Browse the repository at this point in the history
  • Loading branch information
devkanro committed Feb 23, 2024
1 parent e69dae2 commit 3320ff9
Show file tree
Hide file tree
Showing 19 changed files with 783 additions and 74 deletions.
244 changes: 244 additions & 0 deletions foundation/api/foundation.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ public fun OverflowBox(
showOverflow = it
}.then(modifier),
contentAlignment = contentAlignment,
content = content
content = content,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.jetbrains.jewel.foundation.lazy.draggable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectDragGestures
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.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.modifier.modifierLocalProvider
import androidx.compose.ui.zIndex

internal val ModifierLocalDraggableLayoutOffset = modifierLocalOf { Offset.Zero }

public fun Modifier.draggableLayout(): Modifier = composed {
var offset by remember { mutableStateOf(Offset.Zero) }

this.onGloballyPositioned {
offset = it.positionInRoot()
}.modifierLocalProvider(ModifierLocalDraggableLayoutOffset) {
offset
}
}

public fun Modifier.draggableItem(
state: LazyLayoutDraggableState<*>,
key: Any,
draggable: Boolean = true,
orientation: Orientation? = null,
): Modifier {
return if (draggable) {
this.draggingItem(state, key, orientation).draggableItemHandle(state, key)
} else {
this.draggingItem(state, key, orientation)
}
}

private fun Modifier.draggableItemHandle(
state: LazyLayoutDraggableState<*>,
key: Any,
): Modifier = composed {
var itemOffset by remember { mutableStateOf(Offset.Zero) }
var layoutOffset by remember { mutableStateOf(Offset.Zero) }

onGloballyPositioned {
itemOffset = it.positionInRoot()
}.modifierLocalConsumer {
layoutOffset = ModifierLocalDraggableLayoutOffset.current
}.pointerInput(Unit) {
detectDragGestures(onDrag = { change, offset ->
change.consume()
state.onDrag(offset)
}, onDragStart = {
state.onDragStart(key, it + itemOffset - layoutOffset)
}, onDragEnd = {
state.onDragInterrupted()
}, onDragCancel = {
state.onDragInterrupted()
})
}
}

private fun Modifier.draggingItem(
state: LazyLayoutDraggableState<*>,
key: Any,
orientation: Orientation? = null,
): Modifier = composed {
val dragging = state.draggingItemKey == key
if (dragging) {
this.then(
Modifier.zIndex(2f).graphicsLayer(
translationX = if (orientation == Orientation.Vertical) 0f else state.draggingItemOffsetTransformX.toFloat(),
translationY = if (orientation == Orientation.Horizontal) 0f else state.draggingItemOffsetTransformY.toFloat(),
),
)
} else {
this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.jetbrains.jewel.foundation.lazy.draggable

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size

public abstract class LazyLayoutDraggableState<T> {

public var draggingItemOffsetTransformX: Float by mutableStateOf(0f)

public var draggingItemOffsetTransformY: Float by mutableStateOf(0f)

public var draggingItemKey: Any? by mutableStateOf(null)

public var initialOffset: Offset = Offset.Zero

public var draggingOffset: Offset = Offset.Zero

internal val interactionSource: MutableInteractionSource = MutableInteractionSource()

public fun onDragStart(key: Any?, offset: Offset) {
draggingItemKey = key
initialOffset = offset
}

public fun onDrag(offset: Offset) {
draggingItemOffsetTransformX += offset.x
draggingItemOffsetTransformY += offset.y
draggingOffset += offset

val draggingItem = getItemWithKey(draggingItemKey ?: return) ?: return
val hoverItem = getItemAt(initialOffset + draggingOffset)

if (hoverItem != null && draggingItem != hoverItem) {
val changedOffset = draggingItem.offset - hoverItem.offset

draggingItemOffsetTransformX += changedOffset.x
draggingItemOffsetTransformY += changedOffset.y

moveItem(draggingItem.index, hoverItem.index)
}
}

public fun onDragInterrupted() {
draggingItemKey = null
initialOffset = Offset.Zero
draggingOffset = Offset.Zero
draggingItemOffsetTransformX = 0f
draggingItemOffsetTransformY = 0f
}

public abstract fun moveItem(from: Int, to: Int)

public abstract fun getItemAt(offset: Offset): T?

public abstract fun getItemWithKey(key: Any): T?

public abstract val T.offset: Offset

public abstract val T.size: Size

public abstract val T.index: Int

public abstract val T.key: Any?
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ public fun LazyTable(
beyondBoundsItemCount: Int = 0,
horizontalArrangement: Arrangement.Horizontal? = null,
verticalArrangement: Arrangement.Vertical? = null,
content: LazyTableScope.() -> Unit,
content: LazyTableScope.() -> LazyTableCells,
) {
val itemProvider = rememberLazyTableItemProvider(state, pinnedColumns, pinnedRows, content)
val itemProviderLambda = rememberLazyTableItemProviderLambda(state, pinnedColumns, pinnedRows, content)

val measurePolicy = rememberLazyTabletMeasurePolicy(
itemProvider = itemProvider,
itemProviderLambda = itemProviderLambda,
cellSize = cellSize,
state = state,
pinnedColumns = pinnedColumns,
Expand Down Expand Up @@ -94,13 +94,13 @@ public fun LazyTable(
.clip(RectangleShape),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
itemProvider = itemProvider,
itemProvider = itemProviderLambda,
)
}

@Composable
private fun rememberLazyTabletMeasurePolicy(
itemProvider: LazyTableItemProvider,
itemProviderLambda: () -> LazyTableItemProvider,
cellSize: LazyTableCellSize,
state: LazyTableState,
pinnedColumns: Int,
Expand All @@ -111,7 +111,6 @@ private fun rememberLazyTabletMeasurePolicy(
verticalArrangement: Arrangement.Vertical? = null,
): LazyLayoutMeasureScope.(Constraints) -> MeasureResult =
remember<LazyLayoutMeasureScope.(Constraints) -> MeasureResult>(
itemProvider,
state,
pinnedColumns,
pinnedRows,
Expand Down Expand Up @@ -146,6 +145,7 @@ private fun rememberLazyTabletMeasurePolicy(
val horizontalSpacing = horizontalArrangement?.spacing?.roundToPx() ?: 0
val verticalSpacing = verticalArrangement?.spacing?.roundToPx() ?: 0

val itemProvider = itemProviderLambda()
val measuredItemProvider = object : LazyTableMeasuredItemProvider(
availableSize = availableSize,
rows = itemProvider.rowCount,
Expand All @@ -155,7 +155,7 @@ private fun rememberLazyTabletMeasurePolicy(
verticalSpacing = verticalSpacing,
itemProvider = itemProvider,
measureScope = this,
density = this
density = this,
) {
override fun createItem(
column: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMaxOfOrNull
import androidx.compose.ui.util.fastSumBy
import org.jetbrains.jewel.foundation.utils.debugLog
import org.jetbrains.jewel.foundation.util.debugLog
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.abs

Expand All @@ -27,6 +28,8 @@ internal interface LazyTableAnimateScrollScope {

fun getTargetLineOffset(index: Int): Int?

fun getStartLineOffset(): Int

fun ScrollScope.snapToLine(index: Int, scrollOffset: Int)

fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float
Expand Down Expand Up @@ -206,7 +209,7 @@ internal suspend fun LazyTableAnimateScrollScope.animateScrollToItem(
// We found it, animate to it
// Bring to the requested position - will be automatically stopped if not possible
val anim = itemFound.previousAnimation.copy(value = 0f)
val target = (itemFound.lineOffset + scrollOffset).toFloat()
val target = (itemFound.lineOffset + scrollOffset - getStartLineOffset()).toFloat()
var prevValue = 0f
debugLog {
"Seeking by $target at velocity ${itemFound.previousAnimation.velocity}"
Expand Down Expand Up @@ -275,6 +278,14 @@ internal class LazyTableAnimateHorizontalScrollScope(
return null
}

override fun getStartLineOffset(): Int {
return (
state.layoutInfo.pinnedItemsInfo.fastMaxOfOrNull {
it.offset.x + it.size.width
} ?: 0
) + state.layoutInfo.horizontalSpacing
}

override fun ScrollScope.snapToLine(index: Int, scrollOffset: Int) {
state.snapToColumnInternal(index, scrollOffset)
}
Expand Down Expand Up @@ -325,6 +336,14 @@ internal class LazyTableAnimateVerticalScrollScope(
return null
}

override fun getStartLineOffset(): Int {
return (
state.layoutInfo.pinnedItemsInfo.fastMaxOfOrNull {
it.offset.y + it.size.height
} ?: 0
) + state.layoutInfo.verticalSpacing
}

override fun ScrollScope.snapToLine(index: Int, scrollOffset: Int) {
state.snapToRowInternal(index, scrollOffset)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.jetbrains.jewel.foundation.lazy.table

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastFirstOrNull
import org.jetbrains.jewel.foundation.lazy.draggable.LazyLayoutDraggableState

@Composable
public fun rememberLazyTableRowDraggingState(
tableState: LazyTableState = rememberLazyTableState(),
onMove: (Int, Int) -> Unit,
): LazyTableRowDraggingState {
return remember(tableState, onMove) {
LazyTableRowDraggingState(tableState, onMove)
}
}

@Composable
public fun rememberLazyTableColumnDraggingState(
tableState: LazyTableState = rememberLazyTableState(),
onMove: (Int, Int) -> Unit,
): LazyTableColumnDraggingState {
return remember(tableState, onMove) {
LazyTableColumnDraggingState(tableState, onMove)
}
}

public abstract class LazyTableDraggableState(
public val tableState: LazyTableState,
public val onMove: (Int, Int) -> Unit,
) : LazyLayoutDraggableState<LazyTableItemInfo>() {

override fun getItemAt(offset: Offset): LazyTableItemInfo? {
return tableState.layoutInfo.pinnedColumnsInfo.fastFirstOrNull {
offset in Rect(it.offset.toOffset(), it.size.toSize())
} ?: tableState.layoutInfo.pinnedRowsInfo.fastFirstOrNull {
offset in Rect(it.offset.toOffset(), it.size.toSize())
} ?: tableState.layoutInfo.pinnedItemsInfo.fastFirstOrNull {
offset in Rect(it.offset.toOffset(), it.size.toSize())
}
}

override val LazyTableItemInfo.size: Size
get() = this.size.toSize()

override val LazyTableItemInfo.offset: Offset
get() = this.offset.toOffset()

override fun moveItem(from: Int, to: Int) {
onMove(from, to)
}
}

public class LazyTableRowDraggingState(
tableState: LazyTableState,
onMove: (Int, Int) -> Unit,
) : LazyTableDraggableState(tableState, onMove) {

override val LazyTableItemInfo.index: Int
get() = this.row

override val LazyTableItemInfo.key: Any?
get() = (this.key as Pair<Any?, Any?>?)?.second

override fun getItemWithKey(key: Any): LazyTableItemInfo? {
return tableState.layoutInfo.pinnedColumnsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.second == key
} ?: tableState.layoutInfo.pinnedRowsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.second == key
} ?: tableState.layoutInfo.pinnedItemsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.second == key
}
}
}

public class LazyTableColumnDraggingState(
tableState: LazyTableState,
onMove: (Int, Int) -> Unit,
) : LazyTableDraggableState(tableState, onMove) {

override val LazyTableItemInfo.index: Int
get() = this.column

override val LazyTableItemInfo.key: Any?
get() = (this.key as Pair<Any?, Any?>?)?.first

override fun getItemWithKey(key: Any): LazyTableItemInfo? {
return tableState.layoutInfo.pinnedColumnsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.first == key
} ?: tableState.layoutInfo.pinnedRowsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.first == key
} ?: tableState.layoutInfo.pinnedItemsInfo.fastFirstOrNull {
(it.key as Pair<Any?, Any?>?)?.first == key
}
}
}
Loading

0 comments on commit 3320ff9

Please sign in to comment.