diff --git a/algorithm/src/jvmMain/kotlin/com/alexvanyo/composelife/model/CellWindow.kt b/algorithm/src/jvmMain/kotlin/com/alexvanyo/composelife/model/CellWindow.kt index 917541790c..1b559e6e2e 100644 --- a/algorithm/src/jvmMain/kotlin/com/alexvanyo/composelife/model/CellWindow.kt +++ b/algorithm/src/jvmMain/kotlin/com/alexvanyo/composelife/model/CellWindow.kt @@ -23,8 +23,8 @@ 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(val intRect: IntRect) { diff --git a/ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCellsPreviews.kt b/ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCellsPreviews.kt index c372989d84..2dba09857e 100644 --- a/ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCellsPreviews.kt +++ b/ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCellsPreviews.kt @@ -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 @@ -61,6 +62,7 @@ internal fun InteractableCellsPreview(modifier: Modifier = Modifier) { IntSize(10, 10), ), ), + pixelOffsetFromCenter = Offset.Zero, ) } } diff --git a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/CellWindow.kt b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/CellWindow.kt index 75fb3c3893..a16ae251c7 100644 --- a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/CellWindow.kt +++ b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/CellWindow.kt @@ -35,7 +35,6 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.pointerInput @@ -393,14 +392,6 @@ private fun CellWindowImpl( } Box { - // Create the offset modifier for adjusting the components that expect to be precisely sized to a multiple - // of scaledCellDpSize to represent exactly the current CellWindow - val cellWindowOffsetModifier = Modifier - .graphicsLayer { - this.translationX = -fracPixelOffsetFromCenter.x - this.translationY = -fracPixelOffsetFromCenter.y - } - // Apply the navigable modifier around the cells, but not the selection overlay. // This ensures gestures for the selection overlay are given precedence over the cells. Box( @@ -441,7 +432,9 @@ private fun CellWindowImpl( }, scaledCellDpSize = scaledCellDpSize, cellWindow = cellWindow, - modifier = cellWindowOffsetModifier, + pixelOffsetFromCenter = fracPixelOffsetFromCenter, + modifier = Modifier + ) } } @@ -457,7 +450,7 @@ private fun CellWindowImpl( getSelectionCellState = cellWindowUiState::getSelectionCellState, scaledCellDpSize = scaledCellDpSize, cellWindow = cellWindow, - modifier = cellWindowOffsetModifier, + pixelOffsetFromCenter = fracPixelOffsetFromCenter, ) } } diff --git a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCells.kt b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCells.kt index 9b8b583806..4aa9ffac55 100644 --- a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCells.kt +++ b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/InteractableCells.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.isSpecified +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.HistoricalChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.pointerInput @@ -81,10 +82,15 @@ fun InteractableCells( setSelectionSessionState: (SessionValue) -> Unit, scaledCellDpSize: Dp, cellWindow: CellWindow, + pixelOffsetFromCenter: Offset, modifier: Modifier = Modifier, ) { Surface( modifier = modifier + .graphicsLayer { + this.translationX = -pixelOffsetFromCenter.x + this.translationY = -pixelOffsetFromCenter.y + } .requiredSize( scaledCellDpSize * cellWindow.width, scaledCellDpSize * cellWindow.height, diff --git a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/SelectionOverlay.kt b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/SelectionOverlay.kt index cbf27e7a62..aece7dda0f 100644 --- a/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/SelectionOverlay.kt +++ b/ui-cells/src/jbMain/kotlin/com/alexvanyo/composelife/ui/cells/SelectionOverlay.kt @@ -61,7 +61,6 @@ import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope @@ -77,6 +76,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import androidx.compose.ui.unit.toIntRect import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toRect import androidx.compose.ui.unit.toSize @@ -84,8 +84,6 @@ import androidx.compose.ui.util.packInts import androidx.compose.ui.util.unpackInt1 import androidx.compose.ui.util.unpackInt2 import com.alexvanyo.composelife.geometry.times -import com.alexvanyo.composelife.logging.Logger -import com.alexvanyo.composelife.logging.d import com.alexvanyo.composelife.model.CellState import com.alexvanyo.composelife.model.CellWindow import com.alexvanyo.composelife.model.di.CellStateParserProvider @@ -107,9 +105,10 @@ import kotlinx.coroutines.launch import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt +import kotlin.uuid.Uuid /** - * The overlay based on the [selectionState]. + * The overlay based on the [selectionSessionState]. */ context(CellStateParserProvider) @Suppress("LongMethod", "LongParameterList") @@ -120,6 +119,7 @@ fun SelectionOverlay( getSelectionCellState: (SelectionState) -> CellState, scaledCellDpSize: Dp, cellWindow: CellWindow, + pixelOffsetFromCenter: Offset, modifier: Modifier = Modifier, ) { val selectionSessionStateValueHolder = rememberSessionValueHolder( @@ -135,8 +135,7 @@ fun SelectionOverlay( val scaledCellPixelSize = with(LocalDensity.current) { scaledCellDpSize.toPx() } - val dropAvailableBorderColor = MaterialTheme.colorScheme.secondary - val dropPreviewBorderColor = MaterialTheme.colorScheme.tertiary + val dropAvailableBorderColor = MaterialTheme.colorScheme.tertiary val dropPreviewCellStateBorderColor = MaterialTheme.colorScheme.secondary AnimatedContent( @@ -155,18 +154,26 @@ fun SelectionOverlay( .drawWithContent { drawContent() - when (val cellStateDropState = cellStateDropStateHolder.cellStateDropState) { - CellStateDropState.ApplicableDropAvailable -> { - Logger.d { "drawApplicableDropAvailable" } - drawRect(Color.Red.copy(alpha = 0.3f)) - drawApplicableDropAvailable( - color = dropAvailableBorderColor, + when (cellStateDropStateHolder.cellStateDropState) { + CellStateDropState.ApplicableDropAvailable, + is CellStateDropState.DropPreview -> { + drawDashedRect( + selectionColor = dropAvailableBorderColor, + strokeWidth = 12.dp.toPx(), + intervals = floatArrayOf( + 24.dp.toPx(), + 24.dp.toPx(), + ), + phase = 12.dp.toPx(), ) } - is CellStateDropState.DropPreview -> Unit CellStateDropState.None -> Unit } } + .graphicsLayer { + this.translationX = -pixelOffsetFromCenter.x + this.translationY = -pixelOffsetFromCenter.y + } .requiredSize( scaledCellDpSize * cellWindow.width, scaledCellDpSize * cellWindow.height, @@ -174,12 +181,16 @@ fun SelectionOverlay( .cellStateDragAndDropTarget( mutableCellStateDropStateHolder = cellStateDropStateHolder, ) { dropOffset, cellState -> - selectionSessionStateValueHolder.setValue( - SelectionState.Selection( - cellState = cellState, - offset = (cellWindow.topLeft.toOffset() + (dropOffset / scaledCellPixelSize) - - cellState.boundingBox.size.toSize().center).round(), - ), + setSelectionSessionState( + SessionValue( + sessionId = Uuid.random(), + valueId = Uuid.random(), + value = SelectionState.Selection( + cellState = cellState, + offset = (cellWindow.topLeft.toOffset() + (dropOffset / scaledCellPixelSize) - + cellState.boundingBox.size.toSize().center).round(), + ), + ) ) } .drawWithContent { @@ -188,14 +199,13 @@ fun SelectionOverlay( when (val cellStateDropState = cellStateDropStateHolder.cellStateDropState) { CellStateDropState.ApplicableDropAvailable -> Unit is CellStateDropState.DropPreview -> { - drawRect(Color.Green.copy(alpha = 0.3f)) drawDropPreview( dropPreview = cellStateDropState, scaledCellDpSize = scaledCellDpSize, - borderColor = dropPreviewBorderColor, cellStateOutlineColor = dropPreviewCellStateBorderColor, ) } + CellStateDropState.None -> Unit } }, @@ -204,6 +214,7 @@ fun SelectionOverlay( SelectionState.NoSelection -> { Spacer(Modifier.fillMaxSize()) } + is SelectionState.SelectingBox.FixedSelectingBox -> { @Suppress("UNCHECKED_CAST") ( @@ -220,6 +231,7 @@ fun SelectionOverlay( ) ) } + is SelectionState.SelectingBox.TransientSelectingBox -> { TransientSelectingBoxOverlay( selectionState = targetSelectionState, @@ -228,6 +240,7 @@ fun SelectionOverlay( modifier = Modifier.fillMaxSize(), ) } + is SelectionState.Selection -> { @Suppress("UNCHECKED_CAST") ( @@ -247,36 +260,12 @@ fun SelectionOverlay( } } -private fun ContentDrawScope.drawApplicableDropAvailable( - color: Color, -) { - drawSelectionRect( - selectionColor = color, - strokeWidth = 2.dp.toPx(), - intervals = floatArrayOf( - 24.dp.toPx(), - 24.dp.toPx(), - ), - phase = 12.dp.toPx(), - ) -} - private fun ContentDrawScope.drawDropPreview( dropPreview: CellStateDropState.DropPreview, scaledCellDpSize: Dp, - borderColor: Color, cellStateOutlineColor: Color, ) { - drawSelectionRect( - selectionColor = borderColor, - strokeWidth = 2.dp.toPx(), - intervals = floatArrayOf( - 24.dp.toPx(), - 24.dp.toPx(), - ), - phase = 12.dp.toPx(), - ) - drawSelectionRect( + drawDashedRect( selectionColor = cellStateOutlineColor, strokeWidth = 2.dp.toPx(), intervals = floatArrayOf( @@ -284,7 +273,7 @@ private fun ContentDrawScope.drawDropPreview( 12.dp.toPx(), ), phase = 12.dp.toPx(), - rect = ((dropPreview.cellState.boundingBox.intRect.toRect() + rect = ((dropPreview.cellState.boundingBox.size.toIntRect().toRect() .translate(-dropPreview.cellState.boundingBox.size.toSize().center) ) * scaledCellDpSize.toPx()).translate(dropPreview.offset) ) @@ -754,7 +743,7 @@ fun SelectingBox( color = selectionColor, alpha = 0.2f, ) - drawSelectionRect( + drawDashedRect( selectionColor = selectionColor, strokeWidth = 2.dp.toPx(), intervals = floatArrayOf( @@ -767,12 +756,12 @@ fun SelectingBox( } /** - * Draws a selection rectangle, with the given [PathEffect]. + * Draws a dashed line rectangle, with intervals and phase applied symmetrically. * - * This results in the [PathEffect] being applied symmetrically via 8 line segments: from the middle of each side, + * The dashed line effect is drawn in 8 line segments: from the middle of each side, * to each corner. */ -private fun DrawScope.drawSelectionRect( +private fun DrawScope.drawDashedRect( selectionColor: Color, strokeWidth: Float, intervals: FloatArray,