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}"
}
}
kotlinOptions {
jvmTarget = libs.versions.jdk.get()
}
}
dependencies {
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.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<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
.alpha(if (clue.isActive) 1f else 0.2f)
.padding(8.dp)

View File

@@ -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<Measurable>,
verticalCluesMeasurables: List<Measurable>,
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<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 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<Measurable>,
verticalCluesMeasurables: List<Measurable>,
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
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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<Game>(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,