Fix clue rendering on light mode
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 5m35s

This commit is contained in:
2025-10-29 22:13:06 +01:00
parent 222a09ee21
commit 842e6faf24
3 changed files with 262 additions and 237 deletions

View File

@@ -1,18 +1,6 @@
package ch.dissem.yaep.ui.common 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.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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -22,46 +10,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType 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.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.CoroutineScope
import kotlinx.coroutines.Dispatchers 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.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
class DisplayClue<C : Clue>(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 @Composable
@OptIn(ExperimentalTime::class) @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<ItemClass<*>>,
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<ItemClass<*>>,
cell: GameCell<ItemClass<*>>,
options: List<Toggleable<Item<ItemClass<*>>>>,
selectedItem: Item<ItemClass<*>>?,
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<out Clue>, 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) 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()
}
}

View File

@@ -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<out Clue>, 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<C : Clue>(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
}
}
}

View File

@@ -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<ItemClass<*>>,
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<ItemClass<*>>,
cell: GameCell<ItemClass<*>>,
options: List<Toggleable<Item<ItemClass<*>>>>,
selectedItem: Item<ItemClass<*>>?,
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)
}
}
}