All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.peanuuutz.fork.ui.foundation.input.Draggable.kt Maven / Gradle / Ivy

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy