From d0de9fdf7345ae18054b0f8ea4152109962586be Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 30 Jan 2023 17:36:50 +0100 Subject: [PATCH 01/31] Introduce external file drag and drop support for desktop --- .../compose/desktop/examples/dnd/Main.jvm.kt | 75 +++++ .../compose/ui/dnd/ExternalDrag.desktop.kt | 311 ++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt create mode 100644 compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt new file mode 100644 index 0000000000000..fdb5f9f2a0fdc --- /dev/null +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -0,0 +1,75 @@ +/* + * 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.desktop.examples.dnd + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.dnd.onExternalFileDrag +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication + +fun main() = singleWindowApplication( + title = "External dnd demo" +) { + MaterialTheme { + val size = 200.dp + + var isDragging by remember { mutableStateOf(false) } + var text by remember { mutableStateOf(null) } + + Column(modifier = Modifier.padding(20.dp)) { + Box( + modifier = Modifier.size(size, size) + .background( + when { + isDragging -> Color.Green + text != null -> Color.White + else -> Color.Red + } + ) + .onExternalFileDrag( + onDragStart = { + isDragging = true + }, + onDragCancel = { + isDragging = false + }, + onDrag = { + + }, + onDrop = { + text = it.toString() + isDragging = false + }) + ) { + Text(text ?: "Try to drag some files here", modifier = Modifier.align(Alignment.Center)) + } + } + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt new file mode 100644 index 0000000000000..3341482ca5feb --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -0,0 +1,311 @@ +/* + * 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.dnd + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.window.LocalWindow +import java.awt.Point +import java.awt.Window +import java.awt.datatransfer.DataFlavor +import java.awt.dnd.DnDConstants +import java.awt.dnd.DropTarget +import java.awt.dnd.DropTargetDragEvent +import java.awt.dnd.DropTargetDropEvent +import java.awt.dnd.DropTargetEvent +import java.awt.dnd.DropTargetListener +import java.io.File + +@Composable +fun Modifier.onExternalFileDrag( + enabled: Boolean = true, + onDragStart: () -> Unit = {}, + onDrag: (Offset) -> Unit = {}, + onDragCancel: () -> Unit = {}, + onDrop: (List) -> Unit = {}, +): Modifier = composed { + if (!enabled) { + return@composed Modifier + } + val window = LocalWindow.current ?: return@composed Modifier + + var componentCoordinates by remember { mutableStateOf(null) } + val boundsInWindow = componentCoordinates?.boundsInWindow() + + var componentDragHandleId by remember { mutableStateOf(null) } + + // components drag handlers depend on the component position, + // so we should reinstall handlers with a new component position when it was changed + DisposableEffect(window, boundsInWindow) { + val currentComponentCoordinates = componentCoordinates ?: return@DisposableEffect onDispose { } + when (val currentDropTarget = window.dropTarget) { + is AwtWindowDropTarget -> { + // if our drop target is already assigned simply add new drag handler for the current component + componentDragHandleId = currentDropTarget.installComponentDragHandler( + currentComponentCoordinates, onDragStart, onDrag, onDragCancel, onDrop + ) + } + + null -> { + // drop target is not installed for the window, so assign it and add new drag handler for the current component + val newDropTarget = AwtWindowDropTarget(window) + componentDragHandleId = newDropTarget.installComponentDragHandler( + currentComponentCoordinates, onDragStart, onDrag, onDragCancel, onDrop + ) + window.dropTarget = newDropTarget + } + + else -> { + error("Window already has unknown external dnd handler, cannot attach onExternalFileDrag") + } + } + + onDispose { + val dropTarget = window.dropTarget + if (dropTarget is AwtWindowDropTarget) { + val handleIdToRemove = componentDragHandleId + if (handleIdToRemove != null) { + dropTarget.stopDragHandling(handleIdToRemove) + } + } + } + } + + Modifier + .onGloballyPositioned { + componentCoordinates = it + } +} + +/** + * Provides a way to subscribe on external drag for given [window] using [installComponentDragHandler] + * + * [Window] allows having only one [DropTarget], so this is the main [DropTarget] that handles all the drag subscriptions + */ +private class AwtWindowDropTarget( + private val window: Window +) : DropTarget(window, DnDConstants.ACTION_MOVE, null, true) { + private var idsCounter = 0 + + // all components that are subscribed to external drag and drop for the window + private val handlers = mutableMapOf() + + // drag coordinates used to detect that drag entered/exited components + private var windowDragCoordinates: Offset? = null + + init { + addDropTargetListener( + AwtWindowDragTargetListener( + // notify components on window border that drag is started. + onDragEnterWindow = { awtPoint -> + val newWindowDragCoordinates = awtPoint.windowOffset() + for ((_, handler) in handlers) { + val isInside = + isExternalDragInsideComponent(handler.componentCoordinates, newWindowDragCoordinates) + if (isInside) { + handler.onDragStart() + } + } + windowDragCoordinates = newWindowDragCoordinates + }, + // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them + onDragInsideWindow = { awtPoint -> + val newWindowDragCoordinates = awtPoint.windowOffset() + for ((_, handler) in handlers) { + val componentCoordinates = handler.componentCoordinates + val oldDragCoordinates = windowDragCoordinates + + val wasDragInside = isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + val newIsDragInside = + isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) + + if (!wasDragInside && newIsDragInside) { + handler.onDragStart() + } + + if (wasDragInside && !newIsDragInside) { + handler.onDragCancel() + } + + if (newIsDragInside) { + handler.onDrag(componentCoordinates.windowToLocal(newWindowDragCoordinates)) + } + } + windowDragCoordinates = newWindowDragCoordinates + }, + // notify components on window border drag exited window + onDragExit = { + for ((_, handler) in handlers) { + val componentCoordinates = handler.componentCoordinates + val oldDragCoordinates = windowDragCoordinates + val wasDragInside = isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + if (wasDragInside) { + handler.onDragCancel() + } + } + windowDragCoordinates = null + }, + // notify all components under the pointer that drop happened + onDrop = { + var anyDrops = false + for ((_, handler) in handlers) { + if (isExternalDragInsideComponent(handler.componentCoordinates, windowDragCoordinates)) { + handler.onDrop(it) + anyDrops = true + } + } + windowDragCoordinates = null + // tell swing whether some components accepted the drop + return@AwtWindowDragTargetListener anyDrops + } + ) + ) + } + + override fun setActive(isActive: Boolean) { + super.setActive(isActive) + if (!isActive) { + windowDragCoordinates = null + } + } + + /** + * Subscribes on drag events for [window]. + * If drag is going and component is under pointer [onDragStart] can be called synchronously. + * + * @param componentCoordinates coordinates of the component used to properly detect when drag entered/exited component + * @return handler id that can be used later to remove subscription using [stopDragHandling] + */ + fun installComponentDragHandler( + componentCoordinates: LayoutCoordinates, + onDragStart: () -> Unit, + onDrag: (Offset) -> Unit, + onDragCancel: () -> Unit, + onDrop: (List) -> Unit + ): Int { + isActive = true + + handlers[idsCounter] = ComponentDragHandler(componentCoordinates, onDragStart, onDrag, onDragCancel, onDrop) + + if (isExternalDragInsideComponent(componentCoordinates, windowDragCoordinates)) { + onDragStart() + } + return idsCounter++ + } + + /** + * Unsubscribes handler with [handleId]. + * Calls [ComponentDragHandler.onDragCancel] if drag is going and handler's component is under pointer + * + * Disable drag handling for [window] if there are no more handlers. + * + * @param handleId id provided by [installComponentDragHandler] function + */ + fun stopDragHandling(handleId: Int) { + val handler = handlers[handleId] + if (handler != null && isExternalDragInsideComponent(handler.componentCoordinates, windowDragCoordinates)) { + handler.onDragCancel() + } + handlers.remove(handleId) + + if (handlers.isEmpty()) { + isActive = false + } + } + + private fun Point.windowOffset(): Offset { + val transform = window.graphicsConfiguration.defaultTransform + val offsetX = (x - window.insets.left) * transform.scaleX.toFloat() + val offsetY = (y - window.insets.top) * transform.scaleY.toFloat() + + return Offset(offsetX, offsetY) + } + + + private class ComponentDragHandler( + val componentCoordinates: LayoutCoordinates, + val onDragStart: () -> Unit, + val onDrag: (Offset) -> Unit, + val onDragCancel: () -> Unit, + val onDrop: (List) -> Unit + ) + + companion object { + private fun isExternalDragInsideComponent( + componentCoordinates: LayoutCoordinates?, + windowDragCoordinates: Offset? + ): Boolean { + if (componentCoordinates == null || windowDragCoordinates == null) { + return false + } + + return componentCoordinates.boundsInWindow().contains(windowDragCoordinates) + } + } +} + +private class AwtWindowDragTargetListener( + private val onDragEnterWindow: (Point) -> Unit, + private val onDragInsideWindow: (Point) -> Unit, + private val onDragExit: () -> Unit, + private val onDrop: (List) -> Boolean, +) : DropTargetListener { + override fun dragEnter(dtde: DropTargetDragEvent) { + onDragEnterWindow(dtde.location) + } + + override fun dragOver(dtde: DropTargetDragEvent) { + onDragInsideWindow(dtde.location) + } + + override fun dropActionChanged(dtde: DropTargetDragEvent) { + // Should we notify about it? + } + + override fun dragExit(dte: DropTargetEvent) { + onDragExit() + } + + override fun drop(dtde: DropTargetDropEvent) { + dtde.acceptDrop(dtde.dropAction) + + val transferable = dtde.transferable + try { + val data = (transferable.getTransferData(DataFlavor.javaFileListFlavor) as? List<*>) ?: run { + onDragExit() + dtde.dropComplete(false) + return + } + onDrop(data.filterIsInstance()) + dtde.dropComplete(true) + return + } catch (e: Exception) { + onDragExit() + dtde.dropComplete(false) + } + } +} \ No newline at end of file From c67d3e2aa08f85f717d760942971f8a1fb95fad4 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 30 Jan 2023 23:09:44 +0100 Subject: [PATCH 02/31] Introduce more drop data types for external drag support --- .../compose/desktop/examples/dnd/Main.jvm.kt | 21 ++++-- .../compose/ui/dnd/ExternalDrag.desktop.kt | 69 ++++++++++++++++--- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index fdb5f9f2a0fdc..0e52b2c817182 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -16,6 +16,7 @@ package androidx.compose.desktop.examples.dnd +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,8 +30,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.dnd.onExternalFileDrag +import androidx.compose.ui.dnd.DropData +import androidx.compose.ui.dnd.onExternalDrag import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication @@ -42,6 +45,7 @@ fun main() = singleWindowApplication( var isDragging by remember { mutableStateOf(false) } var text by remember { mutableStateOf(null) } + var painter by remember { mutableStateOf(null) } Column(modifier = Modifier.padding(20.dp)) { Box( @@ -53,7 +57,7 @@ fun main() = singleWindowApplication( else -> Color.Red } ) - .onExternalFileDrag( + .onExternalDrag( onDragStart = { isDragging = true }, @@ -63,12 +67,19 @@ fun main() = singleWindowApplication( onDrag = { }, - onDrop = { - text = it.toString() + onDrop = { data -> + text = data.toString() + if (data is DropData.Image) { + painter = data.painter + } isDragging = false }) ) { - Text(text ?: "Try to drag some files here", modifier = Modifier.align(Alignment.Center)) + Text(text ?: "Try to drag some files or image here", modifier = Modifier.align(Alignment.Center)) + val currentPainter = painter + if (currentPainter != null) { + Image(currentPainter, contentDescription = "Pasted Image") + } } } } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 3341482ca5feb..4e84123d478f2 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -25,28 +25,42 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toPainter import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.window.LocalWindow +import java.awt.Image import java.awt.Point import java.awt.Window import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.DataFlavor.selectBestTextFlavor +import java.awt.datatransfer.Transferable import java.awt.dnd.DnDConstants import java.awt.dnd.DropTarget import java.awt.dnd.DropTargetDragEvent import java.awt.dnd.DropTargetDropEvent import java.awt.dnd.DropTargetEvent import java.awt.dnd.DropTargetListener +import java.awt.image.BufferedImage import java.io.File +sealed interface DropData { + data class FilesList(val rawUris: List) : DropData + + data class Image(val painter: Painter) : DropData + + data class Text(val content: String, val mimeType: String?) : DropData +} + @Composable -fun Modifier.onExternalFileDrag( +fun Modifier.onExternalDrag( enabled: Boolean = true, onDragStart: () -> Unit = {}, onDrag: (Offset) -> Unit = {}, onDragCancel: () -> Unit = {}, - onDrop: (List) -> Unit = {}, + onDrop: (DropData) -> Unit = {}, ): Modifier = composed { if (!enabled) { return@composed Modifier @@ -80,7 +94,7 @@ fun Modifier.onExternalFileDrag( } else -> { - error("Window already has unknown external dnd handler, cannot attach onExternalFileDrag") + error("Window already has unknown external dnd handler, cannot attach onExternalDrag") } } @@ -205,7 +219,7 @@ private class AwtWindowDropTarget( onDragStart: () -> Unit, onDrag: (Offset) -> Unit, onDragCancel: () -> Unit, - onDrop: (List) -> Unit + onDrop: (DropData) -> Unit ): Int { isActive = true @@ -251,7 +265,7 @@ private class AwtWindowDropTarget( val onDragStart: () -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, - val onDrop: (List) -> Unit + val onDrop: (DropData) -> Unit ) companion object { @@ -272,7 +286,7 @@ private class AwtWindowDragTargetListener( private val onDragEnterWindow: (Point) -> Unit, private val onDragInsideWindow: (Point) -> Unit, private val onDragExit: () -> Unit, - private val onDrop: (List) -> Boolean, + private val onDrop: (DropData) -> Boolean, ) : DropTargetListener { override fun dragEnter(dtde: DropTargetDragEvent) { onDragEnterWindow(dtde.location) @@ -295,12 +309,12 @@ private class AwtWindowDragTargetListener( val transferable = dtde.transferable try { - val data = (transferable.getTransferData(DataFlavor.javaFileListFlavor) as? List<*>) ?: run { + val dropData = transferable.dropData() ?: run { onDragExit() dtde.dropComplete(false) return } - onDrop(data.filterIsInstance()) + onDrop(dropData) dtde.dropComplete(true) return } catch (e: Exception) { @@ -308,4 +322,43 @@ private class AwtWindowDragTargetListener( dtde.dropComplete(false) } } + + private fun Transferable.dropData(): DropData? { + val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) + + return when { + isDataFlavorSupported(DataFlavor.javaFileListFlavor) -> { + val files = getTransferData(DataFlavor.javaFileListFlavor) as? List<*> ?: return null + DropData.FilesList(files.filterIsInstance().map { it.toURI().toString() }) + } + + isDataFlavorSupported(DataFlavor.imageFlavor) -> { + val image = getTransferData(DataFlavor.imageFlavor) as? Image ?: return null + DropData.Image(image.painter()) + } + + bestTextFlavor != null -> { + val reader = bestTextFlavor.getReaderForText(this) ?: return null + DropData.Text(content = reader.readText(), mimeType = bestTextFlavor.mimeType) + } + + else -> null + } + } + + private fun Image.painter(): Painter { + if (this is BufferedImage) { + return this.toPainter() + } + val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_ARGB) + + val g2 = bufferedImage.createGraphics() + try { + g2.drawImage(this, 0, 0, null) + } finally { + g2.dispose() + } + + return bufferedImage.toPainter() + } } \ No newline at end of file From 2cf90553e3b03d77968e145f0cfc400e9db4d6bf Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 30 Jan 2023 23:37:47 +0100 Subject: [PATCH 03/31] Add docs to external drag API --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 4e84123d478f2..797ffc82f2353 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -46,14 +46,36 @@ import java.awt.dnd.DropTargetListener import java.awt.image.BufferedImage import java.io.File +/** + * Represent data types drag and dropped to an application from outside. + */ sealed interface DropData { + /** + * Represents list of files drag and dropped to an application in a raw [java.net.URI] format. + */ data class FilesList(val rawUris: List) : DropData + /** + * Represents an image drag and dropped to an application. + */ data class Image(val painter: Painter) : DropData + /** + * Represent text drag and dropped to an application. + * + * @param mimeType mimeType of the [content] such as "text/plain", "text/html", etc. + */ data class Text(val content: String, val mimeType: String?) : DropData } +/** + * Adds detector of external drag and drop (e.g. files DnD from Finder to an application) + * + * @param onDragStart will be called when the pointer with external content entered the component. + * @param onDrag will be called for all drag events inside the component. + * @param onDrop is called when the pointer is released with [DropData] the pointer held. + * @param onDragCancel is called if the pointer exited the component bounds or unknown data was dropped. + */ @Composable fun Modifier.onExternalDrag( enabled: Boolean = true, From e07fa2336f3f9fa1f79b724c1a78fa0d7ac37ce0 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 30 Jan 2023 23:44:20 +0100 Subject: [PATCH 04/31] Add offset to dragStart callback like in onDrag function --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 797ffc82f2353..6fc1296182ffc 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -79,7 +79,7 @@ sealed interface DropData { @Composable fun Modifier.onExternalDrag( enabled: Boolean = true, - onDragStart: () -> Unit = {}, + onDragStart: (Offset) -> Unit = {}, onDrag: (Offset) -> Unit = {}, onDragCancel: () -> Unit = {}, onDrop: (DropData) -> Unit = {}, @@ -163,7 +163,7 @@ private class AwtWindowDropTarget( val isInside = isExternalDragInsideComponent(handler.componentCoordinates, newWindowDragCoordinates) if (isInside) { - handler.onDragStart() + handler.onDragStart(calculateOffset(handler.componentCoordinates, newWindowDragCoordinates)) } } windowDragCoordinates = newWindowDragCoordinates @@ -180,7 +180,7 @@ private class AwtWindowDropTarget( isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) if (!wasDragInside && newIsDragInside) { - handler.onDragStart() + handler.onDragStart(calculateOffset(componentCoordinates, newWindowDragCoordinates)) } if (wasDragInside && !newIsDragInside) { @@ -238,7 +238,7 @@ private class AwtWindowDropTarget( */ fun installComponentDragHandler( componentCoordinates: LayoutCoordinates, - onDragStart: () -> Unit, + onDragStart: (Offset) -> Unit, onDrag: (Offset) -> Unit, onDragCancel: () -> Unit, onDrop: (DropData) -> Unit @@ -248,7 +248,7 @@ private class AwtWindowDropTarget( handlers[idsCounter] = ComponentDragHandler(componentCoordinates, onDragStart, onDrag, onDragCancel, onDrop) if (isExternalDragInsideComponent(componentCoordinates, windowDragCoordinates)) { - onDragStart() + onDragStart(calculateOffset(componentCoordinates, windowDragCoordinates!!)) } return idsCounter++ } @@ -284,7 +284,7 @@ private class AwtWindowDropTarget( private class ComponentDragHandler( val componentCoordinates: LayoutCoordinates, - val onDragStart: () -> Unit, + val onDragStart: (Offset) -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, val onDrop: (DropData) -> Unit @@ -301,6 +301,13 @@ private class AwtWindowDropTarget( return componentCoordinates.boundsInWindow().contains(windowDragCoordinates) } + + private fun calculateOffset( + componentCoordinates: LayoutCoordinates, + windowDragCoordinates: Offset + ): Offset { + return componentCoordinates.windowToLocal(windowDragCoordinates) + } } } From b62ff6f3ae7e4b3d2a915ea12b02f1e3577ba1b0 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Tue, 31 Jan 2023 21:24:42 +0100 Subject: [PATCH 05/31] Move awt point -> offset logic closer to drag event --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 6fc1296182ffc..4b7eca954de64 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.window.LocalWindow +import androidx.compose.ui.window.density import java.awt.Image import java.awt.Point import java.awt.Window @@ -156,9 +157,9 @@ private class AwtWindowDropTarget( init { addDropTargetListener( AwtWindowDragTargetListener( + window, // notify components on window border that drag is started. - onDragEnterWindow = { awtPoint -> - val newWindowDragCoordinates = awtPoint.windowOffset() + onDragEnterWindow = { newWindowDragCoordinates -> for ((_, handler) in handlers) { val isInside = isExternalDragInsideComponent(handler.componentCoordinates, newWindowDragCoordinates) @@ -169,8 +170,7 @@ private class AwtWindowDropTarget( windowDragCoordinates = newWindowDragCoordinates }, // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them - onDragInsideWindow = { awtPoint -> - val newWindowDragCoordinates = awtPoint.windowOffset() + onDragInsideWindow = { newWindowDragCoordinates -> for ((_, handler) in handlers) { val componentCoordinates = handler.componentCoordinates val oldDragCoordinates = windowDragCoordinates @@ -273,14 +273,6 @@ private class AwtWindowDropTarget( } } - private fun Point.windowOffset(): Offset { - val transform = window.graphicsConfiguration.defaultTransform - val offsetX = (x - window.insets.left) * transform.scaleX.toFloat() - val offsetY = (y - window.insets.top) * transform.scaleY.toFloat() - - return Offset(offsetX, offsetY) - } - private class ComponentDragHandler( val componentCoordinates: LayoutCoordinates, @@ -312,17 +304,28 @@ private class AwtWindowDropTarget( } private class AwtWindowDragTargetListener( - private val onDragEnterWindow: (Point) -> Unit, - private val onDragInsideWindow: (Point) -> Unit, + private val window: Window, + private val onDragEnterWindow: (Offset) -> Unit, + private val onDragInsideWindow: (Offset) -> Unit, private val onDragExit: () -> Unit, private val onDrop: (DropData) -> Boolean, ) : DropTargetListener { + private val density = window.density.density + override fun dragEnter(dtde: DropTargetDragEvent) { - onDragEnterWindow(dtde.location) + onDragEnterWindow(dtde.location.windowOffset()) } override fun dragOver(dtde: DropTargetDragEvent) { - onDragInsideWindow(dtde.location) + onDragInsideWindow(dtde.location.windowOffset()) + } + + // takes title bar and other insets into account + private fun Point.windowOffset(): Offset { + val offsetX = (x - window.insets.left) * density + val offsetY = (y - window.insets.top) * density + + return Offset(offsetX, offsetY) } override fun dropActionChanged(dtde: DropTargetDragEvent) { From decc04aaee16a3f35aa46269d187ee69b02a3269 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Tue, 31 Jan 2023 22:20:32 +0100 Subject: [PATCH 06/31] Introduce tests for external drag support --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 165 ++++++----- .../compose/ui/dnd/ExternalDragTest.kt | 266 ++++++++++++++++++ 2 files changed, 360 insertions(+), 71 deletions(-) create mode 100644 compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 4b7eca954de64..736db8cdeeeec 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -142,8 +142,10 @@ fun Modifier.onExternalDrag( * Provides a way to subscribe on external drag for given [window] using [installComponentDragHandler] * * [Window] allows having only one [DropTarget], so this is the main [DropTarget] that handles all the drag subscriptions + * + * @VisibleForTesting */ -private class AwtWindowDropTarget( +internal class AwtWindowDropTarget( private val window: Window ) : DropTarget(window, DnDConstants.ACTION_MOVE, null, true) { private var idsCounter = 0 @@ -154,72 +156,92 @@ private class AwtWindowDropTarget( // drag coordinates used to detect that drag entered/exited components private var windowDragCoordinates: Offset? = null - init { - addDropTargetListener( - AwtWindowDragTargetListener( - window, - // notify components on window border that drag is started. - onDragEnterWindow = { newWindowDragCoordinates -> - for ((_, handler) in handlers) { - val isInside = - isExternalDragInsideComponent(handler.componentCoordinates, newWindowDragCoordinates) - if (isInside) { - handler.onDragStart(calculateOffset(handler.componentCoordinates, newWindowDragCoordinates)) - } - } - windowDragCoordinates = newWindowDragCoordinates - }, - // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them - onDragInsideWindow = { newWindowDragCoordinates -> - for ((_, handler) in handlers) { - val componentCoordinates = handler.componentCoordinates - val oldDragCoordinates = windowDragCoordinates - - val wasDragInside = isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) - val newIsDragInside = - isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) - - if (!wasDragInside && newIsDragInside) { - handler.onDragStart(calculateOffset(componentCoordinates, newWindowDragCoordinates)) - } - - if (wasDragInside && !newIsDragInside) { - handler.onDragCancel() - } - - if (newIsDragInside) { - handler.onDrag(componentCoordinates.windowToLocal(newWindowDragCoordinates)) - } - } - windowDragCoordinates = newWindowDragCoordinates - }, - // notify components on window border drag exited window - onDragExit = { - for ((_, handler) in handlers) { - val componentCoordinates = handler.componentCoordinates - val oldDragCoordinates = windowDragCoordinates - val wasDragInside = isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) - if (wasDragInside) { - handler.onDragCancel() - } - } - windowDragCoordinates = null - }, - // notify all components under the pointer that drop happened - onDrop = { - var anyDrops = false - for ((_, handler) in handlers) { - if (isExternalDragInsideComponent(handler.componentCoordinates, windowDragCoordinates)) { - handler.onDrop(it) - anyDrops = true - } - } - windowDragCoordinates = null - // tell swing whether some components accepted the drop - return@AwtWindowDragTargetListener anyDrops + // @VisibleForTesting + val dragTargetListener = AwtWindowDragTargetListener( + window, + // notify components on window border that drag is started. + onDragEnterWindow = { newWindowDragCoordinates -> + for ((_, handler) in handlers) { + val isInside = + isExternalDragInsideComponent( + handler.componentCoordinates, + newWindowDragCoordinates + ) + if (isInside) { + handler.onDragStart( + calculateOffset( + handler.componentCoordinates, + newWindowDragCoordinates + ) + ) + } + } + windowDragCoordinates = newWindowDragCoordinates + }, + // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them + onDragInsideWindow = { newWindowDragCoordinates -> + for ((_, handler) in handlers) { + val componentCoordinates = handler.componentCoordinates + val oldDragCoordinates = windowDragCoordinates + + val wasDragInside = + isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + val newIsDragInside = + isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) + + if (!wasDragInside && newIsDragInside) { + handler.onDragStart( + calculateOffset( + componentCoordinates, + newWindowDragCoordinates + ) + ) + } + + if (wasDragInside && !newIsDragInside) { + handler.onDragCancel() + } + + if (newIsDragInside) { + handler.onDrag(componentCoordinates.windowToLocal(newWindowDragCoordinates)) + } + } + windowDragCoordinates = newWindowDragCoordinates + }, + // notify components on window border drag exited window + onDragExit = { + for ((_, handler) in handlers) { + val componentCoordinates = handler.componentCoordinates + val oldDragCoordinates = windowDragCoordinates + val wasDragInside = + isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + if (wasDragInside) { + handler.onDragCancel() } - ) - ) + } + windowDragCoordinates = null + }, + // notify all components under the pointer that drop happened + onDrop = { + var anyDrops = false + for ((_, handler) in handlers) { + if (isExternalDragInsideComponent( + handler.componentCoordinates, + windowDragCoordinates + ) + ) { + handler.onDrop(it) + anyDrops = true + } + } + windowDragCoordinates = null + // tell swing whether some components accepted the drop + return@AwtWindowDragTargetListener anyDrops + } + ) + + init { + addDropTargetListener(dragTargetListener) } override fun setActive(isActive: Boolean) { @@ -303,12 +325,13 @@ private class AwtWindowDropTarget( } } -private class AwtWindowDragTargetListener( +// @VisibleForTesting +internal class AwtWindowDragTargetListener( private val window: Window, - private val onDragEnterWindow: (Offset) -> Unit, - private val onDragInsideWindow: (Offset) -> Unit, - private val onDragExit: () -> Unit, - private val onDrop: (DropData) -> Boolean, + val onDragEnterWindow: (Offset) -> Unit, + val onDragInsideWindow: (Offset) -> Unit, + val onDragExit: () -> Unit, + val onDrop: (DropData) -> Boolean, ) : DropTargetListener { private val density = window.density.density diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt new file mode 100644 index 0000000000000..32f7ccdfd8225 --- /dev/null +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt @@ -0,0 +1,266 @@ +/* + * 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.dnd + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.Drag +import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.DragCancelled +import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.DragStarted +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.density +import androidx.compose.ui.window.launchApplication +import androidx.compose.ui.window.rememberWindowState +import androidx.compose.ui.window.runApplicationTest +import com.google.common.truth.Truth.assertThat +import java.awt.Window +import org.junit.Test + +class ExternalDragTest { + @Test + fun `drag inside component that close to top left corner`() = runApplicationTest { + lateinit var window: ComposeWindow + + val events = mutableListOf() + + launchApplication { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 200.dp, height = 100.dp) + ) { + window = this.window + + Column { + Box( + modifier = Modifier.fillMaxSize() + .saveExternalDragEvents(events) + ) + } + } + } + + awaitIdle() + assertThat(events.size).isEqualTo(0) + + window.dragEvents { + onDragEnterWindow(Offset(50f, 50f)) + } + awaitIdle() + assertThat(events.size).isEqualTo(1) + assertThat(events.last()).isEqualTo(DragStarted(Offset(50f, 50f))) + + window.dragEvents { + onDragInsideWindow(Offset(70f, 70f)) + } + awaitIdle() + + assertThat(events.size).isEqualTo(2) + assertThat(events.last()).isEqualTo(Drag(Offset(70f, 70f))) + + exitApplication() + } + + + @Test + fun `drag enters component that far from top left corner`() = runApplicationTest { + lateinit var window: ComposeWindow + + val events = mutableListOf() + + launchApplication { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 200.dp, height = 100.dp) + ) { + window = this.window + Column { + Spacer(modifier = Modifier.height(height = 25.dp)) + Box( + modifier = Modifier.fillMaxSize() + .saveExternalDragEvents(events) + ) + } + } + } + + awaitIdle() + val componentYOffset = with(window.density) { + 25.dp.toPx() + } + + assertThat(events.size).isEqualTo(0) + + window.dragEvents { + onDragEnterWindow(Offset(10f, 10f)) + } + awaitIdle() + assertThat(events.size).isEqualTo(0) + + window.dragEvents { + onDragInsideWindow(Offset(70f, componentYOffset + 1f)) + } + awaitIdle() + + assertThat(events.size).isEqualTo(2) + assertThat(events[0]).isEqualTo(DragStarted(Offset(70f, 1f))) + assertThat(events[1]).isEqualTo(Drag(Offset(70f, 1f))) + + exitApplication() + } + + @Test + fun `multiple components`() = runApplicationTest { + lateinit var window: ComposeWindow + + val eventsComponent1 = mutableListOf() + val eventsComponent2 = mutableListOf() + + launchApplication { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 400.dp, height = 400.dp) + ) { + window = this.window + Column { + Box( + modifier = Modifier.size(100.dp, 100.dp) + .saveExternalDragEvents(eventsComponent1) + ) + Box( + modifier = Modifier.size(100.dp, 100.dp) + .saveExternalDragEvents(eventsComponent2) + ) + } + } + } + + awaitIdle() + val component2YOffset = with(window.density) { + 100.dp.toPx() + } + + assertThat(eventsComponent1.size).isEqualTo(0) + assertThat(eventsComponent2.size).isEqualTo(0) + + window.dragEvents { + onDragEnterWindow(Offset(10f, 10f)) + } + awaitIdle() + assertThat(eventsComponent1.size).isEqualTo(1) + assertThat(eventsComponent1.last()).isEqualTo(DragStarted(Offset(10f, 10f))) + + assertThat(eventsComponent2.size).isEqualTo(0) + + window.dragEvents { + onDragInsideWindow(Offset(70f, component2YOffset + 1f)) + } + awaitIdle() + + assertThat(eventsComponent1.size).isEqualTo(2) + assertThat(eventsComponent1.last()).isEqualTo(DragCancelled) + + assertThat(eventsComponent2.size).isEqualTo(2) + assertThat(eventsComponent2[0]).isEqualTo(DragStarted(Offset(70f, 1f))) + assertThat(eventsComponent2[1]).isEqualTo(Drag(Offset(70f, 1f))) + + val dropData = DropData.Text("Text", mimeType = "text/plain") + window.dragEvents { + onDrop(dropData) + } + awaitIdle() + + assertThat(eventsComponent1.size).isEqualTo(2) + + assertThat(eventsComponent2.size).isEqualTo(3) + assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(dropData)) + + exitApplication() + } + + @Test + fun `stop dnd handling when there are no components`() = runApplicationTest { + lateinit var window: ComposeWindow + + lateinit var componentIsVisible: MutableState + + launchApplication { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 400.dp, height = 400.dp) + ) { + window = this.window + Column { + componentIsVisible = remember { mutableStateOf(true) } + if (componentIsVisible.value) { + Box( + modifier = Modifier.fillMaxSize().onExternalDrag() + ) + } + } + } + } + + awaitIdle() + assertThat(window.dropTarget.isActive).isEqualTo(true) + + componentIsVisible.value = false + awaitIdle() + assertThat(window.dropTarget.isActive).isEqualTo(false) + } + + private fun Window.dragEvents(eventsProvider: AwtWindowDragTargetListener.() -> Unit) { + val listener = (dropTarget as AwtWindowDropTarget).dragTargetListener + listener.eventsProvider() + } + + @Composable + private fun Modifier.saveExternalDragEvents(events: MutableList): Modifier { + return this.onExternalDrag( + onDragStart = { + events.add(DragStarted(it)) + }, + onDrop = { + events.add(TestDragEvent.Drop(it)) + }, + onDrag = { + events.add(Drag(it)) + }, + onDragCancel = { + events.add(DragCancelled) + } + ) + } + + private sealed interface TestDragEvent { + data class DragStarted(val offset: Offset) : TestDragEvent + object DragCancelled : TestDragEvent + data class Drag(val offset: Offset) : TestDragEvent + data class Drop(val data: DropData) : TestDragEvent + } +} \ No newline at end of file From 6614da1c50bde93551329769d26d6fd2c40d73ee Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Tue, 31 Jan 2023 22:55:49 +0100 Subject: [PATCH 07/31] Mark external drop API as experimental --- .../kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 5 +++++ .../kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt | 2 ++ 2 files changed, 7 insertions(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 736db8cdeeeec..4514b532e95e9 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset @@ -50,6 +51,7 @@ import java.io.File /** * Represent data types drag and dropped to an application from outside. */ +@ExperimentalComposeUiApi sealed interface DropData { /** * Represents list of files drag and dropped to an application in a raw [java.net.URI] format. @@ -77,6 +79,7 @@ sealed interface DropData { * @param onDrop is called when the pointer is released with [DropData] the pointer held. * @param onDragCancel is called if the pointer exited the component bounds or unknown data was dropped. */ +@ExperimentalComposeUiApi @Composable fun Modifier.onExternalDrag( enabled: Boolean = true, @@ -145,6 +148,7 @@ fun Modifier.onExternalDrag( * * @VisibleForTesting */ +@OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDropTarget( private val window: Window ) : DropTarget(window, DnDConstants.ACTION_MOVE, null, true) { @@ -326,6 +330,7 @@ internal class AwtWindowDropTarget( } // @VisibleForTesting +@OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDragTargetListener( private val window: Window, val onDragEnterWindow: (Offset) -> Unit, diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt index 32f7ccdfd8225..2073610866e67 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.Drag @@ -42,6 +43,7 @@ import com.google.common.truth.Truth.assertThat import java.awt.Window import org.junit.Test +@OptIn(ExperimentalComposeUiApi::class) class ExternalDragTest { @Test fun `drag inside component that close to top left corner`() = runApplicationTest { From bae882e0d4ecdc6ea2bc79682b0e927d0205112e Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 11:39:22 +0100 Subject: [PATCH 08/31] Use handlers.values --- .../androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 4514b532e95e9..0c6d169f98749 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -165,7 +165,7 @@ internal class AwtWindowDropTarget( window, // notify components on window border that drag is started. onDragEnterWindow = { newWindowDragCoordinates -> - for ((_, handler) in handlers) { + for (handler in handlers.values) { val isInside = isExternalDragInsideComponent( handler.componentCoordinates, @@ -184,7 +184,7 @@ internal class AwtWindowDropTarget( }, // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them onDragInsideWindow = { newWindowDragCoordinates -> - for ((_, handler) in handlers) { + for (handler in handlers.values) { val componentCoordinates = handler.componentCoordinates val oldDragCoordinates = windowDragCoordinates @@ -214,7 +214,7 @@ internal class AwtWindowDropTarget( }, // notify components on window border drag exited window onDragExit = { - for ((_, handler) in handlers) { + for (handler in handlers.values) { val componentCoordinates = handler.componentCoordinates val oldDragCoordinates = windowDragCoordinates val wasDragInside = @@ -228,7 +228,7 @@ internal class AwtWindowDropTarget( // notify all components under the pointer that drop happened onDrop = { var anyDrops = false - for ((_, handler) in handlers) { + for (handler in handlers.values) { if (isExternalDragInsideComponent( handler.componentCoordinates, windowDragCoordinates From c4945f49e80fdf365d4077cb222b09c28a13b11f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 12:28:20 +0100 Subject: [PATCH 09/31] Use componentBounds instead of LayoutCoordinates for external drag support --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 0c6d169f98749..30cc07d04fe5a 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toPainter -import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.window.LocalWindow @@ -93,20 +93,20 @@ fun Modifier.onExternalDrag( } val window = LocalWindow.current ?: return@composed Modifier - var componentCoordinates by remember { mutableStateOf(null) } - val boundsInWindow = componentCoordinates?.boundsInWindow() + var componentBounds by remember { mutableStateOf(null) } var componentDragHandleId by remember { mutableStateOf(null) } // components drag handlers depend on the component position, // so we should reinstall handlers with a new component position when it was changed - DisposableEffect(window, boundsInWindow) { - val currentComponentCoordinates = componentCoordinates ?: return@DisposableEffect onDispose { } + DisposableEffect(window, componentBounds) { + val currentComponentBounds = componentBounds ?: return@DisposableEffect onDispose { } + when (val currentDropTarget = window.dropTarget) { is AwtWindowDropTarget -> { // if our drop target is already assigned simply add new drag handler for the current component componentDragHandleId = currentDropTarget.installComponentDragHandler( - currentComponentCoordinates, onDragStart, onDrag, onDragCancel, onDrop + currentComponentBounds, onDragStart, onDrag, onDragCancel, onDrop ) } @@ -114,7 +114,7 @@ fun Modifier.onExternalDrag( // drop target is not installed for the window, so assign it and add new drag handler for the current component val newDropTarget = AwtWindowDropTarget(window) componentDragHandleId = newDropTarget.installComponentDragHandler( - currentComponentCoordinates, onDragStart, onDrag, onDragCancel, onDrop + currentComponentBounds, onDragStart, onDrag, onDragCancel, onDrop ) window.dropTarget = newDropTarget } @@ -137,7 +137,7 @@ fun Modifier.onExternalDrag( Modifier .onGloballyPositioned { - componentCoordinates = it + componentBounds = it.boundsInWindow() } } @@ -166,15 +166,14 @@ internal class AwtWindowDropTarget( // notify components on window border that drag is started. onDragEnterWindow = { newWindowDragCoordinates -> for (handler in handlers.values) { - val isInside = - isExternalDragInsideComponent( - handler.componentCoordinates, - newWindowDragCoordinates - ) + val isInside = isExternalDragInsideComponent( + handler.componentBounds, + newWindowDragCoordinates + ) if (isInside) { handler.onDragStart( calculateOffset( - handler.componentCoordinates, + handler.componentBounds, newWindowDragCoordinates ) ) @@ -185,7 +184,7 @@ internal class AwtWindowDropTarget( // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them onDragInsideWindow = { newWindowDragCoordinates -> for (handler in handlers.values) { - val componentCoordinates = handler.componentCoordinates + val componentCoordinates = handler.componentBounds val oldDragCoordinates = windowDragCoordinates val wasDragInside = @@ -193,13 +192,10 @@ internal class AwtWindowDropTarget( val newIsDragInside = isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) + val dragOffset = calculateOffset(componentCoordinates, newWindowDragCoordinates) + if (!wasDragInside && newIsDragInside) { - handler.onDragStart( - calculateOffset( - componentCoordinates, - newWindowDragCoordinates - ) - ) + handler.onDragStart(dragOffset) } if (wasDragInside && !newIsDragInside) { @@ -207,7 +203,7 @@ internal class AwtWindowDropTarget( } if (newIsDragInside) { - handler.onDrag(componentCoordinates.windowToLocal(newWindowDragCoordinates)) + handler.onDrag(dragOffset) } } windowDragCoordinates = newWindowDragCoordinates @@ -215,7 +211,7 @@ internal class AwtWindowDropTarget( // notify components on window border drag exited window onDragExit = { for (handler in handlers.values) { - val componentCoordinates = handler.componentCoordinates + val componentCoordinates = handler.componentBounds val oldDragCoordinates = windowDragCoordinates val wasDragInside = isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) @@ -229,11 +225,11 @@ internal class AwtWindowDropTarget( onDrop = { var anyDrops = false for (handler in handlers.values) { - if (isExternalDragInsideComponent( - handler.componentCoordinates, - windowDragCoordinates - ) - ) { + val isInside = isExternalDragInsideComponent( + handler.componentBounds, + windowDragCoordinates + ) + if (isInside) { handler.onDrop(it) anyDrops = true } @@ -259,11 +255,11 @@ internal class AwtWindowDropTarget( * Subscribes on drag events for [window]. * If drag is going and component is under pointer [onDragStart] can be called synchronously. * - * @param componentCoordinates coordinates of the component used to properly detect when drag entered/exited component + * @param componentBounds bounds of the component inside [Window] used to properly detect when drag entered/exited component * @return handler id that can be used later to remove subscription using [stopDragHandling] */ fun installComponentDragHandler( - componentCoordinates: LayoutCoordinates, + componentBounds: Rect, onDragStart: (Offset) -> Unit, onDrag: (Offset) -> Unit, onDragCancel: () -> Unit, @@ -271,10 +267,11 @@ internal class AwtWindowDropTarget( ): Int { isActive = true - handlers[idsCounter] = ComponentDragHandler(componentCoordinates, onDragStart, onDrag, onDragCancel, onDrop) + handlers[idsCounter] = + ComponentDragHandler(componentBounds, onDragStart, onDrag, onDragCancel, onDrop) - if (isExternalDragInsideComponent(componentCoordinates, windowDragCoordinates)) { - onDragStart(calculateOffset(componentCoordinates, windowDragCoordinates!!)) + if (isExternalDragInsideComponent(componentBounds, windowDragCoordinates)) { + onDragStart(calculateOffset(componentBounds, windowDragCoordinates!!)) } return idsCounter++ } @@ -289,7 +286,9 @@ internal class AwtWindowDropTarget( */ fun stopDragHandling(handleId: Int) { val handler = handlers[handleId] - if (handler != null && isExternalDragInsideComponent(handler.componentCoordinates, windowDragCoordinates)) { + if (handler != null && + isExternalDragInsideComponent(handler.componentBounds, windowDragCoordinates) + ) { handler.onDragCancel() } handlers.remove(handleId) @@ -301,7 +300,7 @@ internal class AwtWindowDropTarget( private class ComponentDragHandler( - val componentCoordinates: LayoutCoordinates, + val componentBounds: Rect, val onDragStart: (Offset) -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, @@ -310,21 +309,21 @@ internal class AwtWindowDropTarget( companion object { private fun isExternalDragInsideComponent( - componentCoordinates: LayoutCoordinates?, + componentBounds: Rect?, windowDragCoordinates: Offset? ): Boolean { - if (componentCoordinates == null || windowDragCoordinates == null) { + if (componentBounds == null || windowDragCoordinates == null) { return false } - return componentCoordinates.boundsInWindow().contains(windowDragCoordinates) + return componentBounds.contains(windowDragCoordinates) } private fun calculateOffset( - componentCoordinates: LayoutCoordinates, + componentBounds: Rect, windowDragCoordinates: Offset ): Offset { - return componentCoordinates.windowToLocal(windowDragCoordinates) + return windowDragCoordinates - componentBounds.topLeft } } } From bec55313743bf874ec69931ac2b3a512ccf7203f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 14:18:52 +0100 Subject: [PATCH 10/31] Wrap lambdas in rememberUpdatedState --- .../androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 30cc07d04fe5a..6e784c4d6c673 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -93,6 +94,11 @@ fun Modifier.onExternalDrag( } val window = LocalWindow.current ?: return@composed Modifier + val onDragStartState by rememberUpdatedState(onDragStart) + val onDragState by rememberUpdatedState(onDrag) + val onDragCancelState by rememberUpdatedState(onDragCancel) + val onDropState by rememberUpdatedState(onDrop) + var componentBounds by remember { mutableStateOf(null) } var componentDragHandleId by remember { mutableStateOf(null) } @@ -106,7 +112,8 @@ fun Modifier.onExternalDrag( is AwtWindowDropTarget -> { // if our drop target is already assigned simply add new drag handler for the current component componentDragHandleId = currentDropTarget.installComponentDragHandler( - currentComponentBounds, onDragStart, onDrag, onDragCancel, onDrop + currentComponentBounds, + onDragStartState, onDragState, onDragCancelState, onDropState ) } @@ -114,7 +121,8 @@ fun Modifier.onExternalDrag( // drop target is not installed for the window, so assign it and add new drag handler for the current component val newDropTarget = AwtWindowDropTarget(window) componentDragHandleId = newDropTarget.installComponentDragHandler( - currentComponentBounds, onDragStart, onDrag, onDragCancel, onDrop + currentComponentBounds, + onDragStartState, onDragState, onDragCancelState, onDropState ) window.dropTarget = newDropTarget } From b6150fe38ff0edd07ccb73b57a739a4a86c2757f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 14:50:37 +0100 Subject: [PATCH 11/31] Add experimental compose api opt in --- .../kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index 0e52b2c817182..3734dcb5c9ee3 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.dnd.DropData import androidx.compose.ui.dnd.onExternalDrag @@ -37,6 +38,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication +@OptIn(ExperimentalComposeUiApi::class) fun main() = singleWindowApplication( title = "External dnd demo" ) { From 79dc9473dacaeefa58066a27ddbffc4b5f56bde3 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 12:43:20 +0100 Subject: [PATCH 12/31] Introduce test to check that drag enter/cancel are not spammed on component bounds change --- .../compose/ui/dnd/ExternalDragTest.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt index 2073610866e67..15e75bc12741e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt @@ -19,13 +19,17 @@ package androidx.compose.ui.dnd import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposeWindow @@ -236,6 +240,58 @@ class ExternalDragTest { assertThat(window.dropTarget.isActive).isEqualTo(false) } + // https://github.com/JetBrains/compose-multiplatform-core/pull/391#discussion_r1128543475 + @Test + fun `make drag area bigger on hover`() = runApplicationTest { + lateinit var window: ComposeWindow + + val events = mutableListOf() + + launchApplication { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 200.dp, height = 100.dp) + ) { + window = this.window + var width by remember { mutableStateOf(50.dp) } + Column { + Box( + modifier = Modifier + .width(width) + .fillMaxHeight() + .onExternalDrag( + onDragStart = { + // make box bigger on enter + events.add(DragStarted(it)) + width = 100.dp + }, + onDragCancel = { + // make box smalled when drag exited + events.add(DragCancelled) + width = 50.dp + } + ) + ) + } + } + } + + awaitIdle() + assertThat(events.size).isEqualTo(0) + + window.dragEvents { + onDragEnterWindow(Offset(25f, 25f)) + } + + // only one event should be handled -- drag started, even if the component become bigger + // since the pointer is always on the component + repeat(10) { + awaitIdle() + assertThat(events.size).isEqualTo(1) + assertThat(events.last()).isEqualTo(DragStarted(Offset(25f, 25f))) + } + } + private fun Window.dragEvents(eventsProvider: AwtWindowDragTargetListener.() -> Unit) { val listener = (dropTarget as AwtWindowDropTarget).dragTargetListener listener.eventsProvider() From 27b74933a44472643621ebddccdeaf3c85b6566f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 15:06:28 +0100 Subject: [PATCH 13/31] Don't reinstall component drag handlers on recomposition, just provide new component bounds to Swing --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 102 +++++++++++------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 6e784c4d6c673..3eebc67b6035a 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -99,20 +99,13 @@ fun Modifier.onExternalDrag( val onDragCancelState by rememberUpdatedState(onDragCancel) val onDropState by rememberUpdatedState(onDrop) - var componentBounds by remember { mutableStateOf(null) } - var componentDragHandleId by remember { mutableStateOf(null) } - // components drag handlers depend on the component position, - // so we should reinstall handlers with a new component position when it was changed - DisposableEffect(window, componentBounds) { - val currentComponentBounds = componentBounds ?: return@DisposableEffect onDispose { } - + DisposableEffect(window) { when (val currentDropTarget = window.dropTarget) { is AwtWindowDropTarget -> { // if our drop target is already assigned simply add new drag handler for the current component componentDragHandleId = currentDropTarget.installComponentDragHandler( - currentComponentBounds, onDragStartState, onDragState, onDragCancelState, onDropState ) } @@ -121,7 +114,6 @@ fun Modifier.onExternalDrag( // drop target is not installed for the window, so assign it and add new drag handler for the current component val newDropTarget = AwtWindowDropTarget(window) componentDragHandleId = newDropTarget.installComponentDragHandler( - currentComponentBounds, onDragStartState, onDragState, onDragCancelState, onDropState ) window.dropTarget = newDropTarget @@ -133,6 +125,8 @@ fun Modifier.onExternalDrag( } onDispose { + // stop drag events handling for this component when window is changed + // or the component leaves the composition val dropTarget = window.dropTarget if (dropTarget is AwtWindowDropTarget) { val handleIdToRemove = componentDragHandleId @@ -144,8 +138,13 @@ fun Modifier.onExternalDrag( } Modifier - .onGloballyPositioned { - componentBounds = it.boundsInWindow() + .onGloballyPositioned { position -> + // provide new component bounds to Swing to properly detect drag events + val dropTarget = window.dropTarget as? AwtWindowDropTarget + ?: return@onGloballyPositioned + val handleIdToUpdate = componentDragHandleId ?: return@onGloballyPositioned + val componentBounds = position.boundsInWindow() + dropTarget.updateComponentBounds(handleIdToUpdate, componentBounds) } } @@ -174,16 +173,14 @@ internal class AwtWindowDropTarget( // notify components on window border that drag is started. onDragEnterWindow = { newWindowDragCoordinates -> for (handler in handlers.values) { + val componentBounds = handler.currentComponentBounds ?: continue val isInside = isExternalDragInsideComponent( - handler.componentBounds, + componentBounds, newWindowDragCoordinates ) if (isInside) { handler.onDragStart( - calculateOffset( - handler.componentBounds, - newWindowDragCoordinates - ) + calculateOffset(componentBounds, newWindowDragCoordinates) ) } } @@ -192,15 +189,15 @@ internal class AwtWindowDropTarget( // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them onDragInsideWindow = { newWindowDragCoordinates -> for (handler in handlers.values) { - val componentCoordinates = handler.componentBounds + val componentBounds = handler.currentComponentBounds ?: continue val oldDragCoordinates = windowDragCoordinates val wasDragInside = - isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + isExternalDragInsideComponent(componentBounds, oldDragCoordinates) val newIsDragInside = - isExternalDragInsideComponent(componentCoordinates, newWindowDragCoordinates) + isExternalDragInsideComponent(componentBounds, newWindowDragCoordinates) - val dragOffset = calculateOffset(componentCoordinates, newWindowDragCoordinates) + val dragOffset = calculateOffset(componentBounds, newWindowDragCoordinates) if (!wasDragInside && newIsDragInside) { handler.onDragStart(dragOffset) @@ -219,10 +216,10 @@ internal class AwtWindowDropTarget( // notify components on window border drag exited window onDragExit = { for (handler in handlers.values) { - val componentCoordinates = handler.componentBounds + val componentBounds = handler.currentComponentBounds ?: continue val oldDragCoordinates = windowDragCoordinates val wasDragInside = - isExternalDragInsideComponent(componentCoordinates, oldDragCoordinates) + isExternalDragInsideComponent(componentBounds, oldDragCoordinates) if (wasDragInside) { handler.onDragCancel() } @@ -233,8 +230,9 @@ internal class AwtWindowDropTarget( onDrop = { var anyDrops = false for (handler in handlers.values) { + val componentBounds = handler.currentComponentBounds ?: continue val isInside = isExternalDragInsideComponent( - handler.componentBounds, + componentBounds, windowDragCoordinates ) if (isInside) { @@ -260,27 +258,24 @@ internal class AwtWindowDropTarget( } /** - * Subscribes on drag events for [window]. - * If drag is going and component is under pointer [onDragStart] can be called synchronously. + * Adds handler that will be notified on drag events for [window]. + * If component bounds are provided using [updateComponentBounds], + * given lambdas will be called on drag events. * - * @param componentBounds bounds of the component inside [Window] used to properly detect when drag entered/exited component * @return handler id that can be used later to remove subscription using [stopDragHandling] + * or to update component bounds using [updateComponentBounds] */ fun installComponentDragHandler( - componentBounds: Rect, onDragStart: (Offset) -> Unit, onDrag: (Offset) -> Unit, onDragCancel: () -> Unit, onDrop: (DropData) -> Unit ): Int { isActive = true - - handlers[idsCounter] = - ComponentDragHandler(componentBounds, onDragStart, onDrag, onDragCancel, onDrop) - - if (isExternalDragInsideComponent(componentBounds, windowDragCoordinates)) { - onDragStart(calculateOffset(componentBounds, windowDragCoordinates!!)) - } + handlers[idsCounter] = ComponentDragHandler( + currentComponentBounds = null, + onDragStart, onDrag, onDragCancel, onDrop + ) return idsCounter++ } @@ -295,7 +290,7 @@ internal class AwtWindowDropTarget( fun stopDragHandling(handleId: Int) { val handler = handlers[handleId] if (handler != null && - isExternalDragInsideComponent(handler.componentBounds, windowDragCoordinates) + isExternalDragInsideComponent(handler.currentComponentBounds, windowDragCoordinates) ) { handler.onDragCancel() } @@ -306,9 +301,44 @@ internal class AwtWindowDropTarget( } } + /** + * Updates component bounds within the [window], so drag events will be properly handled. + * If drag is going and component is under the pointer, onDragStart and onDrag will be called. + * If drag is going and component moved/become smaller, so that pointer now is not the component, onDragCancel is called. + * + * All further drag events will use [newComponentBounds] to notify handler with [handleId]. + * + * @param newComponentBounds new bounds of the component inside [window] used to properly detect when drag entered/exited component + */ + fun updateComponentBounds(handleId: Int, newComponentBounds: Rect) { + val handler = handlers[handleId] ?: return + val oldComponentBounds = handler.currentComponentBounds + val currentWindowDragCoordinates = windowDragCoordinates + if (currentWindowDragCoordinates != null) { + // drag is going + val wasDragInside = + isExternalDragInsideComponent(oldComponentBounds, currentWindowDragCoordinates) + val newIsDragInside = + isExternalDragInsideComponent(newComponentBounds, currentWindowDragCoordinates) + val dragOffset = calculateOffset(newComponentBounds, currentWindowDragCoordinates) + + if (!wasDragInside && newIsDragInside) { + handler.onDragStart(dragOffset) + } + + if (wasDragInside && !newIsDragInside) { + handler.onDragCancel() + } + + if (newIsDragInside) { + handler.onDrag(dragOffset) + } + } + handler.currentComponentBounds = newComponentBounds + } private class ComponentDragHandler( - val componentBounds: Rect, + var currentComponentBounds: Rect?, val onDragStart: (Offset) -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, From fe62f4f749aca0f7fb256204b338afef847a5740 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 15:32:52 +0100 Subject: [PATCH 14/31] Extract common code for drag event handling --- .../compose/ui/dnd/ExternalDrag.desktop.kt | 157 +++++++++--------- 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 3eebc67b6035a..d52bbc9a7dd3b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -164,6 +164,9 @@ internal class AwtWindowDropTarget( // all components that are subscribed to external drag and drop for the window private val handlers = mutableMapOf() + // bounds of all components that are subscribed to external drag and drop for the window + private val componentBoundsHolder = mutableMapOf() + // drag coordinates used to detect that drag entered/exited components private var windowDragCoordinates: Offset? = null @@ -172,75 +175,51 @@ internal class AwtWindowDropTarget( window, // notify components on window border that drag is started. onDragEnterWindow = { newWindowDragCoordinates -> - for (handler in handlers.values) { - val componentBounds = handler.currentComponentBounds ?: continue - val isInside = isExternalDragInsideComponent( - componentBounds, - newWindowDragCoordinates + forAllPositionedComponents { handler, componentBounds -> + handleDragEvent( + handler, + oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, + oldDragCoordinates = null, currentDragCoordinates = newWindowDragCoordinates ) - if (isInside) { - handler.onDragStart( - calculateOffset(componentBounds, newWindowDragCoordinates) - ) - } } windowDragCoordinates = newWindowDragCoordinates }, // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them onDragInsideWindow = { newWindowDragCoordinates -> - for (handler in handlers.values) { - val componentBounds = handler.currentComponentBounds ?: continue - val oldDragCoordinates = windowDragCoordinates - - val wasDragInside = - isExternalDragInsideComponent(componentBounds, oldDragCoordinates) - val newIsDragInside = - isExternalDragInsideComponent(componentBounds, newWindowDragCoordinates) - - val dragOffset = calculateOffset(componentBounds, newWindowDragCoordinates) - - if (!wasDragInside && newIsDragInside) { - handler.onDragStart(dragOffset) - } - - if (wasDragInside && !newIsDragInside) { - handler.onDragCancel() - } - - if (newIsDragInside) { - handler.onDrag(dragOffset) - } - } + val oldDragCoordinates = windowDragCoordinates windowDragCoordinates = newWindowDragCoordinates + forAllPositionedComponents { handler, componentBounds -> + handleDragEvent( + handler, + oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, + oldDragCoordinates, newWindowDragCoordinates + ) + } }, // notify components on window border drag exited window onDragExit = { - for (handler in handlers.values) { - val componentBounds = handler.currentComponentBounds ?: continue - val oldDragCoordinates = windowDragCoordinates - val wasDragInside = - isExternalDragInsideComponent(componentBounds, oldDragCoordinates) - if (wasDragInside) { - handler.onDragCancel() - } - } + val oldDragCoordinates = windowDragCoordinates windowDragCoordinates = null + forAllPositionedComponents { handler, componentBounds -> + handleDragEvent( + handler, + oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, + oldDragCoordinates = oldDragCoordinates, currentDragCoordinates = null + ) + } }, // notify all components under the pointer that drop happened onDrop = { var anyDrops = false - for (handler in handlers.values) { - val componentBounds = handler.currentComponentBounds ?: continue - val isInside = isExternalDragInsideComponent( - componentBounds, - windowDragCoordinates - ) + val dropCoordinates = windowDragCoordinates + windowDragCoordinates = null + forAllPositionedComponents { handler, componentBounds -> + val isInside = isExternalDragInsideComponent(componentBounds, dropCoordinates) if (isInside) { handler.onDrop(it) anyDrops = true } } - windowDragCoordinates = null // tell swing whether some components accepted the drop return@AwtWindowDragTargetListener anyDrops } @@ -272,10 +251,7 @@ internal class AwtWindowDropTarget( onDrop: (DropData) -> Unit ): Int { isActive = true - handlers[idsCounter] = ComponentDragHandler( - currentComponentBounds = null, - onDragStart, onDrag, onDragCancel, onDrop - ) + handlers[idsCounter] = ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) return idsCounter++ } @@ -289,12 +265,14 @@ internal class AwtWindowDropTarget( */ fun stopDragHandling(handleId: Int) { val handler = handlers[handleId] - if (handler != null && - isExternalDragInsideComponent(handler.currentComponentBounds, windowDragCoordinates) + val componentBounds = componentBoundsHolder[handleId] + if (handler != null && componentBounds != null && + isExternalDragInsideComponent(componentBounds, windowDragCoordinates) ) { handler.onDragCancel() } handlers.remove(handleId) + componentBoundsHolder.remove(handleId) if (handlers.isEmpty()) { isActive = false @@ -304,7 +282,7 @@ internal class AwtWindowDropTarget( /** * Updates component bounds within the [window], so drag events will be properly handled. * If drag is going and component is under the pointer, onDragStart and onDrag will be called. - * If drag is going and component moved/become smaller, so that pointer now is not the component, onDragCancel is called. + * If drag is going and component moved/became smaller, so that pointer now is not the component, onDragCancel is called. * * All further drag events will use [newComponentBounds] to notify handler with [handleId]. * @@ -312,33 +290,22 @@ internal class AwtWindowDropTarget( */ fun updateComponentBounds(handleId: Int, newComponentBounds: Rect) { val handler = handlers[handleId] ?: return - val oldComponentBounds = handler.currentComponentBounds - val currentWindowDragCoordinates = windowDragCoordinates - if (currentWindowDragCoordinates != null) { - // drag is going - val wasDragInside = - isExternalDragInsideComponent(oldComponentBounds, currentWindowDragCoordinates) - val newIsDragInside = - isExternalDragInsideComponent(newComponentBounds, currentWindowDragCoordinates) - val dragOffset = calculateOffset(newComponentBounds, currentWindowDragCoordinates) - - if (!wasDragInside && newIsDragInside) { - handler.onDragStart(dragOffset) - } - - if (wasDragInside && !newIsDragInside) { - handler.onDragCancel() - } + val oldComponentBounds = componentBoundsHolder.put(handleId, newComponentBounds) + handleDragEvent( + handler, oldComponentBounds, newComponentBounds, + oldDragCoordinates = windowDragCoordinates, + currentDragCoordinates = windowDragCoordinates + ) + } - if (newIsDragInside) { - handler.onDrag(dragOffset) - } + private inline fun forAllPositionedComponents(action: (handler: ComponentDragHandler, bounds: Rect) -> Unit) { + for ((handleId, handler) in handlers) { + val bounds = componentBoundsHolder[handleId] ?: continue + action(handler, bounds) } - handler.currentComponentBounds = newComponentBounds } private class ComponentDragHandler( - var currentComponentBounds: Rect?, val onDragStart: (Offset) -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, @@ -363,6 +330,40 @@ internal class AwtWindowDropTarget( ): Offset { return windowDragCoordinates - componentBounds.topLeft } + + /** + * Notifies [handler] about drag events. + * + * Note: this function is pure, so it doesn't update state of [ComponentDragHandler] or current drag position + */ + private fun handleDragEvent( + handler: ComponentDragHandler, + oldComponentBounds: Rect?, + currentComponentBounds: Rect?, + oldDragCoordinates: Offset?, + currentDragCoordinates: Offset? + ) { + val wasDragInside = + isExternalDragInsideComponent(oldComponentBounds, oldDragCoordinates) + val newIsDragInside = + isExternalDragInsideComponent(currentComponentBounds, currentDragCoordinates) + if (!wasDragInside && newIsDragInside) { + val dragOffset = calculateOffset(currentComponentBounds!!, currentDragCoordinates!!) + handler.onDragStart(dragOffset) + return + } + + if (wasDragInside && !newIsDragInside) { + handler.onDragCancel() + return + } + + if (newIsDragInside) { + val dragOffset = calculateOffset(currentComponentBounds!!, currentDragCoordinates!!) + handler.onDrag(dragOffset) + return + } + } } } From 850ba62c849b2e70d75c3971bf49f1ccbaae2235 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 16:02:28 +0100 Subject: [PATCH 15/31] Update idsCounter immediately and use current handleId --- .../kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index d52bbc9a7dd3b..9465351f55334 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -251,8 +251,9 @@ internal class AwtWindowDropTarget( onDrop: (DropData) -> Unit ): Int { isActive = true - handlers[idsCounter] = ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) - return idsCounter++ + val handleId = idsCounter++ + handlers[handleId] = ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) + return handleId } /** From b802866df48234a61e8d38dfbcfeba769f18b87e Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 16:06:03 +0100 Subject: [PATCH 16/31] Change doc --- .../androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 9465351f55334..773f5f55fac27 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -127,13 +127,9 @@ fun Modifier.onExternalDrag( onDispose { // stop drag events handling for this component when window is changed // or the component leaves the composition - val dropTarget = window.dropTarget - if (dropTarget is AwtWindowDropTarget) { - val handleIdToRemove = componentDragHandleId - if (handleIdToRemove != null) { - dropTarget.stopDragHandling(handleIdToRemove) - } - } + val dropTarget = window.dropTarget as? AwtWindowDropTarget ?: return@onDispose + val handleIdToRemove = componentDragHandleId ?: return@onDispose + dropTarget.stopDragHandling(handleIdToRemove) } } @@ -335,7 +331,7 @@ internal class AwtWindowDropTarget( /** * Notifies [handler] about drag events. * - * Note: this function is pure, so it doesn't update state of [ComponentDragHandler] or current drag position + * Note: this function is pure, so it doesn't update any states */ private fun handleDragEvent( handler: ComponentDragHandler, From 33470dee70e4aa27bd17670aff6d236b8ace5dcf Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 8 Mar 2023 16:10:51 +0100 Subject: [PATCH 17/31] Update tests: don't notify onDrag if onDragEntered notified --- .../kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt index 15e75bc12741e..e08ea674f077e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt @@ -133,9 +133,8 @@ class ExternalDragTest { } awaitIdle() - assertThat(events.size).isEqualTo(2) + assertThat(events.size).isEqualTo(1) assertThat(events[0]).isEqualTo(DragStarted(Offset(70f, 1f))) - assertThat(events[1]).isEqualTo(Drag(Offset(70f, 1f))) exitApplication() } @@ -191,9 +190,8 @@ class ExternalDragTest { assertThat(eventsComponent1.size).isEqualTo(2) assertThat(eventsComponent1.last()).isEqualTo(DragCancelled) - assertThat(eventsComponent2.size).isEqualTo(2) - assertThat(eventsComponent2[0]).isEqualTo(DragStarted(Offset(70f, 1f))) - assertThat(eventsComponent2[1]).isEqualTo(Drag(Offset(70f, 1f))) + assertThat(eventsComponent2.size).isEqualTo(1) + assertThat(eventsComponent2.last()).isEqualTo(DragStarted(Offset(70f, 1f))) val dropData = DropData.Text("Text", mimeType = "text/plain") window.dragEvents { @@ -203,7 +201,7 @@ class ExternalDragTest { assertThat(eventsComponent1.size).isEqualTo(2) - assertThat(eventsComponent2.size).isEqualTo(3) + assertThat(eventsComponent2.size).isEqualTo(2) assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(dropData)) exitApplication() From c2f82debf77d793421b05f5f81885c1062e132ee Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Thu, 9 Mar 2023 11:07:32 +0100 Subject: [PATCH 18/31] Remove VisibleForTesting, can be added in the future if needed --- .../kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt index 773f5f55fac27..bbfcc7e728b40 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt @@ -148,8 +148,6 @@ fun Modifier.onExternalDrag( * Provides a way to subscribe on external drag for given [window] using [installComponentDragHandler] * * [Window] allows having only one [DropTarget], so this is the main [DropTarget] that handles all the drag subscriptions - * - * @VisibleForTesting */ @OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDropTarget( @@ -166,7 +164,6 @@ internal class AwtWindowDropTarget( // drag coordinates used to detect that drag entered/exited components private var windowDragCoordinates: Offset? = null - // @VisibleForTesting val dragTargetListener = AwtWindowDragTargetListener( window, // notify components on window border that drag is started. @@ -364,7 +361,6 @@ internal class AwtWindowDropTarget( } } -// @VisibleForTesting @OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDragTargetListener( private val window: Window, From d068e92111b77d22c407a4423f6fd3adf40fce5e Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Thu, 9 Mar 2023 11:10:19 +0100 Subject: [PATCH 19/31] Move external drag support to androidx.compose.ui package --- .../androidx/compose/desktop/examples/dnd/Main.jvm.kt | 4 ++-- .../compose/ui/{dnd => }/ExternalDrag.desktop.kt | 5 +---- .../androidx/compose/ui/{dnd => }/ExternalDragTest.kt | 10 ++++------ 3 files changed, 7 insertions(+), 12 deletions(-) rename compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/{dnd => }/ExternalDrag.desktop.kt (99%) rename compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/{dnd => }/ExternalDragTest.kt (96%) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index 3734dcb5c9ee3..da4624565738e 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -29,12 +29,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.DropData import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.dnd.DropData -import androidx.compose.ui.dnd.onExternalDrag import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.onExternalDrag import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt similarity index 99% rename from compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt rename to compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index bbfcc7e728b40..22b56f5d6ec63 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/dnd/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.compose.ui.dnd +package androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -23,9 +23,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.painter.Painter diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt similarity index 96% rename from compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index e08ea674f077e..0722a55a06a42 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/dnd/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.compose.ui.dnd +package androidx.compose.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,12 +30,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier +import androidx.compose.ui.ExternalDragTest.TestDragEvent.Drag +import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragCancelled +import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragStarted import androidx.compose.ui.awt.ComposeWindow -import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.Drag -import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.DragCancelled -import androidx.compose.ui.dnd.ExternalDragTest.TestDragEvent.DragStarted import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window From 56f0cffbb2445bc64b6b7557a696003d1a13e58d Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Thu, 9 Mar 2023 11:11:35 +0100 Subject: [PATCH 20/31] Rename forAll -> forEach --- .../kotlin/androidx/compose/ui/ExternalDrag.desktop.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 22b56f5d6ec63..48cd4dc2ab8aa 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -165,7 +165,7 @@ internal class AwtWindowDropTarget( window, // notify components on window border that drag is started. onDragEnterWindow = { newWindowDragCoordinates -> - forAllPositionedComponents { handler, componentBounds -> + forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, @@ -178,7 +178,7 @@ internal class AwtWindowDropTarget( onDragInsideWindow = { newWindowDragCoordinates -> val oldDragCoordinates = windowDragCoordinates windowDragCoordinates = newWindowDragCoordinates - forAllPositionedComponents { handler, componentBounds -> + forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, @@ -190,7 +190,7 @@ internal class AwtWindowDropTarget( onDragExit = { val oldDragCoordinates = windowDragCoordinates windowDragCoordinates = null - forAllPositionedComponents { handler, componentBounds -> + forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, @@ -203,7 +203,7 @@ internal class AwtWindowDropTarget( var anyDrops = false val dropCoordinates = windowDragCoordinates windowDragCoordinates = null - forAllPositionedComponents { handler, componentBounds -> + forEachPositionedComponent { handler, componentBounds -> val isInside = isExternalDragInsideComponent(componentBounds, dropCoordinates) if (isInside) { handler.onDrop(it) @@ -289,7 +289,7 @@ internal class AwtWindowDropTarget( ) } - private inline fun forAllPositionedComponents(action: (handler: ComponentDragHandler, bounds: Rect) -> Unit) { + private inline fun forEachPositionedComponent(action: (handler: ComponentDragHandler, bounds: Rect) -> Unit) { for ((handleId, handler) in handlers) { val bounds = componentBoundsHolder[handleId] ?: continue action(handler, bounds) From 095c3a2299221bcbe8ddab23566b1ac12be18052 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Thu, 9 Mar 2023 11:12:47 +0100 Subject: [PATCH 21/31] Merge value acquiring and remove --- .../kotlin/androidx/compose/ui/ExternalDrag.desktop.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 48cd4dc2ab8aa..c0f751cbb7582 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -255,15 +255,13 @@ internal class AwtWindowDropTarget( * @param handleId id provided by [installComponentDragHandler] function */ fun stopDragHandling(handleId: Int) { - val handler = handlers[handleId] - val componentBounds = componentBoundsHolder[handleId] + val handler = handlers.remove(handleId) + val componentBounds = componentBoundsHolder.remove(handleId) if (handler != null && componentBounds != null && isExternalDragInsideComponent(componentBounds, windowDragCoordinates) ) { handler.onDragCancel() } - handlers.remove(handleId) - componentBoundsHolder.remove(handleId) if (handlers.isEmpty()) { isActive = false From 21943c60a34e56b255d763bec0dbea11e638ddc6 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Thu, 9 Mar 2023 11:25:51 +0100 Subject: [PATCH 22/31] Take changed callbacks into account in AwtWindowDropTarget --- .../compose/ui/ExternalDrag.desktop.kt | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index c0f751cbb7582..7d436787057a0 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -18,6 +18,7 @@ package androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -91,10 +92,9 @@ fun Modifier.onExternalDrag( } val window = LocalWindow.current ?: return@composed Modifier - val onDragStartState by rememberUpdatedState(onDragStart) - val onDragState by rememberUpdatedState(onDrag) - val onDragCancelState by rememberUpdatedState(onDragCancel) - val onDropState by rememberUpdatedState(onDrop) + val componentDragHandler = rememberUpdatedState( + AwtWindowDropTarget.ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) + ) var componentDragHandleId by remember { mutableStateOf(null) } @@ -102,17 +102,15 @@ fun Modifier.onExternalDrag( when (val currentDropTarget = window.dropTarget) { is AwtWindowDropTarget -> { // if our drop target is already assigned simply add new drag handler for the current component - componentDragHandleId = currentDropTarget.installComponentDragHandler( - onDragStartState, onDragState, onDragCancelState, onDropState - ) + componentDragHandleId = + currentDropTarget.installComponentDragHandler(componentDragHandler) } null -> { // drop target is not installed for the window, so assign it and add new drag handler for the current component val newDropTarget = AwtWindowDropTarget(window) - componentDragHandleId = newDropTarget.installComponentDragHandler( - onDragStartState, onDragState, onDragCancelState, onDropState - ) + componentDragHandleId = + newDropTarget.installComponentDragHandler(componentDragHandler) window.dropTarget = newDropTarget } @@ -153,7 +151,8 @@ internal class AwtWindowDropTarget( private var idsCounter = 0 // all components that are subscribed to external drag and drop for the window - private val handlers = mutableMapOf() + // handler's callbacks can be changed on recompositions, so State is kept here + private val handlers = mutableMapOf>() // bounds of all components that are subscribed to external drag and drop for the window private val componentBoundsHolder = mutableMapOf() @@ -231,18 +230,16 @@ internal class AwtWindowDropTarget( * If component bounds are provided using [updateComponentBounds], * given lambdas will be called on drag events. * + * [handlerState]'s callbacks can be changed on recompositions. + * New callbacks won't be called with old events, they will be called on new AWT events only. + * * @return handler id that can be used later to remove subscription using [stopDragHandling] * or to update component bounds using [updateComponentBounds] */ - fun installComponentDragHandler( - onDragStart: (Offset) -> Unit, - onDrag: (Offset) -> Unit, - onDragCancel: () -> Unit, - onDrop: (DropData) -> Unit - ): Int { + fun installComponentDragHandler(handlerState: State): Int { isActive = true val handleId = idsCounter++ - handlers[handleId] = ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) + handlers[handleId] = handlerState return handleId } @@ -260,7 +257,7 @@ internal class AwtWindowDropTarget( if (handler != null && componentBounds != null && isExternalDragInsideComponent(componentBounds, windowDragCoordinates) ) { - handler.onDragCancel() + handler.value.onDragCancel() } if (handlers.isEmpty()) { @@ -281,7 +278,7 @@ internal class AwtWindowDropTarget( val handler = handlers[handleId] ?: return val oldComponentBounds = componentBoundsHolder.put(handleId, newComponentBounds) handleDragEvent( - handler, oldComponentBounds, newComponentBounds, + handler.value, oldComponentBounds, newComponentBounds, oldDragCoordinates = windowDragCoordinates, currentDragCoordinates = windowDragCoordinates ) @@ -290,11 +287,11 @@ internal class AwtWindowDropTarget( private inline fun forEachPositionedComponent(action: (handler: ComponentDragHandler, bounds: Rect) -> Unit) { for ((handleId, handler) in handlers) { val bounds = componentBoundsHolder[handleId] ?: continue - action(handler, bounds) + action(handler.value, bounds) } } - private class ComponentDragHandler( + data class ComponentDragHandler( val onDragStart: (Offset) -> Unit, val onDrag: (Offset) -> Unit, val onDragCancel: () -> Unit, From 80eee0d0dfc9fcb619b5e166ec9db874d352c9b8 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 10 Mar 2023 12:36:38 +0100 Subject: [PATCH 23/31] Modify DropData API * don't use sealed interface and data classes * provide mimeTypes for all drop data * provide methods to read drop data content instead of having properties --- .../compose/desktop/examples/dnd/Main.jvm.kt | 2 +- .../compose/ui/ExternalDrag.desktop.kt | 92 +++++++----------- .../compose/ui/ExternalDragAwtExtensions.kt | 95 +++++++++++++++++++ .../androidx/compose/ui/ExternalDragTest.kt | 13 ++- 4 files changed, 144 insertions(+), 58 deletions(-) create mode 100644 compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index da4624565738e..1f25dee0d1656 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -72,7 +72,7 @@ fun main() = singleWindowApplication( onDrop = { data -> text = data.toString() if (data is DropData.Image) { - painter = data.painter + painter = data.readImage() } isDragging = false }) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 7d436787057a0..53efdee65e63e 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -27,47 +27,66 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toPainter import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.window.LocalWindow import androidx.compose.ui.window.density -import java.awt.Image import java.awt.Point import java.awt.Window -import java.awt.datatransfer.DataFlavor -import java.awt.datatransfer.DataFlavor.selectBestTextFlavor -import java.awt.datatransfer.Transferable import java.awt.dnd.DnDConstants import java.awt.dnd.DropTarget import java.awt.dnd.DropTargetDragEvent import java.awt.dnd.DropTargetDropEvent import java.awt.dnd.DropTargetEvent import java.awt.dnd.DropTargetListener -import java.awt.image.BufferedImage -import java.io.File /** - * Represent data types drag and dropped to an application from outside. + * Represent data types drag and dropped to a component from outside an application. */ @ExperimentalComposeUiApi -sealed interface DropData { +interface DropData { /** - * Represents list of files drag and dropped to an application in a raw [java.net.URI] format. + * List of all MIME types for this [DropData]. + * The list is ordered from most richly descriptive to least descriptive. + * + * Some platform-specific mimeTypes can occur in the list. It may be changed in future versions. */ - data class FilesList(val rawUris: List) : DropData + val mimeTypes: List /** - * Represents an image drag and dropped to an application. + * Represents list of files drag and dropped to a component. */ - data class Image(val painter: Painter) : DropData + interface FilesList : DropData { + /** + * Returns list of file paths drag and droppped to an application in a URI format. + */ + fun readFiles(): List + } /** - * Represent text drag and dropped to an application. - * - * @param mimeType mimeType of the [content] such as "text/plain", "text/html", etc. + * Represents an image drag and dropped to a component. */ - data class Text(val content: String, val mimeType: String?) : DropData + interface Image : DropData { + /** + * Returns an image drag and dropped to an application as a [Painter] type. + */ + fun readImage(): Painter + } + + /** + * Represent text drag and dropped to a component. + */ + interface Text : DropData { + /** + * Provides the best MIME type that describes text returned in [readText] + */ + val bestMimeType: String + + /** + * Returns a text dropped to an application. + */ + fun readText(): String + } } /** @@ -405,43 +424,4 @@ internal class AwtWindowDragTargetListener( dtde.dropComplete(false) } } - - private fun Transferable.dropData(): DropData? { - val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) - - return when { - isDataFlavorSupported(DataFlavor.javaFileListFlavor) -> { - val files = getTransferData(DataFlavor.javaFileListFlavor) as? List<*> ?: return null - DropData.FilesList(files.filterIsInstance().map { it.toURI().toString() }) - } - - isDataFlavorSupported(DataFlavor.imageFlavor) -> { - val image = getTransferData(DataFlavor.imageFlavor) as? Image ?: return null - DropData.Image(image.painter()) - } - - bestTextFlavor != null -> { - val reader = bestTextFlavor.getReaderForText(this) ?: return null - DropData.Text(content = reader.readText(), mimeType = bestTextFlavor.mimeType) - } - - else -> null - } - } - - private fun Image.painter(): Painter { - if (this is BufferedImage) { - return this.toPainter() - } - val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_ARGB) - - val g2 = bufferedImage.createGraphics() - try { - g2.drawImage(this, 0, 0, null) - } finally { - g2.dispose() - } - - return bufferedImage.toPainter() - } } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt new file mode 100644 index 0000000000000..6acf5c5542a3f --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt @@ -0,0 +1,95 @@ +/* + * 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 + +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toPainter +import java.awt.Image +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.DataFlavor.selectBestTextFlavor +import java.awt.datatransfer.Transferable +import java.awt.image.BufferedImage +import java.io.File + +@OptIn(ExperimentalComposeUiApi::class) +internal fun Transferable.dropData(): DropData? { + val mimeTypes = transferDataFlavors.map { it.mimeType } + val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) + + return when { + isDataFlavorSupported(DataFlavor.javaFileListFlavor) -> + DropDataFilesListImpl(mimeTypes, this) + + isDataFlavorSupported(DataFlavor.imageFlavor) -> DropDataImageImpl(mimeTypes, this) + + bestTextFlavor != null -> DropDataTextImpl(mimeTypes, bestTextFlavor, this) + + else -> null + } +} + +@OptIn(ExperimentalComposeUiApi::class) +private class DropDataFilesListImpl( + override val mimeTypes: List, + private val transferable: Transferable +) : DropData.FilesList { + override fun readFiles(): List { + val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> + return files.filterIsInstance().map { it.toURI().toString() } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +private class DropDataImageImpl( + override val mimeTypes: List, + private val transferable: Transferable +) : DropData.Image { + override fun readImage(): Painter { + return (transferable.getTransferData(DataFlavor.imageFlavor) as Image).painter() + } + + private fun Image.painter(): Painter { + if (this is BufferedImage) { + return this.toPainter() + } + val bufferedImage = + BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_ARGB) + + val g2 = bufferedImage.createGraphics() + try { + g2.drawImage(this, 0, 0, null) + } finally { + g2.dispose() + } + + return bufferedImage.toPainter() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +private class DropDataTextImpl( + override val mimeTypes: List, + private val bestTextFlavor: DataFlavor, + private val transferable: Transferable +) : DropData.Text { + override val bestMimeType: String = bestTextFlavor.mimeType + + override fun readText(): String { + val reader = bestTextFlavor.getReaderForText(transferable) + return reader.readText() + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index 0722a55a06a42..e591096451b85 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -191,7 +191,7 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(1) assertThat(eventsComponent2.last()).isEqualTo(DragStarted(Offset(70f, 1f))) - val dropData = DropData.Text("Text", mimeType = "text/plain") + val dropData = createTextDropData("Text") window.dragEvents { onDrop(dropData) } @@ -311,6 +311,17 @@ class ExternalDragTest { ) } + private fun createTextDropData(text: String): DropData { + return object : DropData.Text { + override val mimeTypes: List = listOf("text/plain") + override val bestMimeType: String = mimeTypes.first() + + override fun readText(): String { + return text + } + } + } + private sealed interface TestDragEvent { data class DragStarted(val offset: Offset) : TestDragEvent object DragCancelled : TestDragEvent From c0b992c96c502512b27177c260f3362be88d9531 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 10 Mar 2023 14:53:55 +0100 Subject: [PATCH 24/31] Always provide offset and drop data to external drag feature --- .../compose/desktop/examples/dnd/Main.jvm.kt | 3 +- .../compose/ui/ExternalDrag.desktop.kt | 132 +++++++++++------- .../compose/ui/ExternalDragAwtExtensions.kt | 7 +- .../androidx/compose/ui/ExternalDragTest.kt | 52 ++++--- 4 files changed, 126 insertions(+), 68 deletions(-) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index 1f25dee0d1656..007d8dc36dad7 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -69,7 +69,8 @@ fun main() = singleWindowApplication( onDrag = { }, - onDrop = { data -> + onDrop = { state -> + val data = state.data text = data.toString() if (data is DropData.Image) { painter = data.readImage() diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 53efdee65e63e..1eac464b68704 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragState import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.painter.Painter @@ -89,6 +90,12 @@ interface DropData { } } +@ExperimentalComposeUiApi +interface ExternalDragState { + val dragPosition: Offset + val data: DropData +} + /** * Adds detector of external drag and drop (e.g. files DnD from Finder to an application) * @@ -101,10 +108,10 @@ interface DropData { @Composable fun Modifier.onExternalDrag( enabled: Boolean = true, - onDragStart: (Offset) -> Unit = {}, - onDrag: (Offset) -> Unit = {}, + onDragStart: (ExternalDragState) -> Unit = {}, + onDrag: (ExternalDragState) -> Unit = {}, onDragCancel: () -> Unit = {}, - onDrop: (DropData) -> Unit = {}, + onDrop: (ExternalDragState) -> Unit = {}, ): Modifier = composed { if (!enabled) { return@composed Modifier @@ -176,55 +183,58 @@ internal class AwtWindowDropTarget( // bounds of all components that are subscribed to external drag and drop for the window private val componentBoundsHolder = mutableMapOf() - // drag coordinates used to detect that drag entered/exited components - private var windowDragCoordinates: Offset? = null + // state of ongoing external drag and drop in the [window], contains pointer coordinates and data that is dragged + private var currentDragState: WindowDragState? = null val dragTargetListener = AwtWindowDragTargetListener( window, // notify components on window border that drag is started. - onDragEnterWindow = { newWindowDragCoordinates -> + onDragEnterWindow = { newDragState -> + currentDragState = newDragState forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragCoordinates = null, currentDragCoordinates = newWindowDragCoordinates + oldDragState = null, currentDragState = newDragState, ) } - windowDragCoordinates = newWindowDragCoordinates }, // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them - onDragInsideWindow = { newWindowDragCoordinates -> - val oldDragCoordinates = windowDragCoordinates - windowDragCoordinates = newWindowDragCoordinates + onDragInsideWindow = { newDragState -> + val oldDragState = currentDragState + currentDragState = newDragState forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragCoordinates, newWindowDragCoordinates + oldDragState, newDragState ) } }, // notify components on window border drag exited window onDragExit = { - val oldDragCoordinates = windowDragCoordinates - windowDragCoordinates = null + val oldDragState = currentDragState + currentDragState = null forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragCoordinates = oldDragCoordinates, currentDragCoordinates = null + oldDragState = oldDragState, currentDragState = null ) } }, // notify all components under the pointer that drop happened - onDrop = { + onDrop = { newDragState -> var anyDrops = false - val dropCoordinates = windowDragCoordinates - windowDragCoordinates = null + currentDragState = null forEachPositionedComponent { handler, componentBounds -> - val isInside = isExternalDragInsideComponent(componentBounds, dropCoordinates) + val isInside = isExternalDragInsideComponent( + componentBounds, + newDragState.dragPositionInWindow + ) if (isInside) { - handler.onDrop(it) + val offset = calculateOffset(componentBounds, newDragState.dragPositionInWindow) + handler.onDrop(ExternalDragStateImpl(offset, newDragState.data)) anyDrops = true } } @@ -240,7 +250,7 @@ internal class AwtWindowDropTarget( override fun setActive(isActive: Boolean) { super.setActive(isActive) if (!isActive) { - windowDragCoordinates = null + currentDragState = null } } @@ -274,7 +284,7 @@ internal class AwtWindowDropTarget( val handler = handlers.remove(handleId) val componentBounds = componentBoundsHolder.remove(handleId) if (handler != null && componentBounds != null && - isExternalDragInsideComponent(componentBounds, windowDragCoordinates) + isExternalDragInsideComponent(componentBounds, currentDragState?.dragPositionInWindow) ) { handler.value.onDragCancel() } @@ -298,8 +308,8 @@ internal class AwtWindowDropTarget( val oldComponentBounds = componentBoundsHolder.put(handleId, newComponentBounds) handleDragEvent( handler.value, oldComponentBounds, newComponentBounds, - oldDragCoordinates = windowDragCoordinates, - currentDragCoordinates = windowDragCoordinates + oldDragState = currentDragState, + currentDragState = currentDragState ) } @@ -311,12 +321,17 @@ internal class AwtWindowDropTarget( } data class ComponentDragHandler( - val onDragStart: (Offset) -> Unit, - val onDrag: (Offset) -> Unit, + val onDragStart: (ExternalDragState) -> Unit, + val onDrag: (ExternalDragState) -> Unit, val onDragCancel: () -> Unit, - val onDrop: (DropData) -> Unit + val onDrop: (ExternalDragState) -> Unit ) + private data class ExternalDragStateImpl( + override val dragPosition: Offset, + override val data: DropData + ) : ExternalDragState + companion object { private fun isExternalDragInsideComponent( componentBounds: Rect?, @@ -345,16 +360,23 @@ internal class AwtWindowDropTarget( handler: ComponentDragHandler, oldComponentBounds: Rect?, currentComponentBounds: Rect?, - oldDragCoordinates: Offset?, - currentDragCoordinates: Offset? + oldDragState: WindowDragState?, + currentDragState: WindowDragState?, ) { - val wasDragInside = - isExternalDragInsideComponent(oldComponentBounds, oldDragCoordinates) - val newIsDragInside = - isExternalDragInsideComponent(currentComponentBounds, currentDragCoordinates) + val wasDragInside = isExternalDragInsideComponent( + oldComponentBounds, + oldDragState?.dragPositionInWindow + ) + val newIsDragInside = isExternalDragInsideComponent( + currentComponentBounds, + currentDragState?.dragPositionInWindow + ) if (!wasDragInside && newIsDragInside) { - val dragOffset = calculateOffset(currentComponentBounds!!, currentDragCoordinates!!) - handler.onDragStart(dragOffset) + val dragOffset = calculateOffset( + currentComponentBounds!!, + currentDragState!!.dragPositionInWindow + ) + handler.onDragStart(ExternalDragStateImpl(dragOffset, currentDragState.data)) return } @@ -364,8 +386,11 @@ internal class AwtWindowDropTarget( } if (newIsDragInside) { - val dragOffset = calculateOffset(currentComponentBounds!!, currentDragCoordinates!!) - handler.onDrag(dragOffset) + val dragOffset = calculateOffset( + currentComponentBounds!!, + currentDragState!!.dragPositionInWindow + ) + handler.onDrag(ExternalDragStateImpl(dragOffset, currentDragState.data)) return } } @@ -375,19 +400,29 @@ internal class AwtWindowDropTarget( @OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDragTargetListener( private val window: Window, - val onDragEnterWindow: (Offset) -> Unit, - val onDragInsideWindow: (Offset) -> Unit, + val onDragEnterWindow: (WindowDragState) -> Unit, + val onDragInsideWindow: (WindowDragState) -> Unit, val onDragExit: () -> Unit, - val onDrop: (DropData) -> Boolean, + val onDrop: (WindowDragState) -> Boolean, ) : DropTargetListener { private val density = window.density.density override fun dragEnter(dtde: DropTargetDragEvent) { - onDragEnterWindow(dtde.location.windowOffset()) + onDragEnterWindow( + WindowDragState( + dtde.location.windowOffset(), + dtde.transferable.dropData() + ) + ) } override fun dragOver(dtde: DropTargetDragEvent) { - onDragInsideWindow(dtde.location.windowOffset()) + onDragInsideWindow( + WindowDragState( + dtde.location.windowOffset(), + dtde.transferable.dropData() + ) + ) } // takes title bar and other insets into account @@ -411,17 +446,16 @@ internal class AwtWindowDragTargetListener( val transferable = dtde.transferable try { - val dropData = transferable.dropData() ?: run { - onDragExit() - dtde.dropComplete(false) - return - } - onDrop(dropData) + onDrop(WindowDragState(dtde.location.windowOffset(), transferable.dropData())) dtde.dropComplete(true) - return } catch (e: Exception) { onDragExit() dtde.dropComplete(false) } } + + data class WindowDragState( + val dragPositionInWindow: Offset, + val data: DropData + ) } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt index 6acf5c5542a3f..63e0ef8ea5dba 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt @@ -26,7 +26,7 @@ import java.awt.image.BufferedImage import java.io.File @OptIn(ExperimentalComposeUiApi::class) -internal fun Transferable.dropData(): DropData? { +internal fun Transferable.dropData(): DropData { val mimeTypes = transferDataFlavors.map { it.mimeType } val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) @@ -38,10 +38,13 @@ internal fun Transferable.dropData(): DropData? { bestTextFlavor != null -> DropDataTextImpl(mimeTypes, bestTextFlavor, this) - else -> null + else -> UnknownDropData(mimeTypes) } } +@OptIn(ExperimentalComposeUiApi::class) +private class UnknownDropData(override val mimeTypes: List) : DropData + @OptIn(ExperimentalComposeUiApi::class) private class DropDataFilesListImpl( override val mimeTypes: List, diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index e591096451b85..9526a995b3547 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragState import androidx.compose.ui.ExternalDragTest.TestDragEvent.Drag import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragCancelled import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragStarted @@ -73,14 +74,14 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(Offset(50f, 50f)) + onDragEnterWindow(TestWindowDragState(Offset(50f, 50f))) } awaitIdle() assertThat(events.size).isEqualTo(1) assertThat(events.last()).isEqualTo(DragStarted(Offset(50f, 50f))) window.dragEvents { - onDragInsideWindow(Offset(70f, 70f)) + onDragInsideWindow(TestWindowDragState(Offset(70f, 70f))) } awaitIdle() @@ -121,13 +122,13 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(Offset(10f, 10f)) + onDragEnterWindow(TestWindowDragState(Offset(10f, 10f))) } awaitIdle() assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragInsideWindow(Offset(70f, componentYOffset + 1f)) + onDragInsideWindow(TestWindowDragState(Offset(70f, componentYOffset + 1f))) } awaitIdle() @@ -172,7 +173,7 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(Offset(10f, 10f)) + onDragEnterWindow(TestWindowDragState(Offset(10f, 10f))) } awaitIdle() assertThat(eventsComponent1.size).isEqualTo(1) @@ -181,7 +182,7 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(0) window.dragEvents { - onDragInsideWindow(Offset(70f, component2YOffset + 1f)) + onDragInsideWindow(TestWindowDragState(Offset(70f, component2YOffset + 1f))) } awaitIdle() @@ -193,14 +194,14 @@ class ExternalDragTest { val dropData = createTextDropData("Text") window.dragEvents { - onDrop(dropData) + onDrop(TestWindowDragState(Offset(70f, component2YOffset + 1f), dropData)) } awaitIdle() assertThat(eventsComponent1.size).isEqualTo(2) assertThat(eventsComponent2.size).isEqualTo(2) - assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(dropData)) + assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(Offset(70f, 1f), dropData)) exitApplication() } @@ -258,7 +259,7 @@ class ExternalDragTest { .onExternalDrag( onDragStart = { // make box bigger on enter - events.add(DragStarted(it)) + events.add(DragStarted(it.dragPosition)) width = 100.dp }, onDragCancel = { @@ -276,7 +277,7 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(Offset(25f, 25f)) + onDragEnterWindow(TestWindowDragState(Offset(25f, 25f))) } // only one event should be handled -- drag started, even if the component become bigger @@ -297,13 +298,13 @@ class ExternalDragTest { private fun Modifier.saveExternalDragEvents(events: MutableList): Modifier { return this.onExternalDrag( onDragStart = { - events.add(DragStarted(it)) + events.add(DragStarted(it.dragPosition, it.data)) }, onDrop = { - events.add(TestDragEvent.Drop(it)) + events.add(TestDragEvent.Drop(it.dragPosition, it.data)) }, onDrag = { - events.add(Drag(it)) + events.add(Drag(it.dragPosition, it.data)) }, onDragCancel = { events.add(DragCancelled) @@ -322,10 +323,29 @@ class ExternalDragTest { } } + private fun TestWindowDragState(offset: Offset, data: DropData = testDropData): WindowDragState { + return WindowDragState(offset, data) + } + private sealed interface TestDragEvent { - data class DragStarted(val offset: Offset) : TestDragEvent + data class DragStarted( + val offset: Offset, + val data: DropData = testDropData + ) : TestDragEvent + object DragCancelled : TestDragEvent - data class Drag(val offset: Offset) : TestDragEvent - data class Drop(val data: DropData) : TestDragEvent + data class Drag(val offset: Offset, val data: DropData = testDropData) : TestDragEvent + data class Drop(val offset: Offset, val data: DropData = testDropData) : TestDragEvent + } + + companion object { + private val testDropData = object : DropData.Text { + override val mimeTypes: List = listOf("text/plain") + override val bestMimeType: String = mimeTypes.first() + + override fun readText(): String { + return "Test text" + } + } } } \ No newline at end of file From 5f650fb597635c5068ad67ca35cab4b0d4747cae Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 10 Mar 2023 14:55:03 +0100 Subject: [PATCH 25/31] Rename onDragCancel -> onDragExit --- .../androidx/compose/desktop/examples/dnd/Main.jvm.kt | 2 +- .../kotlin/androidx/compose/ui/ExternalDrag.desktop.kt | 6 +++--- .../kotlin/androidx/compose/ui/ExternalDragTest.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index 007d8dc36dad7..593ad499936d2 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -63,7 +63,7 @@ fun main() = singleWindowApplication( onDragStart = { isDragging = true }, - onDragCancel = { + onDragExit = { isDragging = false }, onDrag = { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 1eac464b68704..ccbd6a68e8d89 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -102,7 +102,7 @@ interface ExternalDragState { * @param onDragStart will be called when the pointer with external content entered the component. * @param onDrag will be called for all drag events inside the component. * @param onDrop is called when the pointer is released with [DropData] the pointer held. - * @param onDragCancel is called if the pointer exited the component bounds or unknown data was dropped. + * @param onDragExit is called if the pointer exited the component bounds */ @ExperimentalComposeUiApi @Composable @@ -110,7 +110,7 @@ fun Modifier.onExternalDrag( enabled: Boolean = true, onDragStart: (ExternalDragState) -> Unit = {}, onDrag: (ExternalDragState) -> Unit = {}, - onDragCancel: () -> Unit = {}, + onDragExit: () -> Unit = {}, onDrop: (ExternalDragState) -> Unit = {}, ): Modifier = composed { if (!enabled) { @@ -119,7 +119,7 @@ fun Modifier.onExternalDrag( val window = LocalWindow.current ?: return@composed Modifier val componentDragHandler = rememberUpdatedState( - AwtWindowDropTarget.ComponentDragHandler(onDragStart, onDrag, onDragCancel, onDrop) + AwtWindowDropTarget.ComponentDragHandler(onDragStart, onDrag, onDragExit, onDrop) ) var componentDragHandleId by remember { mutableStateOf(null) } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index 9526a995b3547..b1eb3627565a7 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -262,7 +262,7 @@ class ExternalDragTest { events.add(DragStarted(it.dragPosition)) width = 100.dp }, - onDragCancel = { + onDragExit = { // make box smalled when drag exited events.add(DragCancelled) width = 50.dp @@ -306,7 +306,7 @@ class ExternalDragTest { onDrag = { events.add(Drag(it.dragPosition, it.data)) }, - onDragCancel = { + onDragExit = { events.add(DragCancelled) } ) From 60e7b65e371fb0ecebb0c23540c71ab2beb06ea7 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 10 Mar 2023 15:14:58 +0100 Subject: [PATCH 26/31] Rename DropData -> DragData and update javadocs --- .../compose/desktop/examples/dnd/Main.jvm.kt | 10 ++-- .../compose/ui/ExternalDrag.desktop.kt | 49 ++++++++++++------- .../compose/ui/ExternalDragAwtExtensions.kt | 24 ++++----- .../androidx/compose/ui/ExternalDragTest.kt | 28 +++++------ 4 files changed, 62 insertions(+), 49 deletions(-) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt index 593ad499936d2..7a0c471d71180 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/dnd/Main.jvm.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.DropData +import androidx.compose.ui.DragData import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -70,10 +70,10 @@ fun main() = singleWindowApplication( }, onDrop = { state -> - val data = state.data - text = data.toString() - if (data is DropData.Image) { - painter = data.readImage() + val dragData = state.dragData + text = dragData.toString() + if (dragData is DragData.Image) { + painter = dragData.readImage() } isDragging = false }) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index ccbd6a68e8d89..f8ea7f0e34101 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -42,12 +42,12 @@ import java.awt.dnd.DropTargetEvent import java.awt.dnd.DropTargetListener /** - * Represent data types drag and dropped to a component from outside an application. + * Represent data that is being dragged (or dropped) to a component from outside an application. */ @ExperimentalComposeUiApi -interface DropData { +interface DragData { /** - * List of all MIME types for this [DropData]. + * List of all MIME types for this [DragData]. * The list is ordered from most richly descriptive to least descriptive. * * Some platform-specific mimeTypes can occur in the list. It may be changed in future versions. @@ -57,7 +57,7 @@ interface DropData { /** * Represents list of files drag and dropped to a component. */ - interface FilesList : DropData { + interface FilesList : DragData { /** * Returns list of file paths drag and droppped to an application in a URI format. */ @@ -67,7 +67,7 @@ interface DropData { /** * Represents an image drag and dropped to a component. */ - interface Image : DropData { + interface Image : DragData { /** * Returns an image drag and dropped to an application as a [Painter] type. */ @@ -77,7 +77,7 @@ interface DropData { /** * Represent text drag and dropped to a component. */ - interface Text : DropData { + interface Text : DragData { /** * Provides the best MIME type that describes text returned in [readText] */ @@ -90,19 +90,32 @@ interface DropData { } } +/** + * Represent the current state of drag and drop to a component from outside an application. + * This state is passed to external drag callbacks. + * + * @see onExternalDrag + */ @ExperimentalComposeUiApi interface ExternalDragState { + /** + * Position of the pointer relative to the component + */ val dragPosition: Offset - val data: DropData + + /** + * Data that it being dragged (or dropped) in a component bounds + */ + val dragData: DragData } /** * Adds detector of external drag and drop (e.g. files DnD from Finder to an application) * * @param onDragStart will be called when the pointer with external content entered the component. - * @param onDrag will be called for all drag events inside the component. - * @param onDrop is called when the pointer is released with [DropData] the pointer held. - * @param onDragExit is called if the pointer exited the component bounds + * @param onDrag will be called for pointer movements inside the component. + * @param onDragExit is called if the pointer exited the component bounds. + * @param onDrop is called when the pointer is released. */ @ExperimentalComposeUiApi @Composable @@ -234,7 +247,7 @@ internal class AwtWindowDropTarget( ) if (isInside) { val offset = calculateOffset(componentBounds, newDragState.dragPositionInWindow) - handler.onDrop(ExternalDragStateImpl(offset, newDragState.data)) + handler.onDrop(ExternalDragStateImpl(offset, newDragState.dragData)) anyDrops = true } } @@ -329,7 +342,7 @@ internal class AwtWindowDropTarget( private data class ExternalDragStateImpl( override val dragPosition: Offset, - override val data: DropData + override val dragData: DragData ) : ExternalDragState companion object { @@ -376,7 +389,7 @@ internal class AwtWindowDropTarget( currentComponentBounds!!, currentDragState!!.dragPositionInWindow ) - handler.onDragStart(ExternalDragStateImpl(dragOffset, currentDragState.data)) + handler.onDragStart(ExternalDragStateImpl(dragOffset, currentDragState.dragData)) return } @@ -390,7 +403,7 @@ internal class AwtWindowDropTarget( currentComponentBounds!!, currentDragState!!.dragPositionInWindow ) - handler.onDrag(ExternalDragStateImpl(dragOffset, currentDragState.data)) + handler.onDrag(ExternalDragStateImpl(dragOffset, currentDragState.dragData)) return } } @@ -411,7 +424,7 @@ internal class AwtWindowDragTargetListener( onDragEnterWindow( WindowDragState( dtde.location.windowOffset(), - dtde.transferable.dropData() + dtde.transferable.dragData() ) ) } @@ -420,7 +433,7 @@ internal class AwtWindowDragTargetListener( onDragInsideWindow( WindowDragState( dtde.location.windowOffset(), - dtde.transferable.dropData() + dtde.transferable.dragData() ) ) } @@ -446,7 +459,7 @@ internal class AwtWindowDragTargetListener( val transferable = dtde.transferable try { - onDrop(WindowDragState(dtde.location.windowOffset(), transferable.dropData())) + onDrop(WindowDragState(dtde.location.windowOffset(), transferable.dragData())) dtde.dropComplete(true) } catch (e: Exception) { onDragExit() @@ -456,6 +469,6 @@ internal class AwtWindowDragTargetListener( data class WindowDragState( val dragPositionInWindow: Offset, - val data: DropData + val dragData: DragData ) } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt index 63e0ef8ea5dba..580dc140259ff 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt @@ -26,30 +26,30 @@ import java.awt.image.BufferedImage import java.io.File @OptIn(ExperimentalComposeUiApi::class) -internal fun Transferable.dropData(): DropData { +internal fun Transferable.dragData(): DragData { val mimeTypes = transferDataFlavors.map { it.mimeType } val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) return when { isDataFlavorSupported(DataFlavor.javaFileListFlavor) -> - DropDataFilesListImpl(mimeTypes, this) + DragDataFilesListImpl(mimeTypes, this) - isDataFlavorSupported(DataFlavor.imageFlavor) -> DropDataImageImpl(mimeTypes, this) + isDataFlavorSupported(DataFlavor.imageFlavor) -> DragDataImageImpl(mimeTypes, this) - bestTextFlavor != null -> DropDataTextImpl(mimeTypes, bestTextFlavor, this) + bestTextFlavor != null -> DragDataTextImpl(mimeTypes, bestTextFlavor, this) - else -> UnknownDropData(mimeTypes) + else -> UnknownDragData(mimeTypes) } } @OptIn(ExperimentalComposeUiApi::class) -private class UnknownDropData(override val mimeTypes: List) : DropData +private class UnknownDragData(override val mimeTypes: List) : DragData @OptIn(ExperimentalComposeUiApi::class) -private class DropDataFilesListImpl( +private class DragDataFilesListImpl( override val mimeTypes: List, private val transferable: Transferable -) : DropData.FilesList { +) : DragData.FilesList { override fun readFiles(): List { val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*> return files.filterIsInstance().map { it.toURI().toString() } @@ -57,10 +57,10 @@ private class DropDataFilesListImpl( } @OptIn(ExperimentalComposeUiApi::class) -private class DropDataImageImpl( +private class DragDataImageImpl( override val mimeTypes: List, private val transferable: Transferable -) : DropData.Image { +) : DragData.Image { override fun readImage(): Painter { return (transferable.getTransferData(DataFlavor.imageFlavor) as Image).painter() } @@ -84,11 +84,11 @@ private class DropDataImageImpl( } @OptIn(ExperimentalComposeUiApi::class) -private class DropDataTextImpl( +private class DragDataTextImpl( override val mimeTypes: List, private val bestTextFlavor: DataFlavor, private val transferable: Transferable -) : DropData.Text { +) : DragData.Text { override val bestMimeType: String = bestTextFlavor.mimeType override fun readText(): String { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index b1eb3627565a7..4cac696a6480f 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -192,16 +192,16 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(1) assertThat(eventsComponent2.last()).isEqualTo(DragStarted(Offset(70f, 1f))) - val dropData = createTextDropData("Text") + val dragData = createTextDragData("Text") window.dragEvents { - onDrop(TestWindowDragState(Offset(70f, component2YOffset + 1f), dropData)) + onDrop(TestWindowDragState(Offset(70f, component2YOffset + 1f), dragData)) } awaitIdle() assertThat(eventsComponent1.size).isEqualTo(2) assertThat(eventsComponent2.size).isEqualTo(2) - assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(Offset(70f, 1f), dropData)) + assertThat(eventsComponent2.last()).isEqualTo(TestDragEvent.Drop(Offset(70f, 1f), dragData)) exitApplication() } @@ -298,13 +298,13 @@ class ExternalDragTest { private fun Modifier.saveExternalDragEvents(events: MutableList): Modifier { return this.onExternalDrag( onDragStart = { - events.add(DragStarted(it.dragPosition, it.data)) + events.add(DragStarted(it.dragPosition, it.dragData)) }, onDrop = { - events.add(TestDragEvent.Drop(it.dragPosition, it.data)) + events.add(TestDragEvent.Drop(it.dragPosition, it.dragData)) }, onDrag = { - events.add(Drag(it.dragPosition, it.data)) + events.add(Drag(it.dragPosition, it.dragData)) }, onDragExit = { events.add(DragCancelled) @@ -312,8 +312,8 @@ class ExternalDragTest { ) } - private fun createTextDropData(text: String): DropData { - return object : DropData.Text { + private fun createTextDragData(text: String): DragData { + return object : DragData.Text { override val mimeTypes: List = listOf("text/plain") override val bestMimeType: String = mimeTypes.first() @@ -323,23 +323,23 @@ class ExternalDragTest { } } - private fun TestWindowDragState(offset: Offset, data: DropData = testDropData): WindowDragState { - return WindowDragState(offset, data) + private fun TestWindowDragState(offset: Offset, dragData: DragData = testDragData): WindowDragState { + return WindowDragState(offset, dragData) } private sealed interface TestDragEvent { data class DragStarted( val offset: Offset, - val data: DropData = testDropData + val dragData: DragData = testDragData ) : TestDragEvent object DragCancelled : TestDragEvent - data class Drag(val offset: Offset, val data: DropData = testDropData) : TestDragEvent - data class Drop(val offset: Offset, val data: DropData = testDropData) : TestDragEvent + data class Drag(val offset: Offset, val dragData: DragData = testDragData) : TestDragEvent + data class Drop(val offset: Offset, val dragData: DragData = testDragData) : TestDragEvent } companion object { - private val testDropData = object : DropData.Text { + private val testDragData = object : DragData.Text { override val mimeTypes: List = listOf("text/plain") override val bestMimeType: String = mimeTypes.first() From 6a9c42b55bdaf2a655f2c681d2dfa2ba1ffb0c72 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 13 Mar 2023 10:47:00 +0100 Subject: [PATCH 27/31] Don't provide mimeTypes, it is not needed --- .../compose/ui/ExternalDrag.desktop.kt | 8 ------- .../compose/ui/ExternalDragAwtExtensions.kt | 14 ++++------- .../androidx/compose/ui/ExternalDragTest.kt | 24 +++++++------------ 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index f8ea7f0e34101..4a40379b011cb 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -46,14 +46,6 @@ import java.awt.dnd.DropTargetListener */ @ExperimentalComposeUiApi interface DragData { - /** - * List of all MIME types for this [DragData]. - * The list is ordered from most richly descriptive to least descriptive. - * - * Some platform-specific mimeTypes can occur in the list. It may be changed in future versions. - */ - val mimeTypes: List - /** * Represents list of files drag and dropped to a component. */ diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt index 580dc140259ff..0e3abf6bfac35 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDragAwtExtensions.kt @@ -27,27 +27,25 @@ import java.io.File @OptIn(ExperimentalComposeUiApi::class) internal fun Transferable.dragData(): DragData { - val mimeTypes = transferDataFlavors.map { it.mimeType } val bestTextFlavor = selectBestTextFlavor(transferDataFlavors) return when { isDataFlavorSupported(DataFlavor.javaFileListFlavor) -> - DragDataFilesListImpl(mimeTypes, this) + DragDataFilesListImpl(this) - isDataFlavorSupported(DataFlavor.imageFlavor) -> DragDataImageImpl(mimeTypes, this) + isDataFlavorSupported(DataFlavor.imageFlavor) -> DragDataImageImpl(this) - bestTextFlavor != null -> DragDataTextImpl(mimeTypes, bestTextFlavor, this) + bestTextFlavor != null -> DragDataTextImpl(bestTextFlavor, this) - else -> UnknownDragData(mimeTypes) + else -> UnknownDragData } } @OptIn(ExperimentalComposeUiApi::class) -private class UnknownDragData(override val mimeTypes: List) : DragData +private object UnknownDragData : DragData @OptIn(ExperimentalComposeUiApi::class) private class DragDataFilesListImpl( - override val mimeTypes: List, private val transferable: Transferable ) : DragData.FilesList { override fun readFiles(): List { @@ -58,7 +56,6 @@ private class DragDataFilesListImpl( @OptIn(ExperimentalComposeUiApi::class) private class DragDataImageImpl( - override val mimeTypes: List, private val transferable: Transferable ) : DragData.Image { override fun readImage(): Painter { @@ -85,7 +82,6 @@ private class DragDataImageImpl( @OptIn(ExperimentalComposeUiApi::class) private class DragDataTextImpl( - override val mimeTypes: List, private val bestTextFlavor: DataFlavor, private val transferable: Transferable ) : DragData.Text { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index 4cac696a6480f..18aacda0de6fb 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -312,17 +312,6 @@ class ExternalDragTest { ) } - private fun createTextDragData(text: String): DragData { - return object : DragData.Text { - override val mimeTypes: List = listOf("text/plain") - override val bestMimeType: String = mimeTypes.first() - - override fun readText(): String { - return text - } - } - } - private fun TestWindowDragState(offset: Offset, dragData: DragData = testDragData): WindowDragState { return WindowDragState(offset, dragData) } @@ -339,12 +328,15 @@ class ExternalDragTest { } companion object { - private val testDragData = object : DragData.Text { - override val mimeTypes: List = listOf("text/plain") - override val bestMimeType: String = mimeTypes.first() + private val testDragData = createTextDragData("Test text") + + private fun createTextDragData(text: String): DragData { + return object : DragData.Text { + override val bestMimeType: String = "text/plain" - override fun readText(): String { - return "Test text" + override fun readText(): String { + return text + } } } } From 5b2dac417138174b18453174c1e469fabc5d18d6 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 13 Mar 2023 11:02:58 +0100 Subject: [PATCH 28/31] Rename State -> Value --- .../compose/ui/ExternalDrag.desktop.kt | 90 +++++++++---------- .../androidx/compose/ui/ExternalDragTest.kt | 22 ++--- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 4a40379b011cb..d2fe439f46956 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue -import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragState +import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.painter.Painter @@ -89,7 +89,7 @@ interface DragData { * @see onExternalDrag */ @ExperimentalComposeUiApi -interface ExternalDragState { +interface ExternalDragValue { /** * Position of the pointer relative to the component */ @@ -113,10 +113,10 @@ interface ExternalDragState { @Composable fun Modifier.onExternalDrag( enabled: Boolean = true, - onDragStart: (ExternalDragState) -> Unit = {}, - onDrag: (ExternalDragState) -> Unit = {}, + onDragStart: (ExternalDragValue) -> Unit = {}, + onDrag: (ExternalDragValue) -> Unit = {}, onDragExit: () -> Unit = {}, - onDrop: (ExternalDragState) -> Unit = {}, + onDrop: (ExternalDragValue) -> Unit = {}, ): Modifier = composed { if (!enabled) { return@composed Modifier @@ -189,57 +189,57 @@ internal class AwtWindowDropTarget( private val componentBoundsHolder = mutableMapOf() // state of ongoing external drag and drop in the [window], contains pointer coordinates and data that is dragged - private var currentDragState: WindowDragState? = null + private var currentDragValue: WindowDragValue? = null val dragTargetListener = AwtWindowDragTargetListener( window, // notify components on window border that drag is started. - onDragEnterWindow = { newDragState -> - currentDragState = newDragState + onDragEnterWindow = { newDragValue -> + currentDragValue = newDragValue forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragState = null, currentDragState = newDragState, + oldDragValue = null, currentDragValue = newDragValue, ) } }, // drag moved inside window, we should calculate whether drag entered/exited components or just moved inside them - onDragInsideWindow = { newDragState -> - val oldDragState = currentDragState - currentDragState = newDragState + onDragInsideWindow = { newDragValue -> + val oldDragValue = currentDragValue + currentDragValue = newDragValue forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragState, newDragState + oldDragValue, newDragValue ) } }, // notify components on window border drag exited window onDragExit = { - val oldDragState = currentDragState - currentDragState = null + val oldDragValue = currentDragValue + currentDragValue = null forEachPositionedComponent { handler, componentBounds -> handleDragEvent( handler, oldComponentBounds = componentBounds, currentComponentBounds = componentBounds, - oldDragState = oldDragState, currentDragState = null + oldDragValue = oldDragValue, currentDragValue = null ) } }, // notify all components under the pointer that drop happened - onDrop = { newDragState -> + onDrop = { newDragValue -> var anyDrops = false - currentDragState = null + currentDragValue = null forEachPositionedComponent { handler, componentBounds -> val isInside = isExternalDragInsideComponent( componentBounds, - newDragState.dragPositionInWindow + newDragValue.dragPositionInWindow ) if (isInside) { - val offset = calculateOffset(componentBounds, newDragState.dragPositionInWindow) - handler.onDrop(ExternalDragStateImpl(offset, newDragState.dragData)) + val offset = calculateOffset(componentBounds, newDragValue.dragPositionInWindow) + handler.onDrop(ExternalDragValueImpl(offset, newDragValue.dragData)) anyDrops = true } } @@ -255,7 +255,7 @@ internal class AwtWindowDropTarget( override fun setActive(isActive: Boolean) { super.setActive(isActive) if (!isActive) { - currentDragState = null + currentDragValue = null } } @@ -289,7 +289,7 @@ internal class AwtWindowDropTarget( val handler = handlers.remove(handleId) val componentBounds = componentBoundsHolder.remove(handleId) if (handler != null && componentBounds != null && - isExternalDragInsideComponent(componentBounds, currentDragState?.dragPositionInWindow) + isExternalDragInsideComponent(componentBounds, currentDragValue?.dragPositionInWindow) ) { handler.value.onDragCancel() } @@ -313,8 +313,8 @@ internal class AwtWindowDropTarget( val oldComponentBounds = componentBoundsHolder.put(handleId, newComponentBounds) handleDragEvent( handler.value, oldComponentBounds, newComponentBounds, - oldDragState = currentDragState, - currentDragState = currentDragState + oldDragValue = currentDragValue, + currentDragValue = currentDragValue ) } @@ -326,16 +326,16 @@ internal class AwtWindowDropTarget( } data class ComponentDragHandler( - val onDragStart: (ExternalDragState) -> Unit, - val onDrag: (ExternalDragState) -> Unit, + val onDragStart: (ExternalDragValue) -> Unit, + val onDrag: (ExternalDragValue) -> Unit, val onDragCancel: () -> Unit, - val onDrop: (ExternalDragState) -> Unit + val onDrop: (ExternalDragValue) -> Unit ) - private data class ExternalDragStateImpl( + private data class ExternalDragValueImpl( override val dragPosition: Offset, override val dragData: DragData - ) : ExternalDragState + ) : ExternalDragValue companion object { private fun isExternalDragInsideComponent( @@ -365,23 +365,23 @@ internal class AwtWindowDropTarget( handler: ComponentDragHandler, oldComponentBounds: Rect?, currentComponentBounds: Rect?, - oldDragState: WindowDragState?, - currentDragState: WindowDragState?, + oldDragValue: WindowDragValue?, + currentDragValue: WindowDragValue?, ) { val wasDragInside = isExternalDragInsideComponent( oldComponentBounds, - oldDragState?.dragPositionInWindow + oldDragValue?.dragPositionInWindow ) val newIsDragInside = isExternalDragInsideComponent( currentComponentBounds, - currentDragState?.dragPositionInWindow + currentDragValue?.dragPositionInWindow ) if (!wasDragInside && newIsDragInside) { val dragOffset = calculateOffset( currentComponentBounds!!, - currentDragState!!.dragPositionInWindow + currentDragValue!!.dragPositionInWindow ) - handler.onDragStart(ExternalDragStateImpl(dragOffset, currentDragState.dragData)) + handler.onDragStart(ExternalDragValueImpl(dragOffset, currentDragValue.dragData)) return } @@ -393,9 +393,9 @@ internal class AwtWindowDropTarget( if (newIsDragInside) { val dragOffset = calculateOffset( currentComponentBounds!!, - currentDragState!!.dragPositionInWindow + currentDragValue!!.dragPositionInWindow ) - handler.onDrag(ExternalDragStateImpl(dragOffset, currentDragState.dragData)) + handler.onDrag(ExternalDragValueImpl(dragOffset, currentDragValue.dragData)) return } } @@ -405,16 +405,16 @@ internal class AwtWindowDropTarget( @OptIn(ExperimentalComposeUiApi::class) internal class AwtWindowDragTargetListener( private val window: Window, - val onDragEnterWindow: (WindowDragState) -> Unit, - val onDragInsideWindow: (WindowDragState) -> Unit, + val onDragEnterWindow: (WindowDragValue) -> Unit, + val onDragInsideWindow: (WindowDragValue) -> Unit, val onDragExit: () -> Unit, - val onDrop: (WindowDragState) -> Boolean, + val onDrop: (WindowDragValue) -> Boolean, ) : DropTargetListener { private val density = window.density.density override fun dragEnter(dtde: DropTargetDragEvent) { onDragEnterWindow( - WindowDragState( + WindowDragValue( dtde.location.windowOffset(), dtde.transferable.dragData() ) @@ -423,7 +423,7 @@ internal class AwtWindowDragTargetListener( override fun dragOver(dtde: DropTargetDragEvent) { onDragInsideWindow( - WindowDragState( + WindowDragValue( dtde.location.windowOffset(), dtde.transferable.dragData() ) @@ -451,7 +451,7 @@ internal class AwtWindowDragTargetListener( val transferable = dtde.transferable try { - onDrop(WindowDragState(dtde.location.windowOffset(), transferable.dragData())) + onDrop(WindowDragValue(dtde.location.windowOffset(), transferable.dragData())) dtde.dropComplete(true) } catch (e: Exception) { onDragExit() @@ -459,7 +459,7 @@ internal class AwtWindowDragTargetListener( } } - data class WindowDragState( + data class WindowDragValue( val dragPositionInWindow: Offset, val dragData: DragData ) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt index 18aacda0de6fb..3d4b199b82f23 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/ExternalDragTest.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragState +import androidx.compose.ui.AwtWindowDragTargetListener.WindowDragValue import androidx.compose.ui.ExternalDragTest.TestDragEvent.Drag import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragCancelled import androidx.compose.ui.ExternalDragTest.TestDragEvent.DragStarted @@ -74,14 +74,14 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(TestWindowDragState(Offset(50f, 50f))) + onDragEnterWindow(TestWindowDragValue(Offset(50f, 50f))) } awaitIdle() assertThat(events.size).isEqualTo(1) assertThat(events.last()).isEqualTo(DragStarted(Offset(50f, 50f))) window.dragEvents { - onDragInsideWindow(TestWindowDragState(Offset(70f, 70f))) + onDragInsideWindow(TestWindowDragValue(Offset(70f, 70f))) } awaitIdle() @@ -122,13 +122,13 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(TestWindowDragState(Offset(10f, 10f))) + onDragEnterWindow(TestWindowDragValue(Offset(10f, 10f))) } awaitIdle() assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragInsideWindow(TestWindowDragState(Offset(70f, componentYOffset + 1f))) + onDragInsideWindow(TestWindowDragValue(Offset(70f, componentYOffset + 1f))) } awaitIdle() @@ -173,7 +173,7 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(TestWindowDragState(Offset(10f, 10f))) + onDragEnterWindow(TestWindowDragValue(Offset(10f, 10f))) } awaitIdle() assertThat(eventsComponent1.size).isEqualTo(1) @@ -182,7 +182,7 @@ class ExternalDragTest { assertThat(eventsComponent2.size).isEqualTo(0) window.dragEvents { - onDragInsideWindow(TestWindowDragState(Offset(70f, component2YOffset + 1f))) + onDragInsideWindow(TestWindowDragValue(Offset(70f, component2YOffset + 1f))) } awaitIdle() @@ -194,7 +194,7 @@ class ExternalDragTest { val dragData = createTextDragData("Text") window.dragEvents { - onDrop(TestWindowDragState(Offset(70f, component2YOffset + 1f), dragData)) + onDrop(TestWindowDragValue(Offset(70f, component2YOffset + 1f), dragData)) } awaitIdle() @@ -277,7 +277,7 @@ class ExternalDragTest { assertThat(events.size).isEqualTo(0) window.dragEvents { - onDragEnterWindow(TestWindowDragState(Offset(25f, 25f))) + onDragEnterWindow(TestWindowDragValue(Offset(25f, 25f))) } // only one event should be handled -- drag started, even if the component become bigger @@ -312,8 +312,8 @@ class ExternalDragTest { ) } - private fun TestWindowDragState(offset: Offset, dragData: DragData = testDragData): WindowDragState { - return WindowDragState(offset, dragData) + private fun TestWindowDragValue(offset: Offset, dragData: DragData = testDragData): WindowDragValue { + return WindowDragValue(offset, dragData) } private sealed interface TestDragEvent { From 2c31a2e72c549b2ecceceb1f19ecdc68738d5cbc Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 13 Mar 2023 12:14:54 +0100 Subject: [PATCH 29/31] Convert interface to class for current drag state --- .../compose/ui/ExternalDrag.desktop.kt | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index d2fe439f46956..fc34a46c779e8 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -89,17 +89,16 @@ interface DragData { * @see onExternalDrag */ @ExperimentalComposeUiApi -interface ExternalDragValue { +class ExternalDragValue internal constructor( /** * Position of the pointer relative to the component */ - val dragPosition: Offset - + val dragPosition: Offset, /** * Data that it being dragged (or dropped) in a component bounds */ val dragData: DragData -} +) /** * Adds detector of external drag and drop (e.g. files DnD from Finder to an application) @@ -239,7 +238,7 @@ internal class AwtWindowDropTarget( ) if (isInside) { val offset = calculateOffset(componentBounds, newDragValue.dragPositionInWindow) - handler.onDrop(ExternalDragValueImpl(offset, newDragValue.dragData)) + handler.onDrop(ExternalDragValue(offset, newDragValue.dragData)) anyDrops = true } } @@ -332,11 +331,6 @@ internal class AwtWindowDropTarget( val onDrop: (ExternalDragValue) -> Unit ) - private data class ExternalDragValueImpl( - override val dragPosition: Offset, - override val dragData: DragData - ) : ExternalDragValue - companion object { private fun isExternalDragInsideComponent( componentBounds: Rect?, @@ -381,7 +375,7 @@ internal class AwtWindowDropTarget( currentComponentBounds!!, currentDragValue!!.dragPositionInWindow ) - handler.onDragStart(ExternalDragValueImpl(dragOffset, currentDragValue.dragData)) + handler.onDragStart(ExternalDragValue(dragOffset, currentDragValue.dragData)) return } @@ -395,7 +389,7 @@ internal class AwtWindowDropTarget( currentComponentBounds!!, currentDragValue!!.dragPositionInWindow ) - handler.onDrag(ExternalDragValueImpl(dragOffset, currentDragValue.dragData)) + handler.onDrag(ExternalDragValue(dragOffset, currentDragValue.dragData)) return } } From b44a3b1bc1a819e47a3f05e6fdeeb267184c909b Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 13 Mar 2023 12:17:55 +0100 Subject: [PATCH 30/31] Add immutable annotation --- .../kotlin/androidx/compose/ui/ExternalDrag.desktop.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index fc34a46c779e8..11cfc8662eacf 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -18,6 +18,7 @@ package androidx.compose.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -89,6 +90,7 @@ interface DragData { * @see onExternalDrag */ @ExperimentalComposeUiApi +@Immutable class ExternalDragValue internal constructor( /** * Position of the pointer relative to the component From 62cec60d5f66891ab63e93ecf0e01d500f04ca54 Mon Sep 17 00:00:00 2001 From: Nikolai Rykunov Date: Mon, 13 Mar 2023 16:25:09 +0100 Subject: [PATCH 31/31] Update compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt Co-authored-by: Igor Demin --- .../kotlin/androidx/compose/ui/ExternalDrag.desktop.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt index 11cfc8662eacf..593b9bf77e611 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ExternalDrag.desktop.kt @@ -91,7 +91,7 @@ interface DragData { */ @ExperimentalComposeUiApi @Immutable -class ExternalDragValue internal constructor( +class ExternalDragValue( /** * Position of the pointer relative to the component */