diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt index 0663660bb..24fd37a0b 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DragContainer.kt @@ -38,9 +38,9 @@ import com.pitchedapps.frost.ext.toIntOffset */ @Composable -fun DragContainer( +fun DragContainer( modifier: Modifier = Modifier, - draggableState: DraggableState, + draggableState: DraggableState, content: @Composable () -> Unit ) { Box(modifier = modifier) { @@ -61,26 +61,28 @@ fun DragContainer( * dragged. */ @Composable -fun DragTarget( +fun DragTarget( key: String = "", - draggableState: DraggableState, - content: @Composable (isDragging: Boolean) -> Unit + data: T, + draggableState: DraggableState, + content: DraggableComposeContent, ) { val dragTargetState = draggableState.rememberDragTarget( key = key, + data = data, content = content, ) Box( modifier = Modifier.dragTarget(dragTargetState), ) { - content(false) + content(isDragging = false) } } -private fun Modifier.dragTarget(dragTargetState: DragTargetState): Modifier { +private fun Modifier.dragTarget(dragTargetState: DragTargetState): Modifier { return onGloballyPositioned { dragTargetState.windowPosition = it.positionInWindow() dragTargetState.size = it.size @@ -111,7 +113,7 @@ private fun Modifier.dragTarget(dragTargetState: DragTargetState): Modifier { .alpha(if (dragTargetState.isDragging) 0f else 1f) } -fun Modifier.dropTarget(dropTargetState: DropTargetState): Modifier { +fun Modifier.dropTarget(dropTargetState: DropTargetState): Modifier { return onGloballyPositioned { dropTargetState.bounds = it.boundsInWindow() } } @@ -125,14 +127,14 @@ fun Modifier.dropTarget(dropTargetState: DropTargetState): Modifier { * fillMaxWidth in a grid, but would have a full parent width here without the sizing constraints. */ @Composable -private fun DraggingContents(draggableState: DraggableState) { +private fun DraggingContents(draggableState: DraggableState) { for (target in draggableState.targets) { DraggingContent(target = target) } } @Composable -private fun DraggingContent(target: DragTargetState) { +private fun DraggingContent(target: DragTargetState) { val density = LocalDensity.current Box( modifier = diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt index 19808e74d..a3f3db4b4 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/draggable/DraggableState.kt @@ -28,13 +28,15 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.unit.IntSize @Composable -fun rememberDraggableState(): DraggableState { +fun rememberDraggableState(): DraggableState { return remember { DraggableStateImpl() } } -interface DraggableState { +typealias DraggableComposeContent = @Composable (isDragging: Boolean) -> Unit - val targets: Collection +interface DraggableState { + + val targets: Collection> /** * Being drag for target [key]. @@ -44,30 +46,27 @@ interface DraggableState { * Returns true if the request is accepted. It is the caller's responsibility to not propagate * drag events if the request is denied. */ - fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean + fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean fun onDrag(key: String, offset: Offset) fun onDragEnd(key: String) @Composable - fun rememberDragTarget( - key: String, - content: @Composable (isDragging: Boolean) -> Unit - ): DragTargetState + fun rememberDragTarget(key: String, data: T, content: DraggableComposeContent): DragTargetState - @Composable fun rememberDropTarget(key: String): DropTargetState + @Composable fun rememberDropTarget(key: String): DropTargetState } -class DraggableStateImpl : DraggableState { - private val activeDragTargets = mutableStateMapOf() +class DraggableStateImpl : DraggableState { + private val activeDragTargets = mutableStateMapOf>() - private val dropTargets = mutableStateMapOf() + private val dropTargets = mutableStateMapOf>() - override val targets: Collection + override val targets: Collection> get() = activeDragTargets.values - override fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean { + override fun onDragStart(key: String, dragTargetState: DragTargetState): Boolean { if (key in activeDragTargets) return false activeDragTargets[key] = dragTargetState return true @@ -93,18 +92,19 @@ class DraggableStateImpl : DraggableState { @Composable override fun rememberDragTarget( key: String, - content: @Composable (isDragging: Boolean) -> Unit - ): DragTargetState { + data: T, + content: DraggableComposeContent, + ): DragTargetState { val target = - remember(key, content, this) { - DragTargetState(key = key, draggableState = this, composable = content) + remember(key, data, content, this) { + DragTargetState(key = key, data = data, draggableState = this, composable = content) } DisposableEffect(target) { onDispose { activeDragTargets.remove(key) } } return target } @Composable - override fun rememberDropTarget(key: String): DropTargetState { + override fun rememberDropTarget(key: String): DropTargetState { val target = remember(key, this) { DropTargetState(key, this) } DisposableEffect(target) { dropTargets[key] = target @@ -116,11 +116,14 @@ class DraggableStateImpl : DraggableState { private fun setHover(dragKey: String?, dropKey: String) { val dropTarget = dropTargets[dropKey] ?: return - dropTarget.hoverKey = dragKey + // Safety check; we only want to register active keys + val dragTarget = if (dragKey != null) activeDragTargets[dragKey] else null + dropTarget.hoverKey = dragTarget?.key + dropTarget.hoverData = dragTarget?.data } /** Returns true if drag target exists and is within bounds */ - private fun DropTargetState.hasValidDragTarget(): Boolean { + private fun DropTargetState.hasValidDragTarget(): Boolean { val currentKey = hoverKey ?: return false // no target val dragTarget = activeDragTargets[currentKey] ?: return false // target not valid return dragTarget.within(bounds) @@ -152,17 +155,18 @@ class DraggableStateImpl : DraggableState { } } -private fun DragTargetState?.within(bounds: Rect): Boolean { +private fun DragTargetState<*>?.within(bounds: Rect): Boolean { if (this == null) return false val center = dragPosition + Offset(size.width * 0.5f, size.height * 0.5f) return bounds.contains(center) } /** State for individual dragging target. */ -class DragTargetState( +class DragTargetState( val key: String, - val draggableState: DraggableStateImpl, - val composable: @Composable (isDragging: Boolean) -> Unit + val data: T, + val draggableState: DraggableStateImpl, + val composable: DraggableComposeContent, ) { var isDragging by mutableStateOf(false) var windowPosition = Offset.Zero @@ -170,12 +174,18 @@ class DragTargetState( var size: IntSize by mutableStateOf(IntSize.Zero) } -class DropTargetState(private val key: String, private val draggableState: DraggableStateImpl) { +class DropTargetState( + private val key: String, + private val draggableState: DraggableStateImpl +) { var hoverKey: String? by mutableStateOf(null) - var hoverData: String? by mutableStateOf(null) + var hoverData: T? by mutableStateOf(null) var bounds: Rect = Rect.Zero set(value) { field = value draggableState.checkForDrop(key) } + + val isHovered + get() = hoverKey != null } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt index 41ee7e095..d706cde71 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt @@ -87,7 +87,7 @@ fun TabSelector( unselected: List, onSelect: (List) -> Unit ) { - val draggableState = rememberDraggableState() + val draggableState = rememberDraggableState() DragContainer(modifier = modifier, draggableState = draggableState) { Column(modifier = Modifier.statusBarsPadding()) { @@ -96,7 +96,7 @@ fun TabSelector( columns = GridCells.Fixed(4), ) { items(unselected, key = { it.key }) { - DragTarget(key = it.key, draggableState = draggableState) { isDragging -> + DragTarget(key = it.key, data = it, draggableState = draggableState) { isDragging -> TabItem( modifier = Modifier.thenIf(!isDragging) { @@ -124,7 +124,7 @@ fun TabSelector( @Composable fun TabBottomBar( modifier: Modifier = Modifier, - draggableState: DraggableState, + draggableState: DraggableState, items: List ) { NavigationBar(modifier = modifier) { @@ -133,7 +133,7 @@ fun TabBottomBar( val alpha by animateFloatAsState( - targetValue = if (dropTargetState.hoverKey == null) 1f else 0f, + targetValue = if (!dropTargetState.isHovered) 1f else 0.3f, label = "Nav Item Alpha", ) @@ -141,10 +141,13 @@ fun TabBottomBar( modifier = Modifier.dropTarget(dropTargetState), icon = { // println(dropTargetState.hoverKey) + + val iconItem = dropTargetState.hoverData ?: item + Icon( modifier = Modifier.size(24.dp).alpha(alpha), - imageVector = item.icon, - contentDescription = item.title, + imageVector = iconItem.icon, + contentDescription = iconItem.title, ) }, selected = false,