From b7d7fa485ff18afe96d05313b758a5d255652955 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Wed, 11 Dec 2024 14:37:29 +0100 Subject: [PATCH 1/4] Introduce WebDragAndDropManager --- .../demo/components/DragAndDropExample.web.kt | 191 +++++++++++++++++- .../ui/graphics/SkiaImageAsset.skiko.kt | 31 ++- .../ui/draganddrop/DragAndDrop.jsWasm.kt | 4 +- .../ui/draganddrop/WebDragAndDropManager.kt | 187 +++++++++++++++++ .../compose/ui/window/ComposeWindow.web.kt | 20 +- 5 files changed, 422 insertions(+), 11 deletions(-) create mode 100644 compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt diff --git a/compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/components/DragAndDropExample.web.kt b/compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/components/DragAndDropExample.web.kt index 931cf4f360bcf..85b48899405a4 100644 --- a/compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/components/DragAndDropExample.web.kt +++ b/compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/components/DragAndDropExample.web.kt @@ -16,10 +16,199 @@ package androidx.compose.mpp.demo.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.draganddrop.dragAndDropSource +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.runtime.Composable +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.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.DragAndDropTransferData +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp @Composable actual fun DragAndDropExample() { - Text("No drag and drop in web target yet") + val exportedText = "Hello, DnD!" + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + val textMeasurer = rememberTextMeasurer() + + Column(modifier = Modifier.height(300.dp), verticalArrangement = Arrangement.SpaceEvenly) { + Box( + Modifier + .width(100.dp) + .height(100.dp) + .background(Color.LightGray) + .dragAndDropSource( + drawDragDecoration = { + drawRect( + color = Color.LightGray, + topLeft = Offset(x = 0f, y = 0f), + size = Size(size.width, size.height) + ) + drawRect( + color = Color(255f, 0f, 0f, 0.5f), + topLeft = Offset(x = 50f, y = 50f), + size = Size(size.width / 2, size.height / 2) + ) + drawRect( + color = Color(0f, 255f, 0f, 0.5f), + topLeft = Offset(x = 70f, y = 70f), + size = Size(size.width / 2, size.height / 2) + ) + drawRect( + color = Color(0f, 0f, 255f, 0.5f), + topLeft = Offset(x = 90f, y = 90f), + size = Size(size.width / 2, size.height / 2) + ) + + + val textLayoutResult = textMeasurer.measure( + text = AnnotatedString(exportedText), + layoutDirection = layoutDirection, + density = this + ) + drawText( + textLayoutResult = textLayoutResult, + topLeft = Offset( + x = (size.width - textLayoutResult.size.width) / 2, + y = (size.height - textLayoutResult.size.height) / 2, + ) + ) + } + ) { offset -> + DragAndDropTransferData() + } + ) { + Text("Drag Me", Modifier.align(Alignment.Center)) + } + + Box( + Modifier + .width(100.dp) + .height(100.dp) + .background(Color.LightGray) + .dragAndDropSource( + drawDragDecoration = { + drawRect( + color = Color.Magenta, + topLeft = Offset(x = 0f, y = 0f), + size = Size(size.width, size.height) + ) + drawRect( + color = Color(0f, 0f, 255f, 0.5f), + topLeft = Offset(x = 50f, y = 50f), + size = Size(size.width / 2, size.height / 2) + ) + drawRect( + color = Color(0f, 255f, 0f, 0.5f), + topLeft = Offset(x = 70f, y = 70f), + size = Size(size.width / 2, size.height / 2) + ) + drawRect( + color = Color(255f, 0f, 0f, 0.5f), + topLeft = Offset(x = 90f, y = 90f), + size = Size(size.width / 2, size.height / 2) + ) + + + val textLayoutResult = textMeasurer.measure( + text = AnnotatedString(exportedText), + layoutDirection = layoutDirection, + density = this + ) + drawText( + textLayoutResult = textLayoutResult, + topLeft = Offset( + x = (size.width - textLayoutResult.size.width) / 2, + y = (size.height - textLayoutResult.size.height) / 2, + ) + ) + } + ) { offset -> + DragAndDropTransferData() + } + ) { + Text("Nope, Drag Me!!!", Modifier.align(Alignment.Center)) + } + + } + + var showTargetBorder by remember { mutableStateOf(false) } + var showHovered by remember { mutableStateOf(false) } + var dragCounter by remember { mutableStateOf(0) } + var targetText by remember { mutableStateOf("Drop Here") } + + val dragAndDropTarget = remember { + object: DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + showTargetBorder = true + } + + override fun onEnded(event: DragAndDropEvent) { + showTargetBorder = false + } + + override fun onMoved(event: DragAndDropEvent) { + } + + override fun onEntered(event: DragAndDropEvent) { + showHovered = true + } + + override fun onExited(event: DragAndDropEvent) { + showHovered = false + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + showHovered = false + dragCounter++ + return true + } + } + } + + Box( + Modifier + .size(200.dp) + .background(if (showHovered) Color.Magenta else Color.LightGray, shape = CircleShape) + .border(border = BorderStroke(3.dp, if (showTargetBorder) Color.Black else Color.Transparent), shape = CircleShape) + .clip(CircleShape) + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ), + contentAlignment = Alignment.Center + ) { + Text(targetText + " [" + dragCounter + "]", Modifier.align(Alignment.Center)) + } + } } \ No newline at end of file diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt index c6bb870fcd409..89e3a4118bcf2 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt @@ -71,6 +71,12 @@ fun ImageBitmap.asSkiaBitmap(): Bitmap = else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image") } +fun ImageBitmap.asByteArray(): ByteArray = + when (this) { + is SkiaBackedImageBitmap -> readPixelsAsByteArray(0, 0, this.width, this.height, 0, this.width) + else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image") + } + private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { override val colorSpace = bitmap.colorSpace.toComposeColorSpace() override val config = bitmap.colorType.toComposeConfig() @@ -79,23 +85,20 @@ private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { override val width get() = bitmap.width override fun prepareToDraw() = Unit - override fun readPixels( - buffer: IntArray, + fun readPixelsAsByteArray( startX: Int, startY: Int, width: Int, height: Int, bufferOffset: Int, stride: Int - ) { + ): ByteArray { // similar to https://cs.android.com/android/platform/superproject/+/42c50042d1f05d92ecc57baebe3326a57aeecf77:frameworks/base/graphics/java/android/graphics/Bitmap.java;l=2007 val lastScanline: Int = bufferOffset + (height - 1) * stride require(startX >= 0 && startY >= 0) require(width > 0 && startX + width <= this.width) require(height > 0 && startY + height <= this.height) require(abs(stride) >= width) - require(bufferOffset >= 0 && bufferOffset + width <= buffer.size) - require(lastScanline >= 0 && lastScanline + width <= buffer.size) // similar to https://cs.android.com/android/platform/superproject/+/9054ca2b342b2ea902839f629e820546d8a2458b:frameworks/base/libs/hwui/jni/Bitmap.cpp;l=898;bpv=1 val colorInfo = ColorInfo( @@ -105,7 +108,23 @@ private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { ) val imageInfo = ImageInfo(colorInfo, width, height) val bytesPerPixel = 4 - val bytes = bitmap.readPixels(imageInfo, stride * bytesPerPixel, startX, startY)!! + return bitmap.readPixels(imageInfo, stride * bytesPerPixel, startX, startY)!! + } + + + override fun readPixels( + buffer: IntArray, + startX: Int, + startY: Int, + width: Int, + height: Int, + bufferOffset: Int, + stride: Int + ) { + val bytes = readPixelsAsByteArray( + startX, startY, width, height, bufferOffset, stride) + + val bytesPerPixel = 4 bytes.putBytesInto(buffer, bufferOffset, bytes.size / bytesPerPixel) } } diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt index 507b000555cb1..e5698453477d8 100644 --- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt +++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt @@ -21,14 +21,14 @@ import androidx.compose.ui.geometry.Offset /** * A representation of an event sent by the platform during a drag and drop operation. */ -actual class DragAndDropEvent +actual class DragAndDropEvent(val offset: Offset) /** * Returns the position of this [DragAndDropEvent] relative to the root Compose View in the * layout hierarchy. */ internal actual val DragAndDropEvent.positionInRoot: Offset - get() = TODO("Not yet implemented") + get() = offset /** * Definition for a type representing transferable data. It could be a remote URI, diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt new file mode 100644 index 0000000000000..b2d35e7cc0f03 --- /dev/null +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt @@ -0,0 +1,187 @@ +package androidx.compose.ui.draganddrop + +import androidx.compose.ui.events.EventTargetListener +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.platform.PlatformDragAndDropManager +import androidx.compose.ui.platform.PlatformDragAndDropSource +import androidx.compose.ui.scene.ComposeSceneDragAndDropNode +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.roundToInt +import kotlinx.browser.document +import kotlinx.browser.window +import org.khronos.webgl.Uint8ClampedArray +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.DragEvent +import org.w3c.dom.HTMLCanvasElement +import org.w3c.dom.ImageData +import androidx.compose.ui.graphics.asByteArray +import org.w3c.dom.HTMLElement + +internal abstract class WebDragAndDropManager(eventListener: EventTargetListener, private val density: Density) : + PlatformDragAndDropManager { + override val isRequestDragAndDropTransferRequired: Boolean + get() = false + + abstract val rootDragAndDropNode: ComposeSceneDragAndDropNode + + private val startTransferScope = object : PlatformDragAndDropSource.StartTransferScope { + /** + * Context for an ongoing drag session initiated from Compose. + */ + var dragSessionContext: DragSessionContext? = null + + + override fun startDragAndDropTransfer( + transferData: DragAndDropTransferData, + decorationSize: Size, + drawDragDecoration: DrawScope.() -> Unit + ): Boolean { + dragSessionContext = DragSessionContext() + + val imageBitmap = ImageBitmap( + width = decorationSize.width.roundToInt(), + height = decorationSize.height.roundToInt() + ) + + // This results in blurry text for some reason. + val canvas = Canvas(imageBitmap) + val canvasScope = CanvasDrawScope() + canvasScope.draw(density, LayoutDirection.Ltr, canvas, decorationSize, drawDragDecoration) + + val byteArray = imageBitmap.asByteArray() + + val uint8ClampedArray = Uint8ClampedArray(byteArray.size) + + (0 until byteArray.size / 4).forEachIndexed { index, _ -> + val offset = index * 4 + + // red + uint8ClampedArray.set(offset, byteArray[offset + 2].toInt() and 0xFF) + + // green + uint8ClampedArray.set(offset + 1, byteArray[offset + 1].toInt() and 0xFF) + + // blue + uint8ClampedArray.set(offset + 2, byteArray[offset].toInt() and 0xFF) + + // alpha + uint8ClampedArray.set(offset + 3, byteArray[offset + 3].toInt() and 0xFF) + } + + val imageData = ImageData(uint8ClampedArray, decorationSize.width.toInt(), decorationSize.height.toInt()) + + val canvasConverter = document.createElement("canvas") as HTMLCanvasElement + + val scale = density.density + + canvasConverter.width = decorationSize.width.toInt() + canvasConverter.height = decorationSize.height.toInt() + + require(scale > 0f) + + val width = (decorationSize.width / scale).toInt() + val height = (decorationSize.height / scale).toInt() + + canvasConverter.style.width = "${width}px" + canvasConverter.style.height = "${height}px" + + val canvasConverterContext = canvasConverter.getContext("2d") as CanvasRenderingContext2D + canvasConverterContext.putImageData(imageData, 0.0, 0.0) + + dragSessionContext?.ghostImage = canvasConverter + + return true + } + } + + + init { + initEvents(eventListener) + } + + private fun DragEvent.setAsDragImage(ghostImage: HTMLElement) { + // TODO: Investigate best options to hide the element visually + with (ghostImage.style) { + position = "absolute" + + top = "0" + left = "0" + + setProperty("pointer-events", "none") + } + + // non-image elements passed to setDragImage should be present on document + // the only browser the only browser not burdened with this limitation is Firefox + document.body?.appendChild(ghostImage) + + dataTransfer?.setDragImage(ghostImage, 0, 0) + + // After browser made a snapshot we can safely remove ghostImage from document + // But it should be done in different frame + window.requestAnimationFrame { + ghostImage.remove() + } + } + + private fun initEvents(eventListener: EventTargetListener) { + eventListener.addDisposableEvent("dragstart") { event -> + event as DragEvent + + with (rootDragAndDropNode) { + startTransferScope.startDragAndDropTransfer(event.offset) { + startTransferScope.dragSessionContext != null + } + + if (startTransferScope.dragSessionContext != null) { + val dragEvent = DragAndDropEvent(event.offset) + val acceptedTransfer = acceptDragAndDropTransfer(dragEvent) + + if (acceptedTransfer) { + onStarted(dragEvent) + onEntered(dragEvent) + + startTransferScope.dragSessionContext?.ghostImage?.let { ghostImage -> + event.setAsDragImage(ghostImage) + } + } + } else { + event.preventDefault() + } + } + } + + eventListener.addDisposableEvent("drag") { event -> + event as DragEvent + rootDragAndDropNode.onMoved(DragAndDropEvent(event.offset)) + } + + eventListener.addDisposableEvent("dragend") { event -> + event as DragEvent + val dragAndDropEvent = DragAndDropEvent(event.offset) + rootDragAndDropNode.onDrop(dragAndDropEvent) + rootDragAndDropNode.onEnded(dragAndDropEvent) + + startTransferScope.dragSessionContext = null + } + } + + private val DragEvent.offset get() = Offset( + x = offsetX.toFloat(), + y = offsetY.toFloat() + ) * density.density +} + +private class DragSessionContext( + var ghostImage: HTMLCanvasElement? = null +) + +@Suppress("UNUSED_PARAMETER") +private fun setMethodImplForUint8ClampedArray(obj: Uint8ClampedArray, index: Int, value: Int) { js("obj[index] = value;") } +private operator fun Uint8ClampedArray.set(index: Int, value: Int) = setMethodImplForUint8ClampedArray(this, index, value) + diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt index 9b4f5bdee2eb9..63e68b3d47d86 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.LocalSystemTheme +import androidx.compose.ui.draganddrop.WebDragAndDropManager import androidx.compose.ui.events.EventTargetListener import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -41,10 +42,12 @@ import androidx.compose.ui.input.pointer.composeButtons import androidx.compose.ui.platform.DefaultInputModeManager import androidx.compose.ui.platform.LocalInternalViewModelStoreOwner import androidx.compose.ui.platform.PlatformContext +import androidx.compose.ui.platform.PlatformDragAndDropManager import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WebTextInputService import androidx.compose.ui.platform.WindowInfoImpl import androidx.compose.ui.scene.CanvasLayersComposeScene +import androidx.compose.ui.scene.ComposeSceneDragAndDropNode import androidx.compose.ui.scene.ComposeScenePointer import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize @@ -70,10 +73,10 @@ import org.jetbrains.skia.Canvas import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkikoRenderDelegate import org.w3c.dom.AddEventListenerOptions +import org.w3c.dom.DragEvent import org.w3c.dom.Element import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLStyleElement -import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.HTMLTitleElement import org.w3c.dom.MediaQueryListEvent import org.w3c.dom.TouchEvent @@ -182,6 +185,11 @@ internal class ComposeWindow( override val inputModeManager: InputModeManager = DefaultInputModeManager() + override val dragAndDropManager: PlatformDragAndDropManager = object : WebDragAndDropManager(canvasEvents, density) { + override val rootDragAndDropNode: ComposeSceneDragAndDropNode + get() = scene.rootDragAndDropNode + } + override val textInputService = object : WebTextInputService() { override fun getOffset(rect: Rect): Offset { @@ -304,6 +312,13 @@ internal class ComposeWindow( processKeyboardEvent(event) } + + state.globalEvents.addDisposableEvent("dragover") { event -> + event as DragEvent + event.preventDefault() + event.dataTransfer?.dropEffect = "move" + } + state.globalEvents.addDisposableEvent("focus") { lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } @@ -318,6 +333,7 @@ internal class ComposeWindow( state.init() canvas.setAttribute("tabindex", "0") + canvas.setAttribute("draggable", "true") scene.density = density @@ -567,4 +583,4 @@ fun ComposeViewport( content = content, state = DefaultWindowState(viewportContainer) ) -} +} \ No newline at end of file From f5c22ca54c549491a698694a6ba5fb893c21ad5e Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 13 Dec 2024 09:52:18 +0100 Subject: [PATCH 2/4] Change afterr PR code review --- .../ui/graphics/SkiaImageAsset.skiko.kt | 31 ++------- .../ui/draganddrop/DragAndDrop.jsWasm.kt | 2 +- .../ui/draganddrop/WebDragAndDropManager.kt | 63 +++++++++++-------- .../compose/ui/window/ComposeWindow.web.kt | 9 +-- 4 files changed, 45 insertions(+), 60 deletions(-) diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt index 89e3a4118bcf2..c6bb870fcd409 100644 --- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt +++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt @@ -71,12 +71,6 @@ fun ImageBitmap.asSkiaBitmap(): Bitmap = else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image") } -fun ImageBitmap.asByteArray(): ByteArray = - when (this) { - is SkiaBackedImageBitmap -> readPixelsAsByteArray(0, 0, this.width, this.height, 0, this.width) - else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image") - } - private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { override val colorSpace = bitmap.colorSpace.toComposeColorSpace() override val config = bitmap.colorType.toComposeConfig() @@ -85,20 +79,23 @@ private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { override val width get() = bitmap.width override fun prepareToDraw() = Unit - fun readPixelsAsByteArray( + override fun readPixels( + buffer: IntArray, startX: Int, startY: Int, width: Int, height: Int, bufferOffset: Int, stride: Int - ): ByteArray { + ) { // similar to https://cs.android.com/android/platform/superproject/+/42c50042d1f05d92ecc57baebe3326a57aeecf77:frameworks/base/graphics/java/android/graphics/Bitmap.java;l=2007 val lastScanline: Int = bufferOffset + (height - 1) * stride require(startX >= 0 && startY >= 0) require(width > 0 && startX + width <= this.width) require(height > 0 && startY + height <= this.height) require(abs(stride) >= width) + require(bufferOffset >= 0 && bufferOffset + width <= buffer.size) + require(lastScanline >= 0 && lastScanline + width <= buffer.size) // similar to https://cs.android.com/android/platform/superproject/+/9054ca2b342b2ea902839f629e820546d8a2458b:frameworks/base/libs/hwui/jni/Bitmap.cpp;l=898;bpv=1 val colorInfo = ColorInfo( @@ -108,23 +105,7 @@ private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap { ) val imageInfo = ImageInfo(colorInfo, width, height) val bytesPerPixel = 4 - return bitmap.readPixels(imageInfo, stride * bytesPerPixel, startX, startY)!! - } - - - override fun readPixels( - buffer: IntArray, - startX: Int, - startY: Int, - width: Int, - height: Int, - bufferOffset: Int, - stride: Int - ) { - val bytes = readPixelsAsByteArray( - startX, startY, width, height, bufferOffset, stride) - - val bytesPerPixel = 4 + val bytes = bitmap.readPixels(imageInfo, stride * bytesPerPixel, startX, startY)!! bytes.putBytesInto(buffer, bufferOffset, bytes.size / bytesPerPixel) } } diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt index e5698453477d8..1701d6b7a618e 100644 --- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt +++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.geometry.Offset /** * A representation of an event sent by the platform during a drag and drop operation. */ -actual class DragAndDropEvent(val offset: Offset) +actual class DragAndDropEvent internal constructor(val offset: Offset) /** * Returns the position of this [DragAndDropEvent] relative to the root Compose View in the diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt index b2d35e7cc0f03..25468dd866fcb 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt @@ -23,7 +23,7 @@ import org.w3c.dom.ImageData import androidx.compose.ui.graphics.asByteArray import org.w3c.dom.HTMLElement -internal abstract class WebDragAndDropManager(eventListener: EventTargetListener, private val density: Density) : +internal abstract class WebDragAndDropManager(eventListener: EventTargetListener, globalEventsListener: EventTargetListener, private val density: Density) : PlatformDragAndDropManager { override val isRequestDragAndDropTransferRequired: Boolean get() = false @@ -49,39 +49,24 @@ internal abstract class WebDragAndDropManager(eventListener: EventTargetListener height = decorationSize.height.roundToInt() ) - // This results in blurry text for some reason. val canvas = Canvas(imageBitmap) val canvasScope = CanvasDrawScope() - canvasScope.draw(density, LayoutDirection.Ltr, canvas, decorationSize, drawDragDecoration) - - val byteArray = imageBitmap.asByteArray() - - val uint8ClampedArray = Uint8ClampedArray(byteArray.size) - - (0 until byteArray.size / 4).forEachIndexed { index, _ -> - val offset = index * 4 - - // red - uint8ClampedArray.set(offset, byteArray[offset + 2].toInt() and 0xFF) - // green - uint8ClampedArray.set(offset + 1, byteArray[offset + 1].toInt() and 0xFF) + canvasScope.draw(density, LayoutDirection.Ltr, canvas, decorationSize, drawDragDecoration) - // blue - uint8ClampedArray.set(offset + 2, byteArray[offset].toInt() and 0xFF) + val intArray = IntArray(imageBitmap.width * imageBitmap.height) + imageBitmap.readPixels(intArray) - // alpha - uint8ClampedArray.set(offset + 3, byteArray[offset + 3].toInt() and 0xFF) - } + val uint8ClampedArray = intArray.toUint8ClampedArray() - val imageData = ImageData(uint8ClampedArray, decorationSize.width.toInt(), decorationSize.height.toInt()) + val imageData = ImageData(uint8ClampedArray, imageBitmap.width, imageBitmap.height) val canvasConverter = document.createElement("canvas") as HTMLCanvasElement val scale = density.density - canvasConverter.width = decorationSize.width.toInt() - canvasConverter.height = decorationSize.height.toInt() + canvasConverter.width = imageBitmap.width + canvasConverter.height = imageBitmap.height require(scale > 0f) @@ -102,11 +87,10 @@ internal abstract class WebDragAndDropManager(eventListener: EventTargetListener init { - initEvents(eventListener) + initEvents(eventListener, globalEventsListener) } private fun DragEvent.setAsDragImage(ghostImage: HTMLElement) { - // TODO: Investigate best options to hide the element visually with (ghostImage.style) { position = "absolute" @@ -129,7 +113,7 @@ internal abstract class WebDragAndDropManager(eventListener: EventTargetListener } } - private fun initEvents(eventListener: EventTargetListener) { + private fun initEvents(eventListener: EventTargetListener, globalEventsListener: EventTargetListener) { eventListener.addDisposableEvent("dragstart") { event -> event as DragEvent @@ -169,6 +153,12 @@ internal abstract class WebDragAndDropManager(eventListener: EventTargetListener startTransferScope.dragSessionContext = null } + + globalEventsListener.addDisposableEvent("dragover") { event -> + event as DragEvent + event.preventDefault() + event.dataTransfer?.dropEffect = "move" + } } private val DragEvent.offset get() = Offset( @@ -185,3 +175,24 @@ private class DragSessionContext( private fun setMethodImplForUint8ClampedArray(obj: Uint8ClampedArray, index: Int, value: Int) { js("obj[index] = value;") } private operator fun Uint8ClampedArray.set(index: Int, value: Int) = setMethodImplForUint8ClampedArray(this, index, value) +private fun IntArray.toUint8ClampedArray(): Uint8ClampedArray { + val uint8ClampedArray = Uint8ClampedArray(size * 4) + + forEachIndexed { index, intValue -> + val offset = index * 4 + + // red + uint8ClampedArray[offset] = (intValue shr 16) and 0xFF + + // green + uint8ClampedArray[offset + 1] = (intValue shr 8) and 0xFF + + // blue + uint8ClampedArray[offset + 2] = intValue and 0xFF + + // alpha + uint8ClampedArray[offset + 3] = (intValue shr 24) and 0xFF + } + + return uint8ClampedArray +} \ No newline at end of file diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt index 63e68b3d47d86..e44634033975b 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt @@ -185,7 +185,7 @@ internal class ComposeWindow( override val inputModeManager: InputModeManager = DefaultInputModeManager() - override val dragAndDropManager: PlatformDragAndDropManager = object : WebDragAndDropManager(canvasEvents, density) { + override val dragAndDropManager: PlatformDragAndDropManager = object : WebDragAndDropManager(canvasEvents, state.globalEvents, density) { override val rootDragAndDropNode: ComposeSceneDragAndDropNode get() = scene.rootDragAndDropNode } @@ -312,13 +312,6 @@ internal class ComposeWindow( processKeyboardEvent(event) } - - state.globalEvents.addDisposableEvent("dragover") { event -> - event as DragEvent - event.preventDefault() - event.dataTransfer?.dropEffect = "move" - } - state.globalEvents.addDisposableEvent("focus") { lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } From 1da9cf4205c129aa2cae3b82b887a908160352ed Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 13 Dec 2024 10:10:05 +0100 Subject: [PATCH 3/4] Remove obsolete import --- .../androidx/compose/ui/draganddrop/WebDragAndDropManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt index 25468dd866fcb..12dd6179110ca 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/draganddrop/WebDragAndDropManager.kt @@ -20,7 +20,6 @@ import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.DragEvent import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.ImageData -import androidx.compose.ui.graphics.asByteArray import org.w3c.dom.HTMLElement internal abstract class WebDragAndDropManager(eventListener: EventTargetListener, globalEventsListener: EventTargetListener, private val density: Density) : From 277d76105df6fb35f285f563bf4bfd578856bab2 Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 13 Dec 2024 11:21:07 +0100 Subject: [PATCH 4/4] Make DragAndDropEvent::offset internal --- .../androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt index 1701d6b7a618e..96f01fb6fbd24 100644 --- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt +++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.jsWasm.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.geometry.Offset /** * A representation of an event sent by the platform during a drag and drop operation. */ -actual class DragAndDropEvent internal constructor(val offset: Offset) +actual class DragAndDropEvent internal constructor(internal val offset: Offset) /** * Returns the position of this [DragAndDropEvent] relative to the root Compose View in the