diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt index 9fbb907..7699cbc 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -116,10 +116,6 @@ fun App( modifier = Modifier.blurOnFinished(isSolved), grid = { PuzzleGrid( -// modifier = Modifier -// .aspectRatio(1f) -// .fillMaxWidth() -// .align(Alignment.CenterHorizontally), grid = game.grid, onUpdate = { horizontalClues.forEach { it.update(game.grid) } @@ -128,19 +124,29 @@ fun App( ) }, horizontalClues = { - HorizontalClues( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - clues = horizontalClues - ) + for (clue in horizontalClues) { + HorizontalClue( + modifier = Modifier.forClue(clue), + clue = clue.clue, + isClueViolated = clue.isViolated + ) + } }, verticalClues = { - VerticalClues( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - clues = verticalClues - ) + for (clue in verticalClues) { + VerticalClue( + modifier = Modifier.forClue(clue), + clue = clue.clue, + isClueViolated = clue.isViolated + ) + } }, time = { - Text(time, fontSize = TextUnit(4f, TextUnitType.Companion.Em), textAlign = TextAlign.End) + Text( + time, + fontSize = TextUnit(4f, TextUnitType.Companion.Em), + textAlign = TextAlign.End + ) } ) EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame) diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt index 943d23b..47e93bc 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt @@ -3,13 +3,15 @@ package ch.dissem.yaep.ui.common import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth import androidx.compose.ui.unit.Constraints.Companion.fixed import androidx.compose.ui.unit.dp import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.AspectRatio.SQUARISH +import kotlin.math.max import kotlin.math.min @@ -48,98 +50,222 @@ fun AdaptiveGameLayout( layout(width = constraints.maxWidth, height = constraints.maxHeight) { val aspectRatio = AspectRatio.from(constraints) - val gridConstraints: Constraints - val horizontalCluesConstraints: Constraints - val verticalCluesConstraints: Constraints - val timeConstraints: Constraints + val gridMeasurable = measurables[0][0] + val horizontalCluesMeasurables = measurables[1] + val verticalCluesMeasurables = measurables[2] + val timeMeasurable = measurables[3][0] + val spacingPx = spacing.roundToPx() + when (aspectRatio) { PORTRAIT -> { - val gridSize = constraints.maxWidth - val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx - val timeWidth = constraints.maxWidth / 10 - - gridConstraints = fixed(gridSize, gridSize) - horizontalCluesConstraints = fixed(gridSize, bottomBarHeight / 2) - verticalCluesConstraints = - fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2) - timeConstraints = fixed(timeWidth, bottomBarHeight / 2) - } - - SQUARISH -> { - val gridSize = (8 * min(constraints.maxWidth, constraints.maxHeight)) / 10 - val rightBarWidth = constraints.maxWidth - gridSize - spacingPx - val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx - - gridConstraints = fixed(gridSize, gridSize) - horizontalCluesConstraints = fixed(rightBarWidth, gridSize) - verticalCluesConstraints = fixed(gridSize, bottomBarHeight) - timeConstraints = fixed(rightBarWidth, bottomBarHeight) - } - - LANDSCAPE -> { - val gridSize = constraints.maxHeight - val rightBarWidth = constraints.maxWidth - gridSize - spacingPx - - gridConstraints = fixed(gridSize, gridSize) - val baseSpace = gridSize - 2 * spacingPx - horizontalCluesConstraints = fitPrioritizingWidth(rightBarWidth, rightBarWidth, 0, baseSpace) - verticalCluesConstraints = fitPrioritizingWidth(rightBarWidth, rightBarWidth, 0, baseSpace) - timeConstraints = Constraints.fixedHeight(baseSpace / 10) - } - } - - // The grid takes up most of the space - // Place the clues to the right for landscape and below for portrait. If the aspect ratio is close to 1, we place the horizontal clues to the right and the vertical clues to the left. - // The time is placed at the bottom right corner. - val gridPlaceable = measurables[0][0].measure(gridConstraints) - val horizontalCluesPlaceable = - measurables[1][0].measure(horizontalCluesConstraints) - val verticalCluesPlaceable = measurables[2][0].measure(verticalCluesConstraints) - val timePlaceable = measurables[3][0].apply {}.measure(timeConstraints) - - // Position the grid - gridPlaceable.place(0, 0) - - // Position the horizontal clues - when (aspectRatio) { - PORTRAIT -> { - horizontalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) - } - - SQUARISH, LANDSCAPE -> { - horizontalCluesPlaceable.place(gridPlaceable.width + spacingPx, 0) - } - } - - // Position the vertical clues - when (aspectRatio) { - PORTRAIT -> { - verticalCluesPlaceable.place( - 0, - gridPlaceable.height + horizontalCluesPlaceable.height + 2 * spacingPx + portrait( + constraints, + spacingPx, + gridMeasurable, + horizontalCluesMeasurables, + verticalCluesMeasurables, + timeMeasurable ) } SQUARISH -> { - verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + squarish( + constraints, + spacingPx, + gridMeasurable, + horizontalCluesMeasurables, + verticalCluesMeasurables, + timeMeasurable + ) } LANDSCAPE -> { - verticalCluesPlaceable.place( - gridPlaceable.width + spacingPx, - horizontalCluesPlaceable.height + spacingPx + landscape( + constraints, + spacingPx, + gridMeasurable, + horizontalCluesMeasurables, + verticalCluesMeasurables, + timeMeasurable ) } } - - // Position the time - timePlaceable.place( - x = constraints.maxWidth - timePlaceable.width - spacingPx, - y = constraints.maxHeight - timePlaceable.height - ) } } - +} + +private fun Placeable.PlacementScope.portrait( + constraints: Constraints, + spacingPx: Int, + gridMeasurable: Measurable, + horizontalCluesMeasurables: List, + verticalCluesMeasurables: List, + timeMeasurable: Measurable +) { + val gridSize = constraints.maxWidth + val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx + val timeWidth = constraints.maxWidth / 10 + + val gridConstraints = fixed(gridSize, gridSize) + val horizontalCluesConstraints = fixed(gridSize, bottomBarHeight / 2) + val verticalCluesConstraints = + fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2) + val timeConstraints = fixed(timeWidth, bottomBarHeight / 2) + + val gridPlaceable = gridMeasurable.measure(gridConstraints) + val horizontalCluesPlaceables = horizontalCluesMeasurables.map { + it.measure(horizontalCluesConstraints) + } + val verticalCluesPlaceables = verticalCluesMeasurables.map { + it.measure(verticalCluesConstraints) + } + val timePlaceable = timeMeasurable.measure(timeConstraints) + + // Position the grid + gridPlaceable.place(0, 0) + + // Position the horizontal clues + // TODO horizontalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + + // Position the vertical clues + // TODO verticalCluesPlaceable.place(0, gridPlaceable.height + horizontalCluesPlaceable.height + 2 * spacingPx) + + // Position the time + timePlaceable.place( + x = constraints.maxWidth - timePlaceable.width - spacingPx, + y = constraints.maxHeight - timePlaceable.height + ) +} + +private fun Placeable.PlacementScope.squarish( + constraints: Constraints, + spacingPx: Int, + gridMeasurable: Measurable, + horizontalCluesMeasurables: List, + verticalCluesMeasurables: List, + timeMeasurable: Measurable +) { + val gridSize = (8 * min(constraints.maxWidth, constraints.maxHeight)) / 10 + val rightBarWidth = constraints.maxWidth - gridSize - spacingPx + val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx + + val gridConstraints = fixed(gridSize, gridSize) + val horizontalCluesConstraints = fixed(rightBarWidth, gridSize) + val verticalCluesConstraints = fixed(gridSize, bottomBarHeight) + val timeConstraints = fixed(rightBarWidth, bottomBarHeight) + + val gridPlaceable = gridMeasurable.measure(gridConstraints) + val horizontalCluesPlaceables = horizontalCluesMeasurables.map { + it.measure(horizontalCluesConstraints) + } + val verticalCluesPlaceables = verticalCluesMeasurables.map { + it.measure(verticalCluesConstraints) + } + val timePlaceable = timeMeasurable.measure(timeConstraints) + + // Position the grid + gridPlaceable.place(0, 0) + + // Position the horizontal clues + // TODO horizontalCluesPlaceable.place(gridPlaceable.width + spacingPx, 0) + + // Position the vertical clues + // TODO verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + + // Position the time + timePlaceable.place( + x = constraints.maxWidth - timePlaceable.width - spacingPx, + y = constraints.maxHeight - timePlaceable.height + ) +} + +private fun Placeable.PlacementScope.landscape( + constraints: Constraints, + spacingPx: Int, + gridMeasurable: Measurable, + horizontalCluesMeasurables: List, + verticalCluesMeasurables: List, + timeMeasurable: Measurable +) { + val gridSize = constraints.maxHeight + val gridItemSize = (gridSize - 12 * spacingPx) / 18 + val rightBarWidth = constraints.maxWidth - gridSize - spacingPx + + val gridConstraints = fixed(gridSize, gridSize) + val baseSpace = gridSize - 2 * spacingPx + val horizontalCluesConstraints = fixed( + width = 3 * gridItemSize + 2 * spacingPx, + height = gridItemSize + 2 * spacingPx + ) + val verticalCluesConstraints = fixed( + width = gridItemSize + 2 * spacingPx, + height = 3 * gridItemSize + 2 * spacingPx + ) + val timeConstraints = Constraints.fixedHeight(baseSpace / 10) + + val gridPlaceable = gridMeasurable.measure(gridConstraints) + val horizontalCluesPlaceables = horizontalCluesMeasurables.map { + it.measure(horizontalCluesConstraints) + } + val verticalCluesPlaceables = verticalCluesMeasurables.map { + it.measure(verticalCluesConstraints) + } + val timePlaceable = timeMeasurable.measure(timeConstraints) + + // Position the grid + gridPlaceable.place(0, 0) + + // Position the horizontal clues + val offsetY = placeClues( + placeables = horizontalCluesPlaceables, + offsetX = gridPlaceable.width + spacingPx, + offsetY = 0, + maxWidth = rightBarWidth + ) + + // TODO: add spacer in between + + // Position the vertical clues + placeClues( + placeables = verticalCluesPlaceables, + offsetX = gridPlaceable.width + spacingPx, + offsetY = offsetY + spacingPx, + maxWidth = rightBarWidth + ) + + // Position the time + timePlaceable.place( + x = constraints.maxWidth - timePlaceable.width - spacingPx, + y = constraints.maxHeight - timePlaceable.height + ) +} + +private fun Placeable.PlacementScope.placeClues( + placeables: List, + offsetX: Int, + offsetY: Int, + maxWidth: Int, + minSpacing: Int = 0 +): Int { + if (placeables.isEmpty()) return offsetY + + val itemWidth = placeables.first().width + val itemHeight = placeables.first().height + val columns = max(1, maxWidth / itemWidth) + val spacing = max(minSpacing, (maxWidth - columns * itemWidth) / (columns - 1)) + var currentX = offsetX + var currentY = offsetY + var i = 0 + for (placeable in placeables) { + placeable.place(currentX, currentY) + currentX += itemWidth + spacing + i++ + if (i % columns == 0) { + currentX = offsetX + currentY += itemHeight + } + } + return currentY + itemHeight }