From 7f88095a4b2352917dabdb4e202881c84c9f7b71 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 9 Jul 2025 21:19:41 +0200 Subject: [PATCH] Adaptive Layout (WIP) --- android/build.gradle.kts | 3 - .../kotlin/ch/dissem/yaep/ui/common/App.kt | 45 ---------- .../yaep/ui/common/adaptive game layout.kt | 86 ++++++++++++++----- desktop/build.gradle.kts | 2 +- .../dissem/yaep/ui/desktop/desktop window.kt | 8 +- .../kotlin/ch/dissem/yaep/ui/desktop/main.kt | 11 +-- 6 files changed, 75 insertions(+), 80 deletions(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 44d234d..b6aed50 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -50,9 +50,6 @@ android { merges += "/META-INF/{AL2.0,LGPL2.1}" } } - kotlinOptions { - jvmTarget = libs.versions.jdk.get() - } } dependencies { implementation(libs.androidx.core.ktx) 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 7699cbc..afce694 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 @@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.aspectRatio 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.MaterialTheme import androidx.compose.material3.OutlinedCard @@ -33,7 +31,6 @@ import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.HorizontalClue -import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.domain.NeighbourClue import ch.dissem.yaep.domain.OrderClue import ch.dissem.yaep.domain.SameColumnClue @@ -216,48 +213,6 @@ fun PuzzleGrid( } } -@Composable -fun HorizontalClues( - modifier: Modifier = Modifier, - clues: List> -) { - LazyVerticalGrid( - modifier = modifier, - columns = GridCells.Fixed(4) - ) { - for (clue in clues) { - item { - HorizontalClue( - modifier = Modifier.forClue(clue), - 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 - ) - } - } - } -} - private fun Modifier.forClue(clue: DisplayClue): Modifier = this .alpha(if (clue.isActive) 1f else 0.2f) .padding(8.dp) 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 47e93bc..66ef3e0 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 @@ -1,5 +1,6 @@ package ch.dissem.yaep.ui.common +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout @@ -7,6 +8,7 @@ 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.fixed +import androidx.compose.ui.unit.Constraints.Companion.fixedWidth import androidx.compose.ui.unit.dp import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT @@ -41,10 +43,11 @@ fun AdaptiveGameLayout( grid: @Composable () -> Unit, horizontalClues: @Composable () -> Unit, verticalClues: @Composable () -> Unit, - time: @Composable () -> Unit + time: @Composable () -> Unit, + divider: @Composable () -> Unit = { HorizontalDivider() } ) { Layout( - contents = listOf(grid, horizontalClues, verticalClues, time), + contents = listOf(grid, horizontalClues, verticalClues, time, divider), modifier = modifier ) { measurables, constraints -> layout(width = constraints.maxWidth, height = constraints.maxHeight) { @@ -54,6 +57,7 @@ fun AdaptiveGameLayout( val horizontalCluesMeasurables = measurables[1] val verticalCluesMeasurables = measurables[2] val timeMeasurable = measurables[3][0] + val dividerMeasurable = measurables[4][0] val spacingPx = spacing.roundToPx() @@ -65,7 +69,8 @@ fun AdaptiveGameLayout( gridMeasurable, horizontalCluesMeasurables, verticalCluesMeasurables, - timeMeasurable + timeMeasurable, + dividerMeasurable ) } @@ -87,7 +92,8 @@ fun AdaptiveGameLayout( gridMeasurable, horizontalCluesMeasurables, verticalCluesMeasurables, - timeMeasurable + timeMeasurable, + dividerMeasurable ) } } @@ -101,7 +107,8 @@ private fun Placeable.PlacementScope.portrait( gridMeasurable: Measurable, horizontalCluesMeasurables: List, verticalCluesMeasurables: List, - timeMeasurable: Measurable + timeMeasurable: Measurable, + dividerMeasurable: Measurable ) { val gridSize = constraints.maxWidth val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx @@ -112,6 +119,7 @@ private fun Placeable.PlacementScope.portrait( val verticalCluesConstraints = fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2) val timeConstraints = fixed(timeWidth, bottomBarHeight / 2) + val dividerConstraints = fixedWidth(gridSize) val gridPlaceable = gridMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { @@ -121,15 +129,29 @@ private fun Placeable.PlacementScope.portrait( it.measure(verticalCluesConstraints) } val timePlaceable = timeMeasurable.measure(timeConstraints) + val dividerPlaceable = dividerMeasurable.measure(dividerConstraints) // Position the grid gridPlaceable.place(0, 0) // Position the horizontal clues - // TODO horizontalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + val offsetY = placeClues( + placeables = horizontalCluesPlaceables, + offsetX = 0, + offsetY = gridSize + 2 * spacingPx, + maxWidth = gridSize + ) + + // Add divider in between + dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx) // Position the vertical clues - // TODO verticalCluesPlaceable.place(0, gridPlaceable.height + horizontalCluesPlaceable.height + 2 * spacingPx) + placeClues( + placeables = verticalCluesPlaceables, + offsetX = 0, + offsetY = offsetY + 2 * spacingPx + dividerPlaceable.height, + maxWidth = gridSize + ) // Position the time timePlaceable.place( @@ -146,14 +168,20 @@ private fun Placeable.PlacementScope.squarish( verticalCluesMeasurables: List, timeMeasurable: Measurable ) { - val gridSize = (8 * min(constraints.maxWidth, constraints.maxHeight)) / 10 + val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10 + val gridItemSize = (gridSize - 12 * spacingPx) / 18 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 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() val gridPlaceable = gridMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { @@ -168,10 +196,20 @@ private fun Placeable.PlacementScope.squarish( gridPlaceable.place(0, 0) // Position the horizontal clues - // TODO horizontalCluesPlaceable.place(gridPlaceable.width + spacingPx, 0) + placeClues( + placeables = horizontalCluesPlaceables, + offsetX = gridSize + 2 * spacingPx, + offsetY = 0, + maxWidth = rightBarWidth + ) // Position the vertical clues - // TODO verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx) + placeClues( + placeables = verticalCluesPlaceables, + offsetX = 0, + offsetY = gridSize + 2 * spacingPx, + maxWidth = gridSize + ) // Position the time timePlaceable.place( @@ -186,11 +224,12 @@ private fun Placeable.PlacementScope.landscape( gridMeasurable: Measurable, horizontalCluesMeasurables: List, verticalCluesMeasurables: List, - timeMeasurable: Measurable + timeMeasurable: Measurable, + dividerMeasurable: Measurable ) { val gridSize = constraints.maxHeight val gridItemSize = (gridSize - 12 * spacingPx) / 18 - val rightBarWidth = constraints.maxWidth - gridSize - spacingPx + val rightBarWidth = constraints.maxWidth - gridSize - 2 * spacingPx val gridConstraints = fixed(gridSize, gridSize) val baseSpace = gridSize - 2 * spacingPx @@ -203,6 +242,7 @@ private fun Placeable.PlacementScope.landscape( height = 3 * gridItemSize + 2 * spacingPx ) val timeConstraints = Constraints.fixedHeight(baseSpace / 10) + val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx) val gridPlaceable = gridMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { @@ -212,6 +252,7 @@ private fun Placeable.PlacementScope.landscape( it.measure(verticalCluesConstraints) } val timePlaceable = timeMeasurable.measure(timeConstraints) + val dividerPlaceable = dividerMeasurable.measure(dividerConstraints) // Position the grid gridPlaceable.place(0, 0) @@ -219,18 +260,19 @@ private fun Placeable.PlacementScope.landscape( // Position the horizontal clues val offsetY = placeClues( placeables = horizontalCluesPlaceables, - offsetX = gridPlaceable.width + spacingPx, + offsetX = gridSize + 2 * spacingPx, offsetY = 0, maxWidth = rightBarWidth ) - // TODO: add spacer in between + // Add divider in between + dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx) // Position the vertical clues placeClues( placeables = verticalCluesPlaceables, - offsetX = gridPlaceable.width + spacingPx, - offsetY = offsetY + spacingPx, + offsetX = gridSize + 2 * spacingPx, + offsetY = offsetY + 2 * spacingPx + dividerPlaceable.height, maxWidth = rightBarWidth ) @@ -253,7 +295,7 @@ private fun Placeable.PlacementScope.placeClues( 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)) + val spacing = if (columns == 1) 0 else (maxWidth - columns * itemWidth) / (columns - 1) var currentX = offsetX var currentY = offsetY var i = 0 @@ -261,7 +303,7 @@ private fun Placeable.PlacementScope.placeClues( placeable.place(currentX, currentY) currentX += itemWidth + spacing i++ - if (i % columns == 0) { + if (i % columns == 0 && i < placeables.size) { currentX = offsetX currentY += itemHeight } diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 0ca2bd2..575dc8c 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -25,7 +25,7 @@ kotlin { compose.desktop { application { - mainClass = "MainKt" + mainClass = "ch.dissem.yaep.ui.desktop.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) diff --git a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt index fac2c93..5a18d55 100644 --- a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt +++ b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt @@ -94,16 +94,16 @@ fun AppBar( Icon( painter = painterResource( if (useDarkMode) { - CRes.drawable.sun - } else { CRes.drawable.moon + } else { + CRes.drawable.sun } ), contentDescription = stringResource( if (useDarkMode) { - CRes.string.use_light_mode - } else { CRes.string.use_dark_mode + } else { + CRes.string.use_light_mode } ), modifier = Modifier.size(SwitchDefaults.IconSize), diff --git a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt index 8868d99..2110904 100644 --- a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt +++ b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt @@ -1,3 +1,5 @@ +package ch.dissem.yaep.ui.desktop + import androidx.compose.foundation.layout.padding import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,8 +20,6 @@ import ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.ui.common.App import ch.dissem.yaep.ui.common.theme.emojiFontFamily -import ch.dissem.yaep.ui.desktop.AppBar -import ch.dissem.yaep.ui.desktop.DesktopWindow import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import yaep.commonui.generated.resources.app_name @@ -27,7 +27,7 @@ import yaep.desktop.generated.resources.ic_launcher import yaep.commonui.generated.resources.Res as CRes import yaep.desktop.generated.resources.Res as DRes -fun main() = application { +fun main(): Unit = application { emojiFontFamily = FontFamily( Font( resource = "NotoColorEmoji-Regular.ttf", @@ -37,7 +37,7 @@ fun main() = application { ) val windowState = rememberWindowState( placement = WindowPlacement.Floating, - size = DpSize(1200.dp, 800.dp) + size = DpSize(1250.dp, 800.dp) ) var game by remember { mutableStateOf(generateGame()) } @@ -58,7 +58,8 @@ fun main() = application { setDarkMode = { useDarkMode = it }, onCloseRequest = ::exitApplication, onRestart = { - do while (game.grid.undo()); + @Suppress("ControlFlowWithEmptyBody") + do /* nothing */ while (game.grid.undo()) resetCluesBeacon = Any() }, windowState = windowState,