net.peanuuutz.fork.ui.foundation.input.Draggable.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!
/*
* Copyright 2020 The Android Open Source Project
* Modifications Copyright 2022 Peanuuutz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.peanuuutz.fork.ui.foundation.input
import androidx.compose.runtime.Stable
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import net.peanuuutz.fork.ui.foundation.input.interaction.DragInteraction
import net.peanuuutz.fork.ui.foundation.input.interaction.MutableInteractionSource
import net.peanuuutz.fork.ui.foundation.input.interaction.detectAndEmitDragInteractions
import net.peanuuutz.fork.ui.foundation.input.interaction.tryEmitCancelOnDrag
import net.peanuuutz.fork.ui.inspection.InspectInfo
import net.peanuuutz.fork.ui.ui.context.pointer.PointerEvent
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.composed
import net.peanuuutz.fork.ui.ui.modifier.input.SuspendingPointerInputModifierNode
import net.peanuuutz.fork.ui.ui.node.BranchingModifierNode
import net.peanuuutz.fork.ui.ui.node.PointerEventPass
import net.peanuuutz.fork.ui.ui.node.PointerInputModifierNode
import net.peanuuutz.fork.ui.ui.unit.FloatOffset
import net.peanuuutz.fork.ui.util.MutationPriority
@Stable
fun Modifier.draggable(
interactionSource: MutableInteractionSource? = null,
label: Any? = null,
onStart: (CoroutineScope.() -> Unit)? = null,
onStop: (CoroutineScope.(velocity: FloatOffset) -> Unit)? = null,
isEnabled: Boolean = true,
onDrag: (movement: FloatOffset) -> FloatOffset
): Modifier {
if (!isEnabled) {
return this
}
return composed {
draggable(
state = rememberDragState(onDrag),
label = label,
interactionSource = interactionSource,
onStart = onStart,
onStop = onStop
)
}
}
@Stable
fun Modifier.draggable(
state: DragState,
interactionSource: MutableInteractionSource? = null,
label: Any? = null,
onStart: (CoroutineScope.() -> Unit)? = null,
onStop: (CoroutineScope.(velocity: FloatOffset) -> Unit)? = null,
isEnabled: Boolean = true
): Modifier {
if (!isEnabled) {
return this
}
val element = DraggableModifier(
state = state,
interactionSource = interactionSource,
label = label,
onStart = onStart,
onStop = onStop
)
return this then element
}
// ======== Internal ========
private data class DraggableModifier(
val state: DragState,
val interactionSource: MutableInteractionSource?,
val label: Any?,
val onStart: (CoroutineScope.() -> Unit)?,
val onStop: (CoroutineScope.(FloatOffset) -> Unit)?
) : ModifierNodeElement() {
override fun create(): DraggableModifierNode {
return DraggableModifierNode(
state = state,
interactionSource = interactionSource,
label = label,
onStart = onStart,
onStop = onStop
)
}
override fun update(node: DraggableModifierNode) {
node.state = state
node.interactionSource = interactionSource
node.label = label
node.onStart = onStart
node.onStop = onStop
}
override fun InspectInfo.inspect() {
set("label", label)
}
}
private class DraggableModifierNode(
state: DragState,
interactionSource: MutableInteractionSource?,
label: Any?,
var onStart: (CoroutineScope.() -> Unit)?,
var onStop: (CoroutineScope.(velocity: FloatOffset) -> Unit)?
) : BranchingModifierNode(), PointerInputModifierNode {
var state: DragState = state
set(value) {
if (field == value) {
return
}
field = value
launch("New DragState has been applied")
}
var interactionSource: MutableInteractionSource? = interactionSource
set(value) {
if (field == value) {
return
}
field?.tryEmitCancelOnDrag(
stateProvider = this::dragState,
stateUpdater = this::dragState::set
)
field = value
}
var label: Any? = label
set(value) {
if (field == value) {
return
}
interactionSource?.tryEmitCancelOnDrag(
stateProvider = this::dragState,
stateUpdater = this::dragState::set
)
field = value
}
private val pointerInputHandler: SuspendingPointerInputModifierNode = branch {
SuspendingPointerInputModifierNode {
coroutineScope {
launch {
detectAndEmitDragInteractions(
interactionSourceProvider = this@DraggableModifierNode::interactionSource,
stateProvider = this@DraggableModifierNode::dragState,
stateUpdater = this@DraggableModifierNode::dragState::set,
labelProvider = this@DraggableModifierNode::label
)
}
launch {
detectDrag(
onStart = {
channel.trySend(DragEvent.Start)
},
onStop = { lastMoveEvent, _ ->
val velocity = lastMoveEvent.offset * DraggableVelocityModifier
channel.trySend(DragEvent.Stop(velocity))
},
onCancel = {
channel.trySend(DragEvent.Cancel)
},
onDrag = { moveEvent ->
val movement = moveEvent.offset
channel.trySend(DragEvent.Move(movement))
}
)
}
}
}
}
private val channel: Channel = Channel(capacity = Channel.UNLIMITED)
private var job: Job? = null
private var dragState: DragInteraction.Start? = null
override fun onAttach() {
launch(null)
}
override fun onDetach() {
cancel()
}
override fun onPointerEvent(pass: PointerEventPass, pointerEvent: PointerEvent) {
pointerInputHandler.onPointerEvent(pass, pointerEvent)
}
private fun launch(reason: String?) {
if (reason != null) {
job?.cancel(CancellationException(reason))
}
job = nodeScope.launch {
while (isActive) {
var currentEvent = channel.receive()
if (currentEvent !is DragEvent.Start) {
continue
}
val onStart = onStart
if (onStart != null) {
onStart()
}
state.drag(MutationPriority.User) {
while (isActive) {
currentEvent = channel.receive()
val event = currentEvent
if (event is DragEvent.Move) {
dragBy(event.movement)
} else {
break
}
}
}
val event = currentEvent
val onStop = onStop
if (onStop != null) {
if (event is DragEvent.Stop) {
onStop(event.velocity)
} else if (event is DragEvent.Cancel) {
onStop(FloatOffset.Zero)
}
}
}
}
}
private fun cancel() {
job?.cancel(CancellationException("This drag event listener was cancelled"))
job = null
}
}
// TODO Migrate to data object
private sealed class DragEvent {
object Start : DragEvent()
data class Move(val movement: FloatOffset) : DragEvent()
data class Stop(val velocity: FloatOffset) : DragEvent()
object Cancel : DragEvent()
}
// TODO Implement velocity tracker
private const val DraggableVelocityModifier: Float = 50.0f