From 842e6faf248921efbfeacc806234046b3674994a Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 29 Oct 2025 22:13:06 +0100 Subject: [PATCH] Fix clue rendering on light mode --- .../kotlin/ch/dissem/yaep/ui/common/App.kt | 237 ------------------ .../kotlin/ch/dissem/yaep/ui/common/clues.kt | 144 +++++++++++ .../kotlin/ch/dissem/yaep/ui/common/grid.kt | 118 +++++++++ 3 files changed, 262 insertions(+), 237 deletions(-) create mode 100644 commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/clues.kt create mode 100644 commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.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 fc9143b..318952a 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 @@ -1,18 +1,6 @@ package ch.dissem.yaep.ui.common -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image 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.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -22,46 +10,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType -import androidx.compose.ui.unit.dp -import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.Game -import ch.dissem.yaep.domain.GameCell -import ch.dissem.yaep.domain.GameRow -import ch.dissem.yaep.domain.Grid -import ch.dissem.yaep.domain.HorizontalClue -import ch.dissem.yaep.domain.Item -import ch.dissem.yaep.domain.ItemClass -import ch.dissem.yaep.domain.NeighbourClue -import ch.dissem.yaep.domain.OrderClue -import ch.dissem.yaep.domain.SameColumnClue -import ch.dissem.yaep.domain.TripletClue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import org.jetbrains.compose.resources.painterResource -import yaep.commonui.generated.resources.Res -import yaep.commonui.generated.resources.neighbour -import yaep.commonui.generated.resources.order import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime -class DisplayClue(val clue: C) { - var isActive: Boolean by mutableStateOf(true) - - var isViolated: Boolean by mutableStateOf(false) - - fun update(grid: Grid) { - isViolated = !clue.isValid(grid) - if (isViolated) { - isActive = true - } - } -} @Composable @OptIn(ExperimentalTime::class) @@ -140,199 +98,4 @@ fun App( } } -@Composable -fun PuzzleGrid( - modifier: Modifier = Modifier, - selectDirectly: Boolean, - spacing: Dp = 8.dp, - grid: Grid, - onUpdate: () -> Unit -) { - Column(modifier = modifier) { - for (row in grid) { - PuzzleRow( - row = row, - onUpdate = onUpdate, - onSnapshot = { grid.snapshot() }, - onUndo = { grid.undo() }, - spacing = spacing, - selectDirectly = selectDirectly - ) - } - } -} - -@Composable -private fun PuzzleRow( - row: GameRow>, - onUpdate: () -> Unit, - onSnapshot: () -> Unit, - onUndo: () -> Boolean, - spacing: Dp, - selectDirectly: Boolean -) { - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - val allOptions = row.options - for (cell in row) { - var selection by remember(cell) { mutableStateOf(cell.selection) } - val options = remember(cell) { - allOptions.map { Toggleable(it, cell.options.contains(it)) } - } - LaunchedEffect(cell) { - cell.optionsChangedListeners.add { enabled -> - options.forEach { it.enabled = enabled.contains(it.item) } - } - cell.selectionChangedListeners.add { - selection = it - onUpdate() - } - } - Selector( - modifier = Modifier - .padding(spacing) - .weight(1f), - spacing, - selectDirectly = selectDirectly, - options = options, - onOptionRemoved = { - onSnapshot() - cell.options.remove(it) - row.cleanupOptions() - }, - onOptionAdded = { - cell.options.add(it) - }, - selectedItem = selection, - onSelectItem = { selectedItem -> - onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo) - } - ) - } - } -} - -private fun onSelectItem( - row: GameRow>, - cell: GameCell>, - options: List>>>, - selectedItem: Item>?, - onSnapshot: () -> Unit, - onUndo: () -> Boolean -) { - if (selectedItem != null) { - onSnapshot() - cell.selection = selectedItem - row.cleanupOptions() - } else { - while (cell.selection != null) { - if (!onUndo()) break - } - options.forEach { option -> - option.enabled = cell.options.contains(option.item) - } - } -} - -private fun Modifier.forClue(clue: DisplayClue, padding: Dp = 8.dp): Modifier = this - .alpha(if (clue.isActive) 1f else 0.2f) - .padding(padding) - .onEitherPointerAction { clue.isActive = !clue.isActive } - -@Composable -fun HorizontalClue( - modifier: Modifier = Modifier, - spacing: Dp, - clue: HorizontalClue, - isClueViolated: Boolean -) { - ClueCard( - modifier = modifier, - spacing = spacing, - isClueViolated = isClueViolated - ) { - Row { - when (clue) { - is NeighbourClue<*, *> -> { - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) - Image( - modifier = Modifier.aspectRatio(1f).weight(1f), - painter = painterResource(Res.drawable.neighbour), - contentDescription = null - ) - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) - } - - is OrderClue<*, *> -> { - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.left) - Image( - modifier = Modifier.aspectRatio(1f).weight(1f), - painter = painterResource(Res.drawable.order), - contentDescription = null - ) - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.right) - } - - is TripletClue<*, *, *> -> { - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.c) - } - } - } - } -} - -@Composable -fun VerticalClue( - modifier: Modifier = Modifier, - spacing: Dp, - clue: SameColumnClue<*, *>, - isClueViolated: Boolean = false -) { - ClueCard( - modifier = modifier.aspectRatio(0.5f), - spacing = spacing, - isClueViolated = isClueViolated - ) { - Column { - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) - DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) - } - } -} - expect fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext = Dispatchers.IO) - -@Composable -fun ClueCard( - modifier: Modifier = Modifier, - spacing: Dp, - isClueViolated: Boolean, - content: @Composable () -> Unit -) { - val colors = MaterialTheme.colorScheme - OutlinedCard( - modifier = if (isClueViolated) { - modifier.shadow( - 8.dp, - shape = CardDefaults.outlinedShape, - ambientColor = colors.error, - spotColor = colors.error - ) - } else { - modifier - }, - shape = RoundedCornerShape(spacing), - border = if (isClueViolated) { - remember { BorderStroke(1.0.dp, colors.error) } - } else { - CardDefaults.outlinedCardBorder() - } - ) { - content() - } -} diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/clues.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/clues.kt new file mode 100644 index 0000000..e41fd3b --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/clues.kt @@ -0,0 +1,144 @@ +package ch.dissem.yaep.ui.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.unit.Dp +import androidx.compose.ui.unit.dp +import ch.dissem.yaep.domain.Clue +import ch.dissem.yaep.domain.Grid +import ch.dissem.yaep.domain.HorizontalClue +import ch.dissem.yaep.domain.NeighbourClue +import ch.dissem.yaep.domain.OrderClue +import ch.dissem.yaep.domain.SameColumnClue +import ch.dissem.yaep.domain.TripletClue +import org.jetbrains.compose.resources.painterResource +import yaep.commonui.generated.resources.Res +import yaep.commonui.generated.resources.neighbour +import yaep.commonui.generated.resources.order + +fun Modifier.forClue(clue: DisplayClue, padding: Dp = 8.dp): Modifier = this + .alpha(if (clue.isActive) 1f else 0.2f) + .padding(padding) + .onEitherPointerAction { clue.isActive = !clue.isActive } + +@Composable +fun HorizontalClue( + modifier: Modifier = Modifier, + spacing: Dp, + clue: HorizontalClue, + isClueViolated: Boolean +) { + ClueCard( + modifier = modifier, + spacing = spacing, + isClueViolated = isClueViolated + ) { + Row { + when (clue) { + is NeighbourClue<*, *> -> { + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) + Icon( + modifier = Modifier.aspectRatio(1f).weight(1f), + painter = painterResource(Res.drawable.neighbour), + contentDescription = null + ) + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) + } + + is OrderClue<*, *> -> { + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.left) + Icon( + modifier = Modifier.aspectRatio(1f).weight(1f), + painter = painterResource(Res.drawable.order), + contentDescription = null + ) + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.right) + } + + is TripletClue<*, *, *> -> { + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.c) + } + } + } + } +} + +@Composable +fun VerticalClue( + modifier: Modifier = Modifier, + spacing: Dp, + clue: SameColumnClue<*, *>, + isClueViolated: Boolean = false +) { + ClueCard( + modifier = modifier.aspectRatio(0.5f), + spacing = spacing, + isClueViolated = isClueViolated + ) { + Column { + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a) + DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b) + } + } +} + +@Composable +fun ClueCard( + modifier: Modifier = Modifier, + spacing: Dp, + isClueViolated: Boolean, + content: @Composable () -> Unit +) { + val colors = MaterialTheme.colorScheme + OutlinedCard( + modifier = if (isClueViolated) { + modifier.shadow( + 8.dp, + shape = CardDefaults.outlinedShape, + ambientColor = colors.error, + spotColor = colors.error + ) + } else { + modifier + }, + shape = RoundedCornerShape(spacing), + border = if (isClueViolated) { + remember { BorderStroke(1.0.dp, colors.error) } + } else { + CardDefaults.outlinedCardBorder() + } + ) { + content() + } +} + +class DisplayClue(val clue: C) { + var isActive: Boolean by mutableStateOf(true) + + var isViolated: Boolean by mutableStateOf(false) + + fun update(grid: Grid) { + isViolated = !clue.isValid(grid) + if (isViolated) { + isActive = true + } + } +} diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.kt new file mode 100644 index 0000000..8a75304 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.kt @@ -0,0 +1,118 @@ +package ch.dissem.yaep.ui.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import ch.dissem.yaep.domain.GameCell +import ch.dissem.yaep.domain.GameRow +import ch.dissem.yaep.domain.Grid +import ch.dissem.yaep.domain.Item +import ch.dissem.yaep.domain.ItemClass + +@Composable +fun PuzzleGrid( + modifier: Modifier = Modifier, + selectDirectly: Boolean, + spacing: Dp = 8.dp, + grid: Grid, + onUpdate: () -> Unit +) { + Column(modifier = modifier) { + for (row in grid) { + PuzzleRow( + row = row, + onUpdate = onUpdate, + onSnapshot = { grid.snapshot() }, + onUndo = { grid.undo() }, + spacing = spacing, + selectDirectly = selectDirectly + ) + } + } +} + +@Composable +private fun PuzzleRow( + row: GameRow>, + onUpdate: () -> Unit, + onSnapshot: () -> Unit, + onUndo: () -> Boolean, + spacing: Dp, + selectDirectly: Boolean +) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + val allOptions = row.options + for (cell in row) { + var selection by remember(cell) { mutableStateOf(cell.selection) } + val options = remember(cell) { + allOptions.map { Toggleable(it, cell.options.contains(it)) } + } + LaunchedEffect(cell) { + cell.optionsChangedListeners.add { enabled -> + options.forEach { it.enabled = enabled.contains(it.item) } + } + cell.selectionChangedListeners.add { + selection = it + onUpdate() + } + } + Selector( + modifier = Modifier + .padding(spacing) + .weight(1f), + spacing, + selectDirectly = selectDirectly, + options = options, + onOptionRemoved = { + onSnapshot() + cell.options.remove(it) + row.cleanupOptions() + }, + onOptionAdded = { + cell.options.add(it) + }, + selectedItem = selection, + onSelectItem = { selectedItem -> + onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo) + } + ) + } + } +} + +private fun onSelectItem( + row: GameRow>, + cell: GameCell>, + options: List>>>, + selectedItem: Item>?, + onSnapshot: () -> Unit, + onUndo: () -> Boolean +) { + if (selectedItem != null) { + onSnapshot() + cell.selection = selectedItem + row.cleanupOptions() + } else { + while (cell.selection != null) { + if (!onUndo()) break + } + options.forEach { option -> + option.enabled = cell.options.contains(option.item) + } + } +}