1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:OptIn(ExperimentalFoundationApi::class)
18
19 package androidx.compose.foundation.content.internal
20
21 import androidx.compose.foundation.ExperimentalFoundationApi
22 import androidx.compose.foundation.content.ReceiveContentListener
23 import androidx.compose.foundation.content.ReceiveContentNode
24 import androidx.compose.foundation.content.TransferableContent
25 import androidx.compose.foundation.content.contentReceiver
26 import androidx.compose.ui.modifier.ModifierLocalModifierNode
27 import androidx.compose.ui.modifier.modifierLocalOf
28
29 internal abstract class ReceiveContentConfiguration {
30 abstract val receiveContentListener: ReceiveContentListener
31
onCommitContentnull32 fun onCommitContent(transferableContent: TransferableContent): Boolean {
33 val remaining = receiveContentListener.onReceive(transferableContent)
34 return remaining != transferableContent
35 }
36
37 companion object {
invokenull38 operator fun invoke(
39 receiveContentListener: ReceiveContentListener
40 ): ReceiveContentConfiguration = ReceiveContentConfigurationImpl(receiveContentListener)
41 }
42 }
43
44 private data class ReceiveContentConfigurationImpl(
45 override val receiveContentListener: ReceiveContentListener
46 ) : ReceiveContentConfiguration()
47
48 internal val ModifierLocalReceiveContent = modifierLocalOf<ReceiveContentConfiguration?> { null }
49
50 /**
51 * In a [ModifierLocalModifierNode], reads the current [ReceiveContentConfiguration] that's supplied
52 * by [ModifierLocalReceiveContent] if the node is currently attached.
53 */
getReceiveContentConfigurationnull54 internal fun ModifierLocalModifierNode.getReceiveContentConfiguration() =
55 if (node.isAttached) {
56 ModifierLocalReceiveContent.current
57 } else {
58 null
59 }
60
61 /**
62 * Combines the current [ReceiveContentNode]'s [ReceiveContentConfiguration] with the parent
63 * [ReceiveContentNode]s'. It also counts the drag and drop enter/exit calls to merge drag and drop
64 * areas of parent/children [ReceiveContentListener]s. Unlike regular drop targets, ReceiveContent
65 * does not call onExit when the dragging item moves from parent node to child node since they share
66 * the same boundaries.
67 */
68 @OptIn(ExperimentalFoundationApi::class)
69 internal class DynamicReceiveContentConfiguration(val receiveContentNode: ReceiveContentNode) :
70 ReceiveContentConfiguration() {
71
72 /**
73 * A getter that returns the closest [contentReceiver] modifier configuration if this node is
74 * attached. It returns null if the node is detached or there is no parent [contentReceiver]
75 * found.
76 */
getParentReceiveContentListenernull77 private fun getParentReceiveContentListener(): ReceiveContentListener? {
78 return receiveContentNode.getReceiveContentConfiguration()?.receiveContentListener
79 }
80
81 override val receiveContentListener: ReceiveContentListener =
82 object : ReceiveContentListener {
83 /**
84 * ---------
85 * | A | | |---| | | B |
86 * ---------
87 * DragAndDrop's own callbacks do not work well with nested content. Simply, when B is
88 * nested in A, and the dragging item moves from (A\B) to (A∩B), A receives an exit
89 * event and B receives an enter event. From ReceiveContent's chaining perspective,
90 * anything that gets dropped on B is also dropped on A. Hence, A should not receive an
91 * exit event when the item moves over B.
92 *
93 * This variable counts the difference between number of times enter and exit are
94 * called, but not just on this node. ReceiveContent chaining makes sure that every
95 * enter event that B receives is also delegated A. For example;
96 * - Dragging item moves onto A.
97 * - A receives an enter event from DragAndDrop system. Enter=1, Exit=0
98 * - Dragging item moves onto B.
99 * - A receives an exit event from DragAndDrop system. Enter=1, Exit=1.
100 * - B receives an enter event from DragAndDrop system.
101 * - B delegates this to A.
102 * - A receives an enter event from B. Enter=2, Exit=1
103 *
104 * In conclusion, nodeEnterCount would be 1, meaning that this node is still hovered.
105 */
106 private var nodeEnterCount: Int = 0
107
onDragStartnull108 override fun onDragStart() {
109 // no need to call parent on this because all nodes are going to receive
110 // onStart at the same time from DragAndDrop system.
111 nodeEnterCount = 0
112 receiveContentNode.receiveContentListener.onDragStart()
113 }
114
onDragEndnull115 override fun onDragEnd() {
116 // no need to call parent on this because all nodes are going to receive
117 // onEnd at the same time from DragAndDrop system.
118 receiveContentNode.receiveContentListener.onDragEnd()
119 nodeEnterCount = 0
120 }
121
onDragEnternull122 override fun onDragEnter() {
123 nodeEnterCount++
124 if (nodeEnterCount == 1) {
125 // enter became 1 from 0. Trigger the callback.
126 receiveContentNode.receiveContentListener.onDragEnter()
127 }
128 // We need to call enter on parent because they will receive onExit from their
129 // own DragAndDropTarget.
130 getParentReceiveContentListener()?.onDragEnter()
131 }
132
onDragExitnull133 override fun onDragExit() {
134 val previous = nodeEnterCount
135 nodeEnterCount = (nodeEnterCount - 1).coerceAtLeast(0)
136 if (nodeEnterCount == 0 && previous > 0) {
137 receiveContentNode.receiveContentListener.onDragExit()
138 }
139 // We need to call exit on parent because they also received an enter from us.
140 getParentReceiveContentListener()?.onDragExit()
141 }
142
onReceivenull143 override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
144 // first let this node do whatever it wants. If it consumes everything, we can end
145 // the chain here.
146 val remaining =
147 receiveContentNode.receiveContentListener.onReceive(transferableContent)
148 ?: return null
149
150 // Check whether we have a parent node. If not, we can return the remaining here.
151 val parentReceiveContentListener =
152 getParentReceiveContentListener() ?: return remaining
153
154 // Delegate the rest to the parent node to continue the chain.
155 return parentReceiveContentListener.onReceive(remaining)
156 }
157 }
158 }
159