diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index 94eb91addc670..1b30fd4e3fa58 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -3286,7 +3286,6 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun convertLocalToWindowPosition-MK-Hz9U (J)J public fun convertScreenToLocalPosition-MK-Hz9U (J)J public fun convertWindowToLocalPosition-MK-Hz9U (J)J - public fun createDragAndDropManager ()Landroidx/compose/ui/platform/PlatformDragAndDropManager; public abstract fun getInputModeManager ()Landroidx/compose/ui/input/InputModeManager; public fun getMeasureDrawLayerBounds ()Z public fun getParentFocusManager ()Landroidx/compose/ui/focus/FocusManager; @@ -3299,6 +3298,7 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun isWindowTransparent ()Z public fun requestFocus ()Z public fun setPointerIcon (Landroidx/compose/ui/input/pointer/PointerIcon;)V + public fun startDrag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z } public final class androidx/compose/ui/platform/PlatformContext$Companion { @@ -3317,13 +3317,6 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext$Sem public abstract fun onSemanticsOwnerRemoved (Landroidx/compose/ui/semantics/SemanticsOwner;)V } -public abstract interface class androidx/compose/ui/platform/PlatformDragAndDropManager { - public abstract fun drag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z - public abstract fun getModifier ()Landroidx/compose/ui/Modifier; - public abstract fun isInterestedNode (Landroidx/compose/ui/draganddrop/DragAndDropModifierNode;)Z - public abstract fun registerNodeInterest (Landroidx/compose/ui/draganddrop/DragAndDropModifierNode;)V -} - public final class androidx/compose/ui/platform/PlatformInsets { public static final field $stable I public static final field Companion Landroidx/compose/ui/platform/PlatformInsets$Companion; @@ -3522,6 +3515,7 @@ public abstract interface class androidx/compose/ui/scene/ComposeScene { public abstract fun createLayer (Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;ZLandroidx/compose/runtime/CompositionContext;)Landroidx/compose/ui/scene/ComposeSceneLayer; public abstract fun getCompositionLocalContext ()Landroidx/compose/runtime/CompositionLocalContext; public abstract fun getDensity ()Landroidx/compose/ui/unit/Density; + public abstract fun getDropTarget ()Landroidx/compose/ui/scene/ComposeSceneDropTarget; public abstract fun getFocusManager ()Landroidx/compose/ui/scene/ComposeSceneFocusManager; public abstract fun getLayoutDirection ()Landroidx/compose/ui/unit/LayoutDirection; public abstract fun getSize-bOM6tXw ()Landroidx/compose/ui/unit/IntSize; @@ -3551,6 +3545,15 @@ public final class androidx/compose/ui/scene/ComposeSceneContext$Companion { public final fun getEmpty ()Landroidx/compose/ui/scene/ComposeSceneContext; } +public final class androidx/compose/ui/scene/ComposeSceneDropTarget { + public static final field $stable I + public final fun onChanged (Landroidx/compose/ui/draganddrop/DragAndDropEvent;)V + public final fun onDrop (Landroidx/compose/ui/draganddrop/DragAndDropEvent;)Z + public final fun onEntered (Landroidx/compose/ui/draganddrop/DragAndDropEvent;)Z + public final fun onExited (Landroidx/compose/ui/draganddrop/DragAndDropEvent;)V + public final fun onMoved (Landroidx/compose/ui/draganddrop/DragAndDropEvent;)V +} + public final class androidx/compose/ui/scene/ComposeSceneFocusManager { public static final field $stable I public final fun getFocusRect ()Landroidx/compose/ui/geometry/Rect; diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/AwtDragAndDropManager.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/AwtDragAndDropManager.desktop.kt index d597a89a8a0c5..47c1b3af47a43 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/AwtDragAndDropManager.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/AwtDragAndDropManager.desktop.kt @@ -16,18 +16,14 @@ package androidx.compose.ui.platform -import androidx.collection.ArraySet -import androidx.compose.ui.Modifier import androidx.compose.ui.draganddrop.AwtDragAndDropTransferable import androidx.compose.ui.draganddrop.DragAndDropEvent -import androidx.compose.ui.draganddrop.DragAndDropModifierNode -import androidx.compose.ui.draganddrop.DragAndDropNode +import androidx.compose.ui.draganddrop.DragAndDropManager import androidx.compose.ui.draganddrop.DragAndDropTransferAction import androidx.compose.ui.draganddrop.DragAndDropTransferAction.Companion.Copy import androidx.compose.ui.draganddrop.DragAndDropTransferAction.Companion.Link import androidx.compose.ui.draganddrop.DragAndDropTransferAction.Companion.Move import androidx.compose.ui.draganddrop.DragAndDropTransferData -import androidx.compose.ui.draganddrop.DragAndDropManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas @@ -35,8 +31,7 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.toAwtImage -import androidx.compose.ui.layout.onPlaced -import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection @@ -82,19 +77,13 @@ internal fun DragAndDropTransferAction.Companion.fromAwtAction( } /** - * Implements [PlatformDragAndDropManager] via the AWT drag-and-drop system. + * A drag-and-drop implementation via the AWT drag-and-drop system. */ internal class AwtDragAndDropManager( - private val rootContainer: JComponent -): PlatformDragAndDropManager { - - private val rootDragAndDropNode = DragAndDropNode { null } - - private val transferHandler = ComposeTransferHandler() - - private val dropTarget = ComposeDropTarget() - - private val interestedNodes = ArraySet() + private val rootContainer: JComponent, + private val getScene: () -> ComposeScene +) { + private val scene get() = getScene() private val density: Density get() = rootContainer.density @@ -102,14 +91,6 @@ internal class AwtDragAndDropManager( private val scale: Float get() = density.density - override val modifier: Modifier - get() = Modifier - .then(DragAndDropModifier(rootDragAndDropNode)) - .onPlaced { - rootContainer.transferHandler = transferHandler - rootContainer.dropTarget = dropTarget - } - private fun Point.toOffset(): Offset { val scale = this@AwtDragAndDropManager.scale return Offset( @@ -118,7 +99,7 @@ internal class AwtDragAndDropManager( ) } - override fun drag( + fun startDrag( transferData: DragAndDropTransferData, decorationSize: Size, drawDragDecoration: DrawScope.() -> Unit @@ -143,14 +124,6 @@ internal class AwtDragAndDropManager( return true } - override fun registerNodeInterest(node: DragAndDropModifierNode) { - interestedNodes.add(node) - } - - override fun isInterestedNode(node: DragAndDropModifierNode): Boolean { - return interestedNodes.contains(node) - } - /** * Renders the image to represent the dragged object for AWT. */ @@ -174,158 +147,151 @@ internal class AwtDragAndDropManager( ) } - private inner class ComposeTransferHandler : TransferHandler() { - - private var outgoingTransfer: OutgoingTransfer? = null - - fun startOutgoingTransfer( - transferData: DragAndDropTransferData, - dragImage: Image, - dragDecorationOffset: Offset, - ) { - outgoingTransfer = OutgoingTransfer( - transferData = transferData, - dragImage = dragImage, - dragImageOffset = PlatformAdaptations.dragImageOffset(dragDecorationOffset, scale) - ) + /** + * Receives and processes events from the [DropTarget] installed in the root component. + */ + private val dropTargetListener = object : DropTargetListener { + override fun dragEnter(dtde: DropTargetDragEvent) { + // There's no drag-start event in AWT, so start in dragEnter, and stop in dragExit + val accepted = scene.dropTarget.onEntered(DragAndDropEvent(dtde)) + if (!accepted) { + dtde.rejectDrag() + } + } - val rootContainerLocation = rootContainer.locationOnScreen - val mouseLocation = MouseInfo.getPointerInfo().location?.let { - IntOffset( - x = it.x - rootContainerLocation.x, - y = it.y - rootContainerLocation.y - ) - } ?: rootContainerLocation.let { IntOffset(it.x, it.y) } - transferHandler.exportAsDrag( - rootContainer, - MouseEvent( - rootContainer, - MouseEvent.MOUSE_DRAGGED, - System.currentTimeMillis(), - 0, - mouseLocation.x, - mouseLocation.y, - 0, - false - ), - // This seems to be ignored, and the initial action is MOVE regardless - DnDConstants.ACTION_MOVE - ) + override fun dragExit(dte: DropTargetEvent) { + scene.dropTarget.onExited(DragAndDropEvent(dte)) } - override fun createTransferable(c: JComponent?): Transferable? { - return (outgoingTransfer?.transferData?.transferable as? AwtDragAndDropTransferable) - ?.toAwtTransferable() + override fun dragOver(dtde: DropTargetDragEvent) { + scene.dropTarget.onMoved(DragAndDropEvent(dtde)) } - override fun getSourceActions(c: JComponent?): Int { - val actions = outgoingTransfer?.transferData?.supportedActions ?: emptyList() - return actions.fold( - initial = NONE, - operation = { acc, action -> acc or action.awtAction }, - ) + override fun dropActionChanged(dtde: DropTargetDragEvent) { + scene.dropTarget.onChanged(DragAndDropEvent(dtde)) } - override fun getDragImage() = outgoingTransfer?.dragImage + override fun drop(dtde: DropTargetDropEvent) { + val accepted = scene.dropTarget.onDrop(DragAndDropEvent(dtde)) + dtde.acceptDrop(dtde.dropAction) + dtde.dropComplete(accepted) + } - override fun getDragImageOffset() = outgoingTransfer?.dragImageOffset + private fun DragAndDropEvent(dragEvent: DropTargetDragEvent) = DragAndDropEvent( + nativeEvent = dragEvent, + action = DragAndDropTransferAction.fromAwtAction(dragEvent.dropAction), + positionInRootImpl = dragEvent.location.toOffset() + ) - override fun exportDone(source: JComponent?, data: Transferable?, action: Int) { - super.exportDone(source, data, action) + private fun DragAndDropEvent(dropEvent: DropTargetDropEvent) = DragAndDropEvent( + nativeEvent = dropEvent, + action = DragAndDropTransferAction.fromAwtAction(dropEvent.dropAction), + positionInRootImpl = dropEvent.location.toOffset() + ) - val transferAction = DragAndDropTransferAction.fromAwtAction(action) - outgoingTransfer?.transferData?.onTransferCompleted?.invoke(transferAction) - outgoingTransfer = null - } + private fun DragAndDropEvent(dropEvent: DropTargetEvent) = DragAndDropEvent( + nativeEvent = dropEvent, + action = null, + positionInRootImpl = Offset.Zero + ) } - private class OutgoingTransfer( - val transferData: DragAndDropTransferData, - val dragImage: Image, - val dragImageOffset: Point - ) + /** + * The [TransferHandler] installed as the root container's [JComponent.setTransferHandler] in + * order to implement drop-source functionality. + */ + val transferHandler = ComposeTransferHandler(rootContainer) - private inner class ComposeDropTarget : DropTarget( + /** + * The AWT [DropTarget] installed as the root container's [JComponent.dropTarget] in order to + * implement drop-target functionality. + */ + val dropTarget = DropTarget( rootContainer, DnDConstants.ACTION_MOVE or DnDConstants.ACTION_COPY or DnDConstants.ACTION_LINK, - object : DropTargetListener { - override fun dragEnter(dtde: DropTargetDragEvent) { - val event = DragAndDropEvent(dtde) - - // There's no drag-start event in AWT, so start in dragEnter, and stop in dragExit - val accepted = rootDragAndDropNode.acceptDragAndDropTransfer(event) - interestedNodes.forEach { it.onStarted(event) } - rootDragAndDropNode.onEntered(event) - if (!accepted) { - dtde.rejectDrag() - } - } - - override fun dragExit(dte: DropTargetEvent) { - val event = DragAndDropEvent(dte) - rootDragAndDropNode.onExited(event) - endDrag(event) - } + dropTargetListener, + true + ) +} - override fun dragOver(dtde: DropTargetDragEvent) { - rootDragAndDropNode.onMoved(DragAndDropEvent(dtde)) - } +/** + * The AWT [TransferHandler] we install in the root container in order to implement drag-source + * functionality. + */ +internal class ComposeTransferHandler(private val rootContainer: JComponent) : TransferHandler() { - override fun dropActionChanged(dtde: DropTargetDragEvent) { - rootDragAndDropNode.onChanged(DragAndDropEvent(dtde)) - } + private val scale: Float + get() = rootContainer.density.density - override fun drop(dtde: DropTargetDropEvent) { - val event = DragAndDropEvent(dtde) - dtde.acceptDrop(dtde.dropAction) - dtde.dropComplete(rootDragAndDropNode.onDrop(event)) - endDrag(event) - } + private var outgoingTransfer: OutgoingTransfer? = null - private fun endDrag(event: DragAndDropEvent) { - rootDragAndDropNode.onEnded(event) - interestedNodes.clear() - } + fun startOutgoingTransfer( + transferData: DragAndDropTransferData, + dragImage: Image, + dragDecorationOffset: Offset, + ) { + outgoingTransfer = OutgoingTransfer( + transferData = transferData, + dragImage = dragImage, + dragImageOffset = PlatformAdaptations.dragImageOffset(dragDecorationOffset, scale) + ) - private fun DragAndDropEvent(dragEvent: DropTargetDragEvent) = DragAndDropEvent( - nativeEvent = dragEvent, - action = DragAndDropTransferAction.fromAwtAction(dragEvent.dropAction), - positionInRootImpl = dragEvent.location.toOffset() + val rootContainerLocation = rootContainer.locationOnScreen + val mouseLocation = MouseInfo.getPointerInfo().location?.let { + IntOffset( + x = it.x - rootContainerLocation.x, + y = it.y - rootContainerLocation.y ) + } ?: rootContainerLocation.let { IntOffset(it.x, it.y) } + exportAsDrag( + rootContainer, + MouseEvent( + rootContainer, + MouseEvent.MOUSE_DRAGGED, + System.currentTimeMillis(), + 0, + mouseLocation.x, + mouseLocation.y, + 0, + false + ), + // This seems to be ignored, and the initial action is MOVE regardless + DnDConstants.ACTION_MOVE + ) + } - private fun DragAndDropEvent(dropEvent: DropTargetDropEvent) = DragAndDropEvent( - nativeEvent = dropEvent, - action = DragAndDropTransferAction.fromAwtAction(dropEvent.dropAction), - positionInRootImpl = dropEvent.location.toOffset() - ) + override fun createTransferable(c: JComponent?): Transferable? { + return (outgoingTransfer?.transferData?.transferable as? AwtDragAndDropTransferable) + ?.toAwtTransferable() + } - private fun DragAndDropEvent(dropEvent: DropTargetEvent) = DragAndDropEvent( - nativeEvent = dropEvent, - action = null, - positionInRootImpl = Offset.Zero - ) - }, - true - ) + override fun getSourceActions(c: JComponent?): Int { + val actions = outgoingTransfer?.transferData?.supportedActions ?: emptyList() + return actions.fold( + initial = NONE, + operation = { acc, action -> acc or action.awtAction }, + ) + } -} + override fun getDragImage() = outgoingTransfer?.dragImage -private class DragAndDropModifier( - val dragAndDropNode: DragAndDropNode -) : ModifierNodeElement() { - override fun create() = dragAndDropNode + override fun getDragImageOffset() = outgoingTransfer?.dragImageOffset - override fun update(node: DragAndDropNode) = Unit + override fun exportDone(source: JComponent?, data: Transferable?, action: Int) { + super.exportDone(source, data, action) - override fun InspectorInfo.inspectableProperties() { - name = "RootDragAndDropNode" + val transferAction = DragAndDropTransferAction.fromAwtAction(action) + outgoingTransfer?.transferData?.onTransferCompleted?.invoke(transferAction) + outgoingTransfer = null } - - override fun hashCode(): Int = dragAndDropNode.hashCode() - - override fun equals(other: Any?) = other === this } +private class OutgoingTransfer( + val transferData: DragAndDropTransferData, + val dragImage: Image, + val dragImageOffset: Point +) + /** * The AWT drag-and-drop seems to have some differences between the various OSes. This * interface encapsulates the adaptations to these differences for each OS. @@ -392,3 +358,4 @@ private interface PlatformAdaptations { } } + diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index 19a8bec88405c..844e975e60d84 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -25,11 +25,14 @@ import androidx.compose.ui.awt.AwtEventListeners import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter import androidx.compose.ui.awt.isFocusGainedHandledBySwingPanel import androidx.compose.ui.awt.runOnEDTThread +import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.key.internal import androidx.compose.ui.input.key.toComposeEvent import androidx.compose.ui.input.pointer.AwtCursor @@ -330,6 +333,8 @@ internal class ComposeSceneMediator( */ private var keyboardModifiersRequireUpdate = false + private val dragAndDropManager = AwtDragAndDropManager(container, getScene = { scene }) + init { // Transparency is used during redrawer creation that triggered by [addNotify], so // it must be set to correct value before adding to the hierarchy to handle cases @@ -343,6 +348,10 @@ internal class ComposeSceneMediator( // to react only on changes with [interopLayer]. container.addContainerListener(containerListener) + // AwtDragAndDropManager support + container.transferHandler = dragAndDropManager.transferHandler + container.dropTarget = dragAndDropManager.dropTarget + // It will be enabled dynamically. See DesktopPlatformComponent contentComponent.enableInputMethods(false) contentComponent.focusTraversalKeysEnabled = false @@ -465,6 +474,8 @@ internal class ComposeSceneMediator( container.removeContainerListener(containerListener) container.remove(contentComponent) container.remove(invisibleComponent) + container.transferHandler = null + container.dropTarget = null scene.close() skiaLayerComponent.dispose() @@ -689,12 +700,18 @@ internal class ComposeSceneMediator( return true } + override fun startDrag( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ) = dragAndDropManager.startDrag( + transferData, decorationSize, drawDragDecoration + ) + override val rootForTestListener get() = this@ComposeSceneMediator.rootForTestListener override val semanticsOwnerListener get() = this@ComposeSceneMediator.semanticsOwnerListener - - override fun createDragAndDropManager() = AwtDragAndDropManager(container) } private inner class DesktopPlatformComponent : PlatformComponent { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/DragAndDropTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/DragAndDropTest.kt new file mode 100644 index 0000000000000..13f2389f9c35b --- /dev/null +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/DragAndDropTest.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2024 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 + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.rememberWindowState +import androidx.compose.ui.window.runApplicationTest +import java.awt.Component +import java.awt.Container +import java.awt.Point +import java.awt.dnd.DnDConstants +import java.awt.dnd.DropTarget +import java.awt.dnd.DropTargetDragEvent +import java.awt.dnd.DropTargetDropEvent +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.junit.Test + +class DragAndDropTest { + + @OptIn(ExperimentalFoundationApi::class) + @Test + fun testDragAndDropTarget() = runApplicationTest { + lateinit var window: ComposeWindow + + var dragStarted = false + var dragMoved = false + var dragEnded = false + var dropHappened = false + + val dragAndDropTarget = object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + dragStarted = true + } + + override fun onMoved(event: DragAndDropEvent) { + dragMoved = true + } + + override fun onEnded(event: DragAndDropEvent) { + dragEnded = true + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + dropHappened = true + return true + } + } + + launchTestApplication { + Window( + onCloseRequest = ::exitApplication, + undecorated = true, + state = rememberWindowState(width = 200.dp, height = 100.dp) + ) { + window = this.window + + Box( + modifier = Modifier + .fillMaxSize() + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ) + ) + } + } + + awaitIdle() + + val dropTarget = assertNotNull(window.findDropTarget()) + dropTarget.sendDragEnter() + awaitIdle() + assertTrue(dragStarted) + + dropTarget.sendDragOver() + awaitIdle() + assertTrue(dragMoved) + + dropTarget.sendDrop() + awaitIdle() + assertTrue(dragEnded) + assertTrue(dropHappened) + } + + /** + * Tests that drag-target works in the presence of multiple compositions (ComposeScenes) + * attached to the same root component. + */ + @OptIn(ExperimentalFoundationApi::class) + @Test + fun dragAndDropTargetWorksAfterShowingPopup() = runApplicationTest { + lateinit var window: ComposeWindow + + var dropHappened = false + + val dragAndDropTarget = object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + dropHappened = true + return true + } + } + + var showPopup by mutableStateOf(false) + launchTestApplication { + Window( + onCloseRequest = ::exitApplication, + undecorated = true, + state = rememberWindowState(width = 200.dp, height = 100.dp) + ) { + window = this.window + + Box( + modifier = Modifier + .fillMaxSize() + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ) + ) + + if (showPopup) { + Popup( + alignment = Alignment.BottomEnd, + ) { + Box(Modifier.size(10.dp)) + } + } + } + } + + awaitIdle() + showPopup = true + awaitIdle() + + val dropTarget = assertNotNull(window.findDropTarget()) + dropTarget.sendDragEnter() + dropTarget.sendDragOver() + dropTarget.sendDrop() + awaitIdle() + assertTrue(dropHappened) + } + +} + +private fun Component.findDropTarget(): DropTarget? { + dropTarget?.let { return it } + if (this is Container) { + for (child in components) { + child.findDropTarget()?.let { return it } + } + } + return null +} + +private fun DropTarget.sendDragEnter( + mouseLocation: Point = Point(10, 10), + dropAction: Int = DnDConstants.ACTION_COPY, + srcActions: Int = DnDConstants.ACTION_COPY +) { + dragEnter(DropTargetDragEvent(dropTargetContext, mouseLocation, dropAction, srcActions)) +} + +private fun DropTarget.sendDragOver( + mouseLocation: Point = Point(10, 10), + dropAction: Int = DnDConstants.ACTION_COPY, + srcActions: Int = DnDConstants.ACTION_COPY +) { + dragOver(DropTargetDragEvent(dropTargetContext, mouseLocation, dropAction, srcActions)) +} + +private fun DropTarget.sendDrop( + mouseLocation: Point = Point(10, 10), + dropAction: Int = DnDConstants.ACTION_COPY, + srcActions: Int = DnDConstants.ACTION_COPY +) { + drop(DropTargetDropEvent(dropTargetContext, mouseLocation, dropAction, srcActions)) +} \ No newline at end of file diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index a384b6e2b673d..21e7f12f5adaa 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.Autofill import androidx.compose.ui.autofill.AutofillTree -import androidx.compose.ui.draganddrop.DragAndDropManager import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusOwner import androidx.compose.ui.focus.FocusOwnerImpl @@ -65,10 +64,10 @@ import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformRootForTest import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.RenderNodeLayer -import androidx.compose.ui.platform.asDragAndDropManager import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneInputHandler import androidx.compose.ui.scene.ComposeScenePointer +import androidx.compose.ui.scene.OwnerDragAndDropManager import androidx.compose.ui.semantics.EmptySemanticsElement import androidx.compose.ui.semantics.EmptySemanticsModifier import androidx.compose.ui.semantics.SemanticsOwner @@ -123,8 +122,9 @@ internal class RootNodeOwner( platformContext.parentFocusManager.clearFocus(true) }, ) - private val dragAndDropManager: DragAndDropManager = - platformContext.createDragAndDropManager().asDragAndDropManager() + + val dragAndDropManager = OwnerDragAndDropManager(platformContext) + private val rootSemanticsNode = EmptySemanticsModifier() private val rootModifier = EmptySemanticsElement(rootSemanticsNode) @@ -330,7 +330,7 @@ internal class RootNodeOwner( ): Nothing { awaitCancellation() } - override val dragAndDropManager: DragAndDropManager = this@RootNodeOwner.dragAndDropManager + override val dragAndDropManager = this@RootNodeOwner.dragAndDropManager override val pointerIconService = PointerIconServiceImpl() override val focusOwner get() = this@RootNodeOwner.focusOwner override val windowInfo get() = platformContext.windowInfo diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index 7eb4d0eee5f74..0305a6d288768 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -20,8 +20,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropModifierNode import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager @@ -115,7 +113,14 @@ interface PlatformContext { val parentFocusManager: FocusManager get() = EmptyFocusManager fun requestFocus(): Boolean = true - fun createDragAndDropManager(): PlatformDragAndDropManager = EmptyDragAndDropManager + /** + * Starts a drag-and-drop session with the Compose app as the source of the transfer. + */ + fun startDrag( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ): Boolean = false /** * The listener to track [RootForTest]s. @@ -235,23 +240,6 @@ private object EmptyFocusManager : FocusManager { override fun moveFocus(focusDirection: FocusDirection) = false } -private object EmptyDragAndDropManager : PlatformDragAndDropManager { - override val modifier: Modifier - get() = Modifier - - override fun drag( - transferData: DragAndDropTransferData, - decorationSize: Size, - drawDragDecoration: DrawScope.() -> Unit - ): Boolean { - return false - } - - override fun registerNodeInterest(node: DragAndDropModifierNode) = Unit - - override fun isInterestedNode(node: DragAndDropModifierNode): Boolean = false -} - /** * Helper delegate to re-send missing events to a new listener. */ diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformDragAndDropManager.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformDragAndDropManager.kt deleted file mode 100644 index 2bbf62bc9e5d9..0000000000000 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformDragAndDropManager.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2024 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.platform - -import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropManager -import androidx.compose.ui.draganddrop.DragAndDropModifierNode -import androidx.compose.ui.draganddrop.DragAndDropTransferData -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.node.RootNodeOwner - -/** - * The interface for platform implementations of [DragAndDropManager]. - * - * This is needed because [DragAndDropManager] itself is `internal`, and therefore can't be - * implemented by 3rd-party code. The solution is for [RootNodeOwner] to implement the internal - * [DragAndDropManager] by delegating to a public [PlatformDragAndDropManager] provided by - * [PlatformContext]. - * - * For documentation of the methods of this interface refer to [DragAndDropManager]. - */ -@InternalComposeUiApi -interface PlatformDragAndDropManager { - val modifier: Modifier - - fun drag( - transferData: DragAndDropTransferData, - decorationSize: Size, - drawDragDecoration: DrawScope.() -> Unit, - ): Boolean - - fun registerNodeInterest(node: DragAndDropModifierNode) - - fun isInterestedNode(node: DragAndDropModifierNode): Boolean -} - - -/** - * Returns a [DragAndDropManager] that delegates to `this` [PlatformDragAndDropManager]. - */ -internal fun PlatformDragAndDropManager.asDragAndDropManager() = object : DragAndDropManager { - override val modifier: Modifier - get() = this@asDragAndDropManager.modifier - - override fun drag( - transferData: DragAndDropTransferData, - decorationSize: Size, - drawDragDecoration: DrawScope.() -> Unit - ): Boolean { - return this@asDragAndDropManager.drag(transferData, decorationSize, drawDragDecoration) - } - - override fun registerNodeInterest(node: DragAndDropModifierNode) { - this@asDragAndDropManager.registerNodeInterest(node) - } - - override fun isInterestedNode(node: DragAndDropModifierNode): Boolean { - return this@asDragAndDropManager.isInterestedNode(node) - } -} \ No newline at end of file diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt index b2b9a15733c75..6f6740dc9d991 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/CanvasLayersComposeScene.skiko.kt @@ -150,6 +150,10 @@ private class CanvasLayersComposeSceneImpl( focusOwner = { focusedOwner.focusOwner } ) + override val dropTarget = ComposeSceneDropTarget( + activeDragAndDropManager = { focusedOwner.dragAndDropManager } + ) + private val layers = mutableListOf() private val _layersCopyCache = CopiedList { it.addAll(layers) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt index f4bc3e4ca803b..f71df1e5db8a0 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt @@ -117,6 +117,12 @@ interface ComposeScene { */ val focusManager: ComposeSceneFocusManager + /** + * The object through which drag-and-drop implementations report drop-target events to the + * scene. + */ + val dropTarget: ComposeSceneDropTarget + /** * Close all resources and subscriptions. Not calling this method when [ComposeScene] is no * longer needed will cause a memory leak. diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/OwnerDragAndDropManager.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/OwnerDragAndDropManager.skiko.kt new file mode 100644 index 0000000000000..8b9f98e907105 --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/OwnerDragAndDropManager.skiko.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024 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.scene + +import androidx.collection.ArraySet +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropManager +import androidx.compose.ui.draganddrop.DragAndDropModifierNode +import androidx.compose.ui.draganddrop.DragAndDropNode +import androidx.compose.ui.draganddrop.DragAndDropTransferData +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.PlatformContext + +/** + * The object provided by [ComposeScene] to allow reporting drop-target events to it. + */ +@InternalComposeUiApi +class ComposeSceneDropTarget internal constructor( + private val activeDragAndDropManager: () -> OwnerDragAndDropManager, +) { + fun onEntered(event: DragAndDropEvent): Boolean = + activeDragAndDropManager().onEntered(event) + + fun onExited(event: DragAndDropEvent): Unit = + activeDragAndDropManager().onExited(event) + + fun onMoved(event: DragAndDropEvent): Unit = + activeDragAndDropManager().onMoved(event) + + fun onChanged(event: DragAndDropEvent): Unit = + activeDragAndDropManager().onChanged(event) + + fun onDrop(event: DragAndDropEvent): Boolean = + activeDragAndDropManager().onDrop(event) +} + +/** + * The actual [DragAndDropManager] implementation tied to a specific + * [androidx.compose.ui.node.RootNodeOwner]. + */ +internal class OwnerDragAndDropManager( + private val platformContext: PlatformContext +) : DragAndDropManager { + private val rootDragAndDropNode = DragAndDropNode { null } + private val interestedNodes = ArraySet() + + override val modifier: Modifier = DragAndDropModifier(rootDragAndDropNode) + + override fun drag( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ): Boolean { + return platformContext.startDrag(transferData, decorationSize, drawDragDecoration) + } + + override fun registerNodeInterest(node: DragAndDropModifierNode) { + interestedNodes.add(node) + } + + override fun isInterestedNode(node: DragAndDropModifierNode): Boolean { + return interestedNodes.contains(node) + } + + fun onEntered(event: DragAndDropEvent): Boolean { + val accepted = rootDragAndDropNode.acceptDragAndDropTransfer(event) + interestedNodes.forEach { it.onStarted(event) } + rootDragAndDropNode.onEntered(event) + return accepted + } + + fun onExited(event: DragAndDropEvent) { + rootDragAndDropNode.onExited(event) + endDrag(event) + } + + fun onMoved(event: DragAndDropEvent) { + rootDragAndDropNode.onMoved(event) + } + + fun onChanged(event: DragAndDropEvent) { + rootDragAndDropNode.onChanged(event) + } + + fun onDrop(event: DragAndDropEvent): Boolean { + val accepted = rootDragAndDropNode.onDrop(event) + endDrag(event) + return accepted + } + + private fun endDrag(event: DragAndDropEvent) { + rootDragAndDropNode.onEnded(event) + interestedNodes.clear() + } +} + +private class DragAndDropModifier( + val dragAndDropNode: DragAndDropNode +) : ModifierNodeElement() { + override fun create() = dragAndDropNode + + override fun update(node: DragAndDropNode) = Unit + + override fun InspectorInfo.inspectableProperties() { + name = "RootDragAndDropNode" + } + + override fun hashCode(): Int = dragAndDropNode.hashCode() + + override fun equals(other: Any?) = other === this +} diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt index 3981892206ee8..169f1b2830d5b 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/PlatformLayersComposeScene.skiko.kt @@ -90,7 +90,7 @@ private class PlatformLayersComposeSceneImpl( composeSceneContext = composeSceneContext, invalidate = invalidate ) { - private val mainOwner by lazy { + private val mainOwner: RootNodeOwner by lazy { RootNodeOwner( density = density, layoutDirection = layoutDirection, @@ -130,6 +130,10 @@ private class PlatformLayersComposeSceneImpl( focusOwner = { mainOwner.focusOwner } ) + override val dropTarget = ComposeSceneDropTarget( + activeDragAndDropManager = { mainOwner.dragAndDropManager } + ) + init { onOwnerAppended(mainOwner) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 2b80055f4f669..5251e63e3ccdf 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -21,8 +21,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropModifierNode import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -46,7 +44,6 @@ import androidx.compose.ui.platform.EmptyViewConfiguration import androidx.compose.ui.platform.LocalLayoutMargins import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformContext -import androidx.compose.ui.platform.PlatformDragAndDropManager import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.platform.UIKitTextInputService @@ -742,27 +739,12 @@ internal class ComposeSceneMediator( positionOnScreen ) - override fun createDragAndDropManager(): PlatformDragAndDropManager { - return object : PlatformDragAndDropManager { - override val modifier: Modifier - get() = Modifier - - override fun drag( - transferData: DragAndDropTransferData, - decorationSize: Size, - drawDragDecoration: DrawScope.() -> Unit - ): Boolean { - TODO("Drag&drop isn't implemented") - } - - override fun registerNodeInterest(node: DragAndDropModifierNode) { - TODO("Drag&drop isn't implemented") - } - - override fun isInterestedNode(node: DragAndDropModifierNode): Boolean { - TODO("Drag&drop isn't implemented") - } - } + override fun startDrag( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ): Boolean { + TODO("Drag&drop isn't implemented") } override val measureDrawLayerBounds get() = this@ComposeSceneMediator.measureDrawLayerBounds diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt index 16cbbaf95d807..8a1a918ad9584 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt @@ -22,8 +22,6 @@ import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.LocalSystemTheme -import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropModifierNode import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.events.EventTargetListener import androidx.compose.ui.geometry.Offset @@ -33,10 +31,8 @@ import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager -import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.toComposeEvent import androidx.compose.ui.input.pointer.BrowserCursor -import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerButtons import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerIcon @@ -49,7 +45,6 @@ import androidx.compose.ui.native.ComposeLayer import androidx.compose.ui.platform.DefaultInputModeManager import androidx.compose.ui.platform.LocalInternalViewModelStoreOwner import androidx.compose.ui.platform.PlatformContext -import androidx.compose.ui.platform.PlatformDragAndDropManager import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WebTextInputService import androidx.compose.ui.platform.WindowInfoImpl @@ -218,27 +213,12 @@ internal class ComposeWindow( } } - override fun createDragAndDropManager(): PlatformDragAndDropManager { - return object : PlatformDragAndDropManager { - override val modifier: Modifier - get() = Modifier - - override fun drag( - transferData: DragAndDropTransferData, - decorationSize: Size, - drawDragDecoration: DrawScope.() -> Unit - ): Boolean { - TODO("Drag&drop isn't implemented") - } - - override fun registerNodeInterest(node: DragAndDropModifierNode) { - TODO("Drag&drop isn't implemented") - } - - override fun isInterestedNode(node: DragAndDropModifierNode): Boolean { - TODO("Drag&drop isn't implemented") - } - } + override fun startDrag( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ): Boolean { + TODO("Drag&drop isn't implemented") } }