Adaptive Layout (WIP)

This commit is contained in:
Christian Basler
2025-07-09 21:19:41 +02:00
parent 3dbc994dc5
commit 7f88095a4b
6 changed files with 75 additions and 80 deletions

View File

@@ -50,9 +50,6 @@ android {
merges += "/META-INF/{AL2.0,LGPL2.1}" merges += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
kotlinOptions {
jvmTarget = libs.versions.jdk.get()
}
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)

View File

@@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight 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.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard 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.Game
import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.Grid
import ch.dissem.yaep.domain.HorizontalClue import ch.dissem.yaep.domain.HorizontalClue
import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.domain.NeighbourClue import ch.dissem.yaep.domain.NeighbourClue
import ch.dissem.yaep.domain.OrderClue import ch.dissem.yaep.domain.OrderClue
import ch.dissem.yaep.domain.SameColumnClue import ch.dissem.yaep.domain.SameColumnClue
@@ -216,48 +213,6 @@ fun PuzzleGrid(
} }
} }
@Composable
fun HorizontalClues(
modifier: Modifier = Modifier,
clues: List<DisplayClue<HorizontalClue>>
) {
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<DisplayClue<SameColumnClue<ItemClass<*>, 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<out Clue>): Modifier = this private fun Modifier.forClue(clue: DisplayClue<out Clue>): Modifier = this
.alpha(if (clue.isActive) 1f else 0.2f) .alpha(if (clue.isActive) 1f else 0.2f)
.padding(8.dp) .padding(8.dp)

View File

@@ -1,5 +1,6 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout 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.layout.Placeable
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Constraints.Companion.fixed import androidx.compose.ui.unit.Constraints.Companion.fixed
import androidx.compose.ui.unit.Constraints.Companion.fixedWidth
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE
import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT
@@ -41,10 +43,11 @@ fun AdaptiveGameLayout(
grid: @Composable () -> Unit, grid: @Composable () -> Unit,
horizontalClues: @Composable () -> Unit, horizontalClues: @Composable () -> Unit,
verticalClues: @Composable () -> Unit, verticalClues: @Composable () -> Unit,
time: @Composable () -> Unit time: @Composable () -> Unit,
divider: @Composable () -> Unit = { HorizontalDivider() }
) { ) {
Layout( Layout(
contents = listOf(grid, horizontalClues, verticalClues, time), contents = listOf(grid, horizontalClues, verticalClues, time, divider),
modifier = modifier modifier = modifier
) { measurables, constraints -> ) { measurables, constraints ->
layout(width = constraints.maxWidth, height = constraints.maxHeight) { layout(width = constraints.maxWidth, height = constraints.maxHeight) {
@@ -54,6 +57,7 @@ fun AdaptiveGameLayout(
val horizontalCluesMeasurables = measurables[1] val horizontalCluesMeasurables = measurables[1]
val verticalCluesMeasurables = measurables[2] val verticalCluesMeasurables = measurables[2]
val timeMeasurable = measurables[3][0] val timeMeasurable = measurables[3][0]
val dividerMeasurable = measurables[4][0]
val spacingPx = spacing.roundToPx() val spacingPx = spacing.roundToPx()
@@ -65,7 +69,8 @@ fun AdaptiveGameLayout(
gridMeasurable, gridMeasurable,
horizontalCluesMeasurables, horizontalCluesMeasurables,
verticalCluesMeasurables, verticalCluesMeasurables,
timeMeasurable timeMeasurable,
dividerMeasurable
) )
} }
@@ -87,7 +92,8 @@ fun AdaptiveGameLayout(
gridMeasurable, gridMeasurable,
horizontalCluesMeasurables, horizontalCluesMeasurables,
verticalCluesMeasurables, verticalCluesMeasurables,
timeMeasurable timeMeasurable,
dividerMeasurable
) )
} }
} }
@@ -101,7 +107,8 @@ private fun Placeable.PlacementScope.portrait(
gridMeasurable: Measurable, gridMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>, horizontalCluesMeasurables: List<Measurable>,
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable timeMeasurable: Measurable,
dividerMeasurable: Measurable
) { ) {
val gridSize = constraints.maxWidth val gridSize = constraints.maxWidth
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
@@ -112,6 +119,7 @@ private fun Placeable.PlacementScope.portrait(
val verticalCluesConstraints = val verticalCluesConstraints =
fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2) fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2)
val timeConstraints = fixed(timeWidth, bottomBarHeight / 2) val timeConstraints = fixed(timeWidth, bottomBarHeight / 2)
val dividerConstraints = fixedWidth(gridSize)
val gridPlaceable = gridMeasurable.measure(gridConstraints) val gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
@@ -121,15 +129,29 @@ private fun Placeable.PlacementScope.portrait(
it.measure(verticalCluesConstraints) it.measure(verticalCluesConstraints)
} }
val timePlaceable = timeMeasurable.measure(timeConstraints) val timePlaceable = timeMeasurable.measure(timeConstraints)
val dividerPlaceable = dividerMeasurable.measure(dividerConstraints)
// Position the grid // Position the grid
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
// Position the horizontal clues // 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 // 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 // Position the time
timePlaceable.place( timePlaceable.place(
@@ -146,14 +168,20 @@ private fun Placeable.PlacementScope.squarish(
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable 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 rightBarWidth = constraints.maxWidth - gridSize - spacingPx
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
val gridConstraints = fixed(gridSize, gridSize) val gridConstraints = fixed(gridSize, gridSize)
val horizontalCluesConstraints = fixed(rightBarWidth, gridSize) val horizontalCluesConstraints = fixed(
val verticalCluesConstraints = fixed(gridSize, bottomBarHeight) width = 3 * gridItemSize + 2 * spacingPx,
val timeConstraints = fixed(rightBarWidth, bottomBarHeight) 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 gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
@@ -168,10 +196,20 @@ private fun Placeable.PlacementScope.squarish(
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
// Position the horizontal clues // 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 // 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 // Position the time
timePlaceable.place( timePlaceable.place(
@@ -186,11 +224,12 @@ private fun Placeable.PlacementScope.landscape(
gridMeasurable: Measurable, gridMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>, horizontalCluesMeasurables: List<Measurable>,
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable timeMeasurable: Measurable,
dividerMeasurable: Measurable
) { ) {
val gridSize = constraints.maxHeight val gridSize = constraints.maxHeight
val gridItemSize = (gridSize - 12 * spacingPx) / 18 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 gridConstraints = fixed(gridSize, gridSize)
val baseSpace = gridSize - 2 * spacingPx val baseSpace = gridSize - 2 * spacingPx
@@ -203,6 +242,7 @@ private fun Placeable.PlacementScope.landscape(
height = 3 * gridItemSize + 2 * spacingPx height = 3 * gridItemSize + 2 * spacingPx
) )
val timeConstraints = Constraints.fixedHeight(baseSpace / 10) val timeConstraints = Constraints.fixedHeight(baseSpace / 10)
val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx)
val gridPlaceable = gridMeasurable.measure(gridConstraints) val gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
@@ -212,6 +252,7 @@ private fun Placeable.PlacementScope.landscape(
it.measure(verticalCluesConstraints) it.measure(verticalCluesConstraints)
} }
val timePlaceable = timeMeasurable.measure(timeConstraints) val timePlaceable = timeMeasurable.measure(timeConstraints)
val dividerPlaceable = dividerMeasurable.measure(dividerConstraints)
// Position the grid // Position the grid
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
@@ -219,18 +260,19 @@ private fun Placeable.PlacementScope.landscape(
// Position the horizontal clues // Position the horizontal clues
val offsetY = placeClues( val offsetY = placeClues(
placeables = horizontalCluesPlaceables, placeables = horizontalCluesPlaceables,
offsetX = gridPlaceable.width + spacingPx, offsetX = gridSize + 2 * spacingPx,
offsetY = 0, offsetY = 0,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
// TODO: add spacer in between // Add divider in between
dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx)
// Position the vertical clues // Position the vertical clues
placeClues( placeClues(
placeables = verticalCluesPlaceables, placeables = verticalCluesPlaceables,
offsetX = gridPlaceable.width + spacingPx, offsetX = gridSize + 2 * spacingPx,
offsetY = offsetY + spacingPx, offsetY = offsetY + 2 * spacingPx + dividerPlaceable.height,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
@@ -253,7 +295,7 @@ private fun Placeable.PlacementScope.placeClues(
val itemWidth = placeables.first().width val itemWidth = placeables.first().width
val itemHeight = placeables.first().height val itemHeight = placeables.first().height
val columns = max(1, maxWidth / itemWidth) 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 currentX = offsetX
var currentY = offsetY var currentY = offsetY
var i = 0 var i = 0
@@ -261,7 +303,7 @@ private fun Placeable.PlacementScope.placeClues(
placeable.place(currentX, currentY) placeable.place(currentX, currentY)
currentX += itemWidth + spacing currentX += itemWidth + spacing
i++ i++
if (i % columns == 0) { if (i % columns == 0 && i < placeables.size) {
currentX = offsetX currentX = offsetX
currentY += itemHeight currentY += itemHeight
} }

View File

@@ -25,7 +25,7 @@ kotlin {
compose.desktop { compose.desktop {
application { application {
mainClass = "MainKt" mainClass = "ch.dissem.yaep.ui.desktop.MainKt"
nativeDistributions { nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)

View File

@@ -94,16 +94,16 @@ fun AppBar(
Icon( Icon(
painter = painterResource( painter = painterResource(
if (useDarkMode) { if (useDarkMode) {
CRes.drawable.sun
} else {
CRes.drawable.moon CRes.drawable.moon
} else {
CRes.drawable.sun
} }
), ),
contentDescription = stringResource( contentDescription = stringResource(
if (useDarkMode) { if (useDarkMode) {
CRes.string.use_light_mode
} else {
CRes.string.use_dark_mode CRes.string.use_dark_mode
} else {
CRes.string.use_light_mode
} }
), ),
modifier = Modifier.size(SwitchDefaults.IconSize), modifier = Modifier.size(SwitchDefaults.IconSize),

View File

@@ -1,3 +1,5 @@
package ch.dissem.yaep.ui.desktop
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.domain.generateGame
import ch.dissem.yaep.ui.common.App import ch.dissem.yaep.ui.common.App
import ch.dissem.yaep.ui.common.theme.emojiFontFamily 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.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import yaep.commonui.generated.resources.app_name 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.commonui.generated.resources.Res as CRes
import yaep.desktop.generated.resources.Res as DRes import yaep.desktop.generated.resources.Res as DRes
fun main() = application { fun main(): Unit = application {
emojiFontFamily = FontFamily( emojiFontFamily = FontFamily(
Font( Font(
resource = "NotoColorEmoji-Regular.ttf", resource = "NotoColorEmoji-Regular.ttf",
@@ -37,7 +37,7 @@ fun main() = application {
) )
val windowState = rememberWindowState( val windowState = rememberWindowState(
placement = WindowPlacement.Floating, placement = WindowPlacement.Floating,
size = DpSize(1200.dp, 800.dp) size = DpSize(1250.dp, 800.dp)
) )
var game by remember { mutableStateOf<Game>(generateGame()) } var game by remember { mutableStateOf<Game>(generateGame()) }
@@ -58,7 +58,8 @@ fun main() = application {
setDarkMode = { useDarkMode = it }, setDarkMode = { useDarkMode = it },
onCloseRequest = ::exitApplication, onCloseRequest = ::exitApplication,
onRestart = { onRestart = {
do while (game.grid.undo()); @Suppress("ControlFlowWithEmptyBody")
do /* nothing */ while (game.grid.undo())
resetCluesBeacon = Any() resetCluesBeacon = Any()
}, },
windowState = windowState, windowState = windowState,