net.peanuuutz.fork.ui.foundation.input.ContentDragState.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fork-ui Show documentation
Show all versions of fork-ui Show documentation
Comprehensive API designed for Minecraft modders
The newest version!
package net.peanuuutz.fork.ui.foundation.input
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import net.peanuuutz.fork.ui.animation.spec.target.DefaultFloatOffsetAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.ui.layout.Alignment
import net.peanuuutz.fork.ui.ui.modifier.layout.layoutId
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.unit.FloatOffset
import net.peanuuutz.fork.ui.ui.unit.IntOffset
import net.peanuuutz.fork.ui.ui.unit.IntSize
import net.peanuuutz.fork.ui.ui.unit.coerceIn
import net.peanuuutz.fork.ui.ui.unit.roundToIntOffset
import net.peanuuutz.fork.ui.util.MutationPriority
import net.peanuuutz.fork.util.common.fastFindOrNull
import net.peanuuutz.fork.util.common.pack2Floats
import net.peanuuutz.fork.util.common.unpackFloat1
import net.peanuuutz.fork.util.common.unpackFloat2
@Composable
fun rememberContentDragState(
initialOffset: FloatOffset = FloatOffset.Zero
): ContentDragState {
return rememberSaveable(
saver = ContentDragState.Saver
) {
ContentDragState(initialOffset)
}
}
@Stable
class ContentDragState(
initialOffset: FloatOffset = FloatOffset.Zero
) : DragState {
// -------- States --------
// ---- Offset ----
var offset: FloatOffset
get() = _offset
set(value) {
if (_offset == value) {
return
}
_offset = value.coerceIn(FloatOffset.Zero, maxOffset)
}
val roundedOffset: IntOffset by derivedStateOf { offset.roundToIntOffset() }
var maxOffset: FloatOffset
get() = _maxOffset
set(value) {
if (_maxOffset == value) {
return
}
_maxOffset = value
_offset = offset.coerceIn(FloatOffset.Zero, value)
}
// ---- Container ----
val viewportSize: IntSize?
get() = _viewportSize
val contentSize: IntSize?
get() = _contentSize
val containerInfo: LayoutInfo?
get() = _containerInfo
// ======== Internal ========
// -------- Delegate --------
private val delegate: DragState = DragState { movement ->
val currentOffset = offset
val newOffset = currentOffset - movement
val coercedNewOffset = newOffset.coerceIn(FloatOffset.Zero, maxOffset)
val consumedMovement = currentOffset - coercedNewOffset
_offset = coercedNewOffset
consumedMovement
}
override val isDragging: Boolean
get() = delegate.isDragging
override suspend fun drag(priority: MutationPriority, scope: suspend DragScope.() -> Unit) {
delegate.drag(priority, scope)
}
override fun dragBy(movement: FloatOffset): FloatOffset {
return delegate.dragBy(movement)
}
// -------- States --------
// ---- Offset ----
private var _offset: FloatOffset by mutableStateOf(initialOffset)
private var _maxOffset: FloatOffset by mutableStateOf(FloatOffset.Zero)
// ---- Container ----
private var _viewportSize: IntSize? by mutableStateOf(null)
private var _contentSize: IntSize? by mutableStateOf(null)
private var _containerInfo: LayoutInfo? by mutableStateOf(null)
internal fun attach(info: LayoutInfo) {
_viewportSize = info.size
_contentSize = info.innerInfo!!.size
_containerInfo = info
}
internal fun detach() {
_viewportSize = null
_contentSize = null
_containerInfo = null
}
// ======== Public ========
companion object {
val Saver: Saver = Saver(
save = { state ->
val offset = state.offset
pack2Floats(offset.x, offset.y)
},
restore = { packedLong ->
val initialOffset = FloatOffset(
x = unpackFloat1(packedLong),
y = unpackFloat2(packedLong)
)
ContentDragState(initialOffset)
}
)
}
}
// -------- States --------
fun ContentDragState.canDrag(movement: FloatOffset): Boolean {
val (candidateX, candidateY) = offset + movement
val (maxX, maxY) = maxOffset
return candidateX in 0.0f..maxX && candidateY in 0.0f..maxY
}
// -------- State Operations --------
suspend fun ContentDragState.animateToItemByLayoutId(
layoutId: Any?,
alignmentInViewport: Alignment = Alignment.CenterOnPlane,
localOffset: FloatOffset = FloatOffset.Zero,
animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
priority: MutationPriority = MutationPriority.Default
): FloatOffset {
return animateToItem(
alignmentInViewport = alignmentInViewport,
localOffset = localOffset,
animationSpec = animationSpec,
priority = priority
) { items ->
items.fastFindOrNull { it.layoutId == layoutId }
}
}
suspend fun ContentDragState.snapToItemByLayoutId(
layoutId: Any?,
alignmentInViewport: Alignment = Alignment.CenterOnPlane,
localOffset: FloatOffset = FloatOffset.Zero,
priority: MutationPriority = MutationPriority.Default
): FloatOffset {
return snapToItem(
alignmentInViewport = alignmentInViewport,
localOffset = localOffset,
priority = priority
) { items ->
items.fastFindOrNull { it.layoutId == layoutId }
}
}
suspend fun ContentDragState.animateToItem(
alignmentInViewport: Alignment = Alignment.CenterOnPlane,
localOffset: FloatOffset = FloatOffset.Zero,
animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
priority: MutationPriority = MutationPriority.Default,
selector: (items: List) -> LayoutInfo?
): FloatOffset {
val targetOffset = calculateTargetOffsetFromItem(
alignmentInViewport = alignmentInViewport,
localOffset = localOffset,
selector = selector
)
return animateTo(
targetOffset = targetOffset,
animationSpec = animationSpec,
priority = priority
)
}
suspend fun ContentDragState.snapToItem(
alignmentInViewport: Alignment = Alignment.CenterOnPlane,
localOffset: FloatOffset = FloatOffset.Zero,
priority: MutationPriority = MutationPriority.Default,
selector: (items: List) -> LayoutInfo?
): FloatOffset {
val targetOffset = calculateTargetOffsetFromItem(
alignmentInViewport = alignmentInViewport,
localOffset = localOffset,
selector = selector
)
return snapTo(
targetOffset = targetOffset,
priority = priority
)
}
suspend fun ContentDragState.animateTo(
targetOffset: FloatOffset,
animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
priority: MutationPriority = MutationPriority.Default
): FloatOffset {
return animateDragBy(
movement = convertTargetOffsetToMovement(targetOffset),
animationSpec = animationSpec,
priority = priority
)
}
suspend fun ContentDragState.snapTo(
targetOffset: FloatOffset,
priority: MutationPriority = MutationPriority.Default
): FloatOffset {
var movement = FloatOffset.Zero
drag(priority) {
val providedMovement = convertTargetOffsetToMovement(targetOffset)
movement = dragBy(providedMovement)
}
return movement
}
// ======== Internal ========
internal const val TestMovementDuration: Float = 0.02f // 20 milliseconds
// -------- State Operations --------
private fun ContentDragState.convertTargetOffsetToMovement(targetOffset: FloatOffset): FloatOffset {
return targetOffset.coerceIn(FloatOffset.Zero, maxOffset) - offset
}
internal fun ContentDragState.calculateTargetOffsetFromItem(
alignmentInViewport: Alignment,
localOffset: FloatOffset,
selector: (items: List) -> LayoutInfo?
): FloatOffset {
val currentOffset = offset
val viewportInfo = containerInfo ?: return currentOffset
val itemInfo = selector(viewportInfo.childrenInfo) ?: return currentOffset
val itemPosition = viewportInfo.localPositionOf(itemInfo)
val alignmentOffset = alignmentInViewport.align(
contentSize = itemInfo.size,
availableSpace = viewportInfo.size
)
val itemOffset = itemPosition - alignmentOffset
return currentOffset + itemOffset + localOffset
}