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