Skip to content

Commit

Permalink
Add CellStateDropState and fancy drag and drop support
Browse files Browse the repository at this point in the history
  • Loading branch information
alexvanyo committed Dec 28, 2024
1 parent 2bf2e15 commit 4193e59
Show file tree
Hide file tree
Showing 22 changed files with 1,412 additions and 790 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import androidx.compose.ui.unit.IntSize
/**
* A finite rectangular region of a cell universe.
*
* This is represented by an [IntRect], with the [IntRect.topLeft] being the top-left most point (inclusive), and
* [IntRect.bottomRight] being the bottom-right most point (exclusive).
* This is represented by an [IntRect] [intRect], with the [IntRect.topLeft] being the top-left most point (inclusive),
* and [IntRect.bottomRight] being the bottom-right most point (exclusive).
*/
@JvmInline
value class CellWindow(private val intRect: IntRect) {
value class CellWindow(val intRect: IntRect) {

init {
require(intRect.top <= intRect.bottom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection

operator fun Rect.times(scale: Float): Rect =
Rect(
topLeft = topLeft * scale,
bottomRight = bottomRight * scale,
)

operator fun Rect.div(scale: Float): Rect =
Rect(
topLeft = topLeft / scale,
bottomRight = bottomRight / scale,
)

fun Rect.topStart(layoutDirection: LayoutDirection): Offset = when (layoutDirection) {
LayoutDirection.Ltr -> topLeft
LayoutDirection.Rtl -> topRight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.alexvanyo.composelife.ui.app.createComponent
import com.alexvanyo.composelife.ui.cells.CellWindowInjectEntryPoint
import com.alexvanyo.composelife.ui.cells.CellWindowLocalEntryPoint
import com.alexvanyo.composelife.ui.cells.cellStateDragAndDropTarget
import com.alexvanyo.composelife.ui.cells.rememberMutableCellStateDropStateHolder
import kotlinx.coroutines.test.runCurrent
import org.junit.runner.RunWith
import kotlin.test.Test
Expand Down Expand Up @@ -97,9 +98,11 @@ class LoadedCellStatePreviewTests : BaseUiInjectTest<TestComposeLifeApplicationC
Spacer(
modifier = Modifier
.testTag("TestDropTarget")
.cellStateDragAndDropTarget {
droppedCellState = it
}
.cellStateDragAndDropTarget(
rememberMutableCellStateDropStateHolder { _, cellState ->
droppedCellState = cellState
},
)
.size(100.dp)
.background(Color.Blue),
)
Expand All @@ -125,8 +128,8 @@ class LoadedCellStatePreviewTests : BaseUiInjectTest<TestComposeLifeApplicationC
downTime,
downTime,
MotionEvent.ACTION_DOWN,
loadedCellStatePreviewCenter.x.toFloat(),
loadedCellStatePreviewCenter.y.toFloat(),
loadedCellStatePreviewCenter.x,
loadedCellStatePreviewCenter.y,
0,
).apply {
source = InputDevice.SOURCE_TOUCHSCREEN
Expand All @@ -141,8 +144,8 @@ class LoadedCellStatePreviewTests : BaseUiInjectTest<TestComposeLifeApplicationC
downTime,
SystemClock.uptimeMillis(),
MotionEvent.ACTION_MOVE,
testDropTargetCenter.x.toFloat(),
testDropTargetCenter.y.toFloat(),
testDropTargetCenter.x,
testDropTargetCenter.y,
0,
).apply {
source = InputDevice.SOURCE_TOUCHSCREEN
Expand All @@ -154,8 +157,8 @@ class LoadedCellStatePreviewTests : BaseUiInjectTest<TestComposeLifeApplicationC
downTime,
SystemClock.uptimeMillis(),
MotionEvent.ACTION_UP,
testDropTargetCenter.x.toFloat(),
testDropTargetCenter.y.toFloat(),
testDropTargetCenter.x,
testDropTargetCenter.y,
0,
).apply {
source = InputDevice.SOURCE_TOUCHSCREEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,37 @@ import android.content.ClipDescription
import android.os.Build
import android.view.View
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
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.draganddrop.mimeTypes
import androidx.compose.ui.draganddrop.toAndroidDragEvent
import androidx.compose.ui.geometry.Offset
import com.alexvanyo.composelife.model.CellState
import com.alexvanyo.composelife.model.CellStateParser
import com.alexvanyo.composelife.model.DeserializationResult
import com.alexvanyo.composelife.model.RunLengthEncodedCellStateSerializer
import com.alexvanyo.composelife.model.di.CellStateParserProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first

actual fun Modifier.cellStateDragAndDropSource(
getCellState: () -> CellState,
): Modifier =
dragAndDropSource(
transferData = {
val clipData = ClipData.newPlainText(
"cellState",
RunLengthEncodedCellStateSerializer.serializeToString(getCellState())
.joinToString("\n"),
)

DragAndDropTransferData(
clipData = ClipData.newPlainText(
"cellState",
RunLengthEncodedCellStateSerializer.serializeToString(getCellState())
.joinToString("\n"),
),
clipData = clipData,
localState = clipData,
flags = if (Build.VERSION.SDK_INT >= 24) {
View.DRAG_FLAG_GLOBAL
} else {
Expand All @@ -57,38 +61,65 @@ actual fun Modifier.cellStateDragAndDropSource(
},
)

context(CellStateParserProvider)
@Composable
@Suppress("ComposeComposableModifier")
actual fun Modifier.cellStateDragAndDropTarget(
setSelectionToCellState: (CellState) -> Unit,
): Modifier {
val coroutineScope = rememberCoroutineScope()
val target = remember(cellStateParser, coroutineScope) {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
val clipData = event.toAndroidDragEvent().clipData
coroutineScope.launch {
when (
val deserializationResult = cellStateParser.parseCellState(clipData)
) {
is DeserializationResult.Successful -> {
setSelectionToCellState(deserializationResult.cellState)
}
is DeserializationResult.Unsuccessful -> {
// TODO: Show error for unsuccessful drag and drop
}
}
}
return true
}
}
internal actual fun cellStateShouldStartDragAndDrop(event: DragAndDropEvent): Boolean =
event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)

internal actual class DragAndDropSession {
private var _isEntered by mutableStateOf(false)
private var _isEnded by mutableStateOf(false)
private var _isDropped by mutableStateOf(false)
private var _rootOffset by mutableStateOf(Offset.Zero)

actual val isEntered get() = _isEntered
actual val isEnded get() = _isEnded
actual val isDropped get() = _isDropped
actual val rootOffset get() = _rootOffset
var clipData by mutableStateOf<ClipData?>(null)
actual var deserializationResult by mutableStateOf<DeserializationResult?>(null)

actual fun updateWithOnStarted(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData ?: androidDragEvent.localState as? ClipData
}

return dragAndDropTarget(
shouldStartDragAndDrop = { event ->
event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
},
target = target,
)
actual fun updateWithOnEntered(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData ?: androidDragEvent.localState as? ClipData
_isEntered = true
_rootOffset = Offset(androidDragEvent.x, androidDragEvent.y)
}

actual fun updateWithOnMoved(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData ?: androidDragEvent.localState as? ClipData
_rootOffset = Offset(androidDragEvent.x, androidDragEvent.y)
}

actual fun updateWithOnExited(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData ?: androidDragEvent.localState as? ClipData
_isEntered = false
_rootOffset = Offset(androidDragEvent.x, androidDragEvent.y)
}

actual fun updateWithOnEnded(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData ?: androidDragEvent.localState as? ClipData
_isEnded = true
}

actual fun updateWithOnDrop(event: DragAndDropEvent) {
val androidDragEvent = event.toAndroidDragEvent()
clipData = androidDragEvent.clipData
_rootOffset = Offset(androidDragEvent.x, androidDragEvent.y)
_isDropped = true
}
}

internal actual suspend fun awaitAndParseCellState(
session: DragAndDropSession,
cellStateParser: CellStateParser,
): DeserializationResult =
cellStateParser.parseCellState(
snapshotFlow { session.clipData }.filterNotNull().first(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
Expand Down Expand Up @@ -61,6 +62,7 @@ internal fun InteractableCellsPreview(modifier: Modifier = Modifier) {
IntSize(10, 10),
),
),
pixelOffsetFromCenter = Offset.Zero,
)
}
}
Expand Down
Loading

0 comments on commit 4193e59

Please sign in to comment.