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

commonMain.androidx.compose.ui.draganddrop.DragAndDropNode.kt Maven / Gradle / Ivy

/*
 * Copyright 2023 The Android Open Source Project
 *
 * 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 androidx.compose.ui.draganddrop

import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction.CancelTraversal
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
import androidx.compose.ui.node.requireLayoutNode
import androidx.compose.ui.node.requireOwner
import androidx.compose.ui.node.traverseDescendants
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
import kotlin.js.JsName
import kotlin.jvm.JvmName

/**
 * A [Modifier.Node] providing low level access to platform drag and drop operations. In most cases,
 * you will want to delegate to the [DragAndDropModifierNode] returned by the eponymous factory
 * method.
 */
@Deprecated(
    message =
        "This interface is deprecated in favor to " +
            "DragAndDropSourceModifierNode and DragAndDropTargetModifierNode",
    replaceWith = ReplaceWith("DragAndDropSourceModifierNode")
)
interface DragAndDropModifierNode : DelegatableNode, DragAndDropTarget {
    /**
     * Begins a drag and drop session for transferring data.
     *
     * @param transferData the data to be transferred after successful completion of the drag and
     *   drop gesture.
     * @param decorationSize the size of the drag decoration to be drawn.
     * @param drawDragDecoration provides the visual representation of the item dragged during the
     *   drag and drop gesture.
     */
    @Deprecated("Use DragAndDropSourceModifierNode.requestDragAndDropTransfer instead")
    fun drag(
        transferData: DragAndDropTransferData,
        decorationSize: Size,
        drawDragDecoration: DrawScope.() -> Unit,
    )

    /**
     * The entry point to register interest in a drag and drop session for receiving data.
     *
     * @return true to indicate interest in the contents of a drag and drop session, false indicates
     *   no interest. If false is returned, this [Modifier] will not receive any [DragAndDropTarget]
     *   events.
     *
     * All [DragAndDropModifierNode] instances in the hierarchy will be given an opportunity to
     * participate in a drag and drop session via this method.
     */
    fun acceptDragAndDropTransfer(startEvent: DragAndDropEvent): Boolean
}

/**
 * A [Modifier.Node] that can be used as a source for platform drag and drop operations. In most
 * cases, you will want to delegate to the [DragAndDropSourceModifierNode] returned by the eponymous
 * factory method.
 */
sealed interface DragAndDropSourceModifierNode : LayoutAwareModifierNode {
    /**
     * Returns a boolean value indicating whether requesting drag and drop transfer is required.
     *
     * This variable is used to check if the platform requires drag and drop transfer initiated by
     * application explicitly, for example via a custom gesture.
     *
     * @see requestDragAndDropTransfer
     */
    val isRequestDragAndDropTransferRequired: Boolean

    /**
     * Requests a drag and drop transfer. [isRequestDragAndDropTransferRequired] can be used to
     * check if it required to be performed.
     *
     * @param offset the offset value representing position of the input pointer.
     */
    fun requestDragAndDropTransfer(offset: Offset)
}

/**
 * A [Modifier.Node] that can be used as a target for platform drag and drop operations. In most
 * cases, you will want to delegate to the [DragAndDropTargetModifierNode] returned by the eponymous
 * factory method.
 *
 * This interface does not define any additional methods or properties. It simply serves as a marker
 * interface to identify nodes that can be used as drag and drop target modifiers.
 */
sealed interface DragAndDropTargetModifierNode : LayoutAwareModifierNode

/**
 * Creates a [Modifier.Node] for starting platform drag and drop sessions with the intention of
 * transferring data. A drag and stop session is started by calling [DragAndDropModifierNode.drag].
 */
@Deprecated(
    message = "Use DragAndDropSourceModifierNode instead",
    replaceWith = ReplaceWith("DragAndDropSourceModifierNode")
)
@Suppress("DEPRECATION")
@JsName("funDragAndDropModifierNode1")
fun DragAndDropModifierNode(): DragAndDropModifierNode = DragAndDropNode(onStartTransfer = null)

/**
 * Creates a [Modifier.Node] for receiving transfer data from platform drag and drop sessions. All
 * [DragAndDropModifierNode] instances provided by this function may also start drag and drop
 * sessions by calling [DragAndDropModifierNode.drag].
 *
 * @param shouldStartDragAndDrop allows for inspecting the start [DragAndDropEvent] for a given
 *   session to decide whether or not the provided [DragAndDropTarget] would like to receive from
 *   it.
 * @param target allows for receiving events and transfer data from a given drag and drop session.
 */
@Deprecated(
    message = "Use DragAndDropTargetModifierNode instead",
    replaceWith = ReplaceWith("DragAndDropTargetModifierNode")
)
@Suppress("DEPRECATION")
@JsName("funDragAndDropModifierNode2")
fun DragAndDropModifierNode(
    shouldStartDragAndDrop: (event: DragAndDropEvent) -> Boolean,
    target: DragAndDropTarget
): DragAndDropModifierNode =
    DragAndDropNode(
        onDropTargetValidate = { event -> if (shouldStartDragAndDrop(event)) target else null }
    )

/**
 * Creates a [DragAndDropSourceModifierNode] for starting platform drag and drop sessions with the
 * intention of transferring data.
 *
 * @param onStartTransfer the callback function that is invoked when drag and drop session starts.
 *   It takes an [Offset] parameter representing the start position of the drag.
 */
fun DragAndDropSourceModifierNode(
    onStartTransfer: DragAndDropStartTransferScope.(Offset) -> Unit,
): DragAndDropSourceModifierNode = DragAndDropNode(onStartTransfer = onStartTransfer)

/**
 * Creates a [DragAndDropTargetModifierNode] for receiving transfer data from platform drag and drop
 * sessions.
 *
 * @param shouldStartDragAndDrop allows for inspecting the start [DragAndDropEvent] for a given
 *   session to decide whether or not the provided [DragAndDropTarget] would like to receive from
 *   it.
 * @param target allows for receiving events and transfer data from a given drag and drop session.
 */
fun DragAndDropTargetModifierNode(
    shouldStartDragAndDrop: (event: DragAndDropEvent) -> Boolean,
    target: DragAndDropTarget
): DragAndDropTargetModifierNode =
    DragAndDropNode(
        onDropTargetValidate = { event -> if (shouldStartDragAndDrop(event)) target else null }
    )

/**
 * Core implementation of drag and drop. This [Modifier.Node] implements tree traversal for drag and
 * drop, as well as hit testing and propagation of events for drag or drop gestures.
 *
 * It uses the [DragAndDropEvent] as a representation of a single mutable drag and drop session.
 *
 * The implementation implicitly maintains a sorted tree of nodes where the order of traversal is
 * determined by the proximity to the last event. That is, after finding a receiving node, the next
 * event will follow the same path the previous event did unless a fork is found and another node
 * should receive the event.
 *
 * This optimizes traversal for the common case of move events where the event remains within a
 * single node, or moves to a sibling of the node.
 *
 * This intended to be used directly only by [DragAndDropManager].
 */
@Suppress("DEPRECATION")
internal class DragAndDropNode(
    private var onStartTransfer: (DragAndDropStartTransferScope.(Offset) -> Unit)? = null,
    private val onDropTargetValidate: ((DragAndDropEvent) -> DragAndDropTarget?)? = null,
) :
    Modifier.Node(),
    TraversableNode,
    DragAndDropModifierNode,
    DragAndDropSourceModifierNode,
    DragAndDropTargetModifierNode,
    DragAndDropTarget {
    private companion object {
        private object DragAndDropTraversableKey
    }

    override val traverseKey: Any = DragAndDropTraversableKey

    private val dragAndDropManager: DragAndDropManager
        get() = requireOwner().dragAndDropManager

    /** Child currently receiving drag gestures for dropping into */
    private var lastChildDragAndDropModifierNode: DragAndDropNode? = null

    /** This as a drop target if eligible for processing */
    private var thisDragAndDropTarget: DragAndDropTarget? = null

    /**
     * Indicates whether there is a child that is eligible to receive a drop gesture immediately.
     * This is true if the last move happened over a child that is interested in receiving a drop.
     */
    @get:JvmName("hasEligibleDropTarget")
    val hasEligibleDropTarget: Boolean
        get() = lastChildDragAndDropModifierNode != null || thisDragAndDropTarget != null

    internal var size: IntSize = IntSize.Zero

    // start Node

    override fun onDetach() {
        // Clean up
        thisDragAndDropTarget = null
        lastChildDragAndDropModifierNode = null
    }

    // end Node

    // start LayoutAwareModifierNode

    override fun onRemeasured(size: IntSize) {
        this.size = size
    }

    // end LayoutAwareModifierNode

    // start DragAndDropSourceModifierNode

    override val isRequestDragAndDropTransferRequired: Boolean
        get() = dragAndDropManager.isRequestDragAndDropTransferRequired

    override fun requestDragAndDropTransfer(offset: Offset) {
        checkPrecondition(onStartTransfer != null)
        dragAndDropManager.requestDragAndDropTransfer(this, offset)
    }

    // end DragAndDropSourceModifierNode

    /**
     * Initiates a drag-and-drop operation for transferring data.
     *
     * @param offset the offset value representing position of the input pointer.
     * @param isTransferStarted a lambda function that returns true if the drag-and-drop transfer
     *   has started, or false otherwise.
     */
    fun DragAndDropStartTransferScope.startDragAndDropTransfer(
        offset: Offset,
        isTransferStarted: () -> Boolean
    ) {
        val nodeCoordinates = requireLayoutNode().coordinates
        traverseSelfAndDescendants { currentNode ->
            // TODO: b/303904810 unattached nodes should not be found from an attached
            //  root drag and drop node
            if (!currentNode.isAttached) {
                return@traverseSelfAndDescendants SkipSubtreeAndContinueTraversal
            }

            val onStartTransfer =
                currentNode.onStartTransfer ?: return@traverseSelfAndDescendants ContinueTraversal

            if (offset != Offset.Unspecified) {
                val currentCoordinates = currentNode.requireLayoutNode().coordinates
                val localPosition = currentCoordinates.localPositionOf(nodeCoordinates, offset)
                if (!currentNode.size.toSize().toRect().contains(localPosition)) {
                    return@traverseSelfAndDescendants ContinueTraversal
                }

                onStartTransfer.invoke(this, localPosition)
            } else {
                onStartTransfer.invoke(this, Offset.Unspecified)
            }

            if (isTransferStarted()) {
                CancelTraversal
            } else {
                ContinueTraversal
            }
        }
    }

    // start DragAndDropModifierNode

    @Deprecated("Use DragAndDropSourceModifierNode.requestDragAndDropTransfer instead")
    override fun drag(
        transferData: DragAndDropTransferData,
        decorationSize: Size,
        drawDragDecoration: DrawScope.() -> Unit
    ) {
        checkPrecondition(onStartTransfer == null)
        onStartTransfer = {
            startDragAndDropTransfer(transferData, decorationSize, drawDragDecoration)
        }
        dragAndDropManager.requestDragAndDropTransfer(this, Offset.Unspecified)
        onStartTransfer = null
    }

    /**
     * The entry point to register interest in a drag and drop session for receiving data.
     *
     * @return true to indicate interest in the contents of a drag and drop session, false indicates
     *   no interest. If false is returned, this [Modifier] will not receive any [DragAndDropTarget]
     *   events.
     */
    override fun acceptDragAndDropTransfer(startEvent: DragAndDropEvent): Boolean {
        var handled = false
        traverseSelfAndDescendants { currentNode ->
            // TODO: b/303904810 unattached nodes should not be found from an attached
            //  root drag and drop node
            if (!currentNode.isAttached) {
                return@traverseSelfAndDescendants SkipSubtreeAndContinueTraversal
            }

            checkPrecondition(currentNode.thisDragAndDropTarget == null) {
                "DragAndDropTarget self reference must be null" +
                    " at the start of a drag and drop session"
            }

            // Start receiving events
            currentNode.thisDragAndDropTarget = currentNode.onDropTargetValidate?.invoke(startEvent)

            val accepted = currentNode.thisDragAndDropTarget != null
            if (accepted) {
                dragAndDropManager.registerTargetInterest(currentNode)
            }
            handled = handled || accepted
            ContinueTraversal
        }
        return handled
    }

    // end DragAndDropModifierNode

    // start DropTarget

    override fun onStarted(event: DragAndDropEvent) {
        when (val self = thisDragAndDropTarget) {
            null -> lastChildDragAndDropModifierNode?.onStarted(event = event)
            else -> self.onStarted(event = event)
        }
    }

    override fun onEntered(event: DragAndDropEvent) {
        when (val self = thisDragAndDropTarget) {
            null -> lastChildDragAndDropModifierNode?.onEntered(event = event)
            else -> self.onEntered(event = event)
        }
    }

    override fun onMoved(event: DragAndDropEvent) {
        val currentChildNode: DragAndDropNode? = lastChildDragAndDropModifierNode
        val newChildNode: DragAndDropNode? =
            when {
                // Moved within child.
                currentChildNode?.contains(event.positionInRoot) == true -> currentChildNode
                // Position is now outside active child, maybe it entered a different one.
                else ->
                    firstDescendantOrNull { child ->
                        // Only dispatch to children who previously accepted the onStart gesture
                        dragAndDropManager.isInterestedTarget(child) &&
                            child.contains(event.positionInRoot)
                    }
            }

        when {
            // Left us and went to a child.
            newChildNode != null && currentChildNode == null -> {
                newChildNode.dispatchEntered(event)
                thisDragAndDropTarget?.onExited(event = event)
            }
            // Left the child and returned to us.
            newChildNode == null && currentChildNode != null -> {
                thisDragAndDropTarget?.dispatchEntered(event)
                currentChildNode.onExited(event = event)
            }
            // Left one child and entered another.
            newChildNode != currentChildNode -> {
                newChildNode?.dispatchEntered(event)
                currentChildNode?.onExited(event = event)
            }
            // Stayed in the same child.
            newChildNode != null -> newChildNode.onMoved(event = event)
            // Stayed in us.
            else -> thisDragAndDropTarget?.onMoved(event = event)
        }

        [email protected] = newChildNode
    }

    override fun onChanged(event: DragAndDropEvent) {
        when (val self = thisDragAndDropTarget) {
            null -> lastChildDragAndDropModifierNode?.onChanged(event = event)
            else -> self.onChanged(event = event)
        }
    }

    override fun onExited(event: DragAndDropEvent) {
        thisDragAndDropTarget?.onExited(event = event)
        lastChildDragAndDropModifierNode?.onExited(event = event)
        lastChildDragAndDropModifierNode = null
    }

    override fun onDrop(event: DragAndDropEvent): Boolean {
        return when (val currentChildDropTarget = lastChildDragAndDropModifierNode) {
            null -> thisDragAndDropTarget?.onDrop(event = event) ?: false
            else -> currentChildDropTarget.onDrop(event = event)
        }
    }

    override fun onEnded(event: DragAndDropEvent) = traverseSelfAndDescendants { currentNode ->
        // TODO: b/303904810 unattached nodes should not be found from an attached
        //  root drag and drop node
        if (!currentNode.node.isAttached) {
            return@traverseSelfAndDescendants SkipSubtreeAndContinueTraversal
        }
        currentNode.thisDragAndDropTarget?.onEnded(event = event)
        currentNode.thisDragAndDropTarget = null
        currentNode.lastChildDragAndDropModifierNode = null
        ContinueTraversal
    }
    // end DropTarget
}

private fun DragAndDropTarget.dispatchEntered(event: DragAndDropEvent) = run {
    // Notify of entry
    onEntered(event = event)
    // Start move
    onMoved(event = event)
}

/** Hit test for a [DragAndDropNode]. */
private fun DragAndDropNode.contains(positionInRoot: Offset): Boolean {
    if (!node.isAttached) return false
    val currentCoordinates = requireLayoutNode().coordinates
    if (!currentCoordinates.isAttached) return false

    val (x1, y1) = currentCoordinates.positionInRoot()

    // Use measured size instead of size from currentCoordinates because it might be different
    //  (eg if padding is applied)
    val x2 = x1 + size.width
    val y2 = y1 + size.height

    return positionInRoot.x in x1..x2 && positionInRoot.y in y1..y2
}

private fun  T.traverseSelfAndDescendants(
    block: (T) -> TraverseDescendantsAction
) {
    if (block(this) != ContinueTraversal) return
    traverseDescendants(block)
}

private inline fun  T.firstDescendantOrNull(
    crossinline predicate: (T) -> Boolean
): T? {
    // TODO: b/303904810 unattached nodes should not be found from an attached
    //  root drag and drop node
    if (!node.isAttached) return null
    var match: T? = null
    traverseDescendants { child ->
        if (predicate(child)) {
            match = child
            return@traverseDescendants CancelTraversal
        }
        ContinueTraversal
    }
    return match
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy