From a7e8325b26886bfb459abea26217f34e9b88f006 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Mon, 7 Jul 2025 18:45:28 +0200 Subject: [PATCH] Adaptive Layout (WIP) --- .../kotlin/ch/dissem/yaep/ui/common/App.kt | 107 ++++++------- .../yaep/ui/common/adaptive game layout.kt | 145 ++++++++++++++++++ 2 files changed, 191 insertions(+), 61 deletions(-) create mode 100644 commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt 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 30b83a7..9fbb907 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 @@ -6,14 +6,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text @@ -27,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp @@ -128,40 +127,26 @@ fun App( } ) }, - clues = { - PuzzleClues( - horizontalClues = horizontalClues, - verticalClues = verticalClues + horizontalClues = { + HorizontalClues( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + clues = horizontalClues ) }, - time = time + verticalClues = { + VerticalClues( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + clues = verticalClues + ) + }, + time = { + Text(time, fontSize = TextUnit(4f, TextUnitType.Companion.Em), textAlign = TextAlign.End) + } ) EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame) } } -@Composable -fun AdaptiveGameLayout( - modifier: Modifier = Modifier, - grid: @Composable () -> Unit, - clues: @Composable () -> Unit, - time: String = "00:00" -) { - // TODO: actually use an adaptive layout - Row(modifier = modifier) { - Column(modifier = Modifier.weight(0.6f)) { - grid() - } - Column( - modifier = Modifier.padding(start = 16.dp).weight(0.4f).fillMaxHeight() - ) { - clues() - - Text(time, fontSize = TextUnit(4f, TextUnitType.Em)) - } - } -} - @Composable fun PuzzleGrid( modifier: Modifier = Modifier, @@ -226,42 +211,42 @@ fun PuzzleGrid( } @Composable -fun PuzzleClues( +fun HorizontalClues( modifier: Modifier = Modifier, - horizontalClues: List>, - verticalClues: List, ItemClass<*>>>> + clues: List> ) { - Column(modifier = modifier) { - LazyVerticalGrid( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - columns = GridCells.Fixed(4) - ) { - for (clue in horizontalClues) { - item { - HorizontalClue( - modifier = Modifier - .forClue(clue), - clue = clue.clue, - isClueViolated = clue.isViolated - ) - } + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(4) + ) { + for (clue in clues) { + item { + HorizontalClue( + modifier = Modifier.forClue(clue), + clue = clue.clue, + isClueViolated = clue.isViolated + ) } } - HorizontalDivider() - LazyVerticalGrid( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - columns = GridCells.Fixed(8) - ) { - for (clue in verticalClues) { - item { - VerticalClue( - modifier = Modifier - .forClue(clue) - .aspectRatio(0.33333334f), - clue = clue.clue, - isClueViolated = clue.isViolated - ) - } + } +} + +@Composable +fun VerticalClues( + modifier: Modifier = Modifier, + clues: List, ItemClass<*>>>> +) { + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(8) + ) { + for (clue in clues) { + item { + VerticalClue( + modifier = Modifier.forClue(clue), + clue = clue.clue, + isClueViolated = clue.isViolated + ) } } } 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 new file mode 100644 index 0000000..943d23b --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt @@ -0,0 +1,145 @@ +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.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.min + + +internal val spacing = 8.dp + +private enum class AspectRatio { + PORTRAIT, LANDSCAPE, SQUARISH; + + companion object { + private const val landscapeRatio = 1.4f + private const val portraitRatio = 1 / landscapeRatio + + fun from(constraints: Constraints): AspectRatio { + val ratio = constraints.maxWidth.toFloat() / constraints.maxHeight.toFloat() + return when { + ratio < portraitRatio -> PORTRAIT + ratio > landscapeRatio -> LANDSCAPE + else -> SQUARISH + } + } + } +} + +@Composable +fun AdaptiveGameLayout( + modifier: Modifier = Modifier, + grid: @Composable () -> Unit, + horizontalClues: @Composable () -> Unit, + verticalClues: @Composable () -> Unit, + time: @Composable () -> Unit +) { + Layout( + contents = listOf(grid, horizontalClues, verticalClues, time), + modifier = modifier + ) { measurables, constraints -> + 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 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 + ) + } + + SQUARISH -> { + verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + } + + LANDSCAPE -> { + verticalCluesPlaceable.place( + gridPlaceable.width + spacingPx, + horizontalCluesPlaceable.height + spacingPx + ) + } + } + + // Position the time + timePlaceable.place( + x = constraints.maxWidth - timePlaceable.width - spacingPx, + y = constraints.maxHeight - timePlaceable.height + ) + } + } + +} +