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 318952a..ee7e92c 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 @@ -15,12 +15,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import ch.dissem.yaep.domain.Game +import ch.dissem.yaep.domain.HorizontalClue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime - @Composable @OptIn(ExperimentalTime::class) fun App( diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle clues.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle clues.kt new file mode 100644 index 0000000..e9abc55 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle clues.kt @@ -0,0 +1,184 @@ +package ch.dissem.yaep.ui.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +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 ch.dissem.yaep.domain.Clue +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 +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 + +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 +fun PuzzleClues( + modifier: Modifier = Modifier.Companion, + horizontalClues: List>, + verticalClues: List, ItemClass<*>>>> +) { + Column(modifier = modifier) { + LazyVerticalGrid( + modifier = Modifier.Companion.fillMaxWidth().wrapContentHeight(), + columns = GridCells.Fixed(4) + ) { + for (clue in horizontalClues) { + item { + HorizontalClue( + modifier = Modifier.Companion + .forClue(clue), + clue = clue.clue, + isClueViolated = clue.isViolated + ) + } + } + } + HorizontalDivider() + LazyVerticalGrid( + modifier = Modifier.Companion.fillMaxWidth().wrapContentHeight(), + columns = GridCells.Fixed(8) + ) { + for (clue in verticalClues) { + item { + VerticalClue( + modifier = Modifier.Companion + .forClue(clue) + .aspectRatio(0.33333334f), + 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) + .onEitherPointerAction { clue.isActive = !clue.isActive } + +@Composable +fun HorizontalClue( + modifier: Modifier = Modifier.Companion, + clue: HorizontalClue, + isClueViolated: Boolean +) { + ClueCard( + modifier = modifier, + isClueViolated = isClueViolated + ) { + Row { + when (clue) { + is NeighbourClue<*, *> -> { + DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) + Image( + modifier = Modifier.Companion.aspectRatio(1f).weight(1f), + painter = painterResource(Res.drawable.neighbour), + contentDescription = null + ) + DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) + } + + is OrderClue<*, *> -> { + DrawItem(modifier = Modifier.Companion.weight(1f), clue.left) + Image( + modifier = Modifier.Companion.aspectRatio(1f).weight(1f), + painter = painterResource(Res.drawable.order), + contentDescription = null + ) + DrawItem(modifier = Modifier.Companion.weight(1f), clue.right) + } + + is TripletClue<*, *, *> -> { + DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) + DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) + DrawItem(modifier = Modifier.Companion.weight(1f), clue.c) + } + } + } + } +} + +@Composable +fun VerticalClue( + modifier: Modifier = Modifier.Companion, + clue: SameColumnClue<*, *>, + isClueViolated: Boolean = false +) { + ClueCard( + modifier = modifier.aspectRatio(0.5f), + isClueViolated = isClueViolated + ) { + Column { + DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) + DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) + } + } +} + +@Composable +fun ClueCard( + modifier: Modifier = Modifier.Companion, + 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 + }, + border = if (isClueViolated) { + remember { BorderStroke(1.0.dp, colors.error) } + } else { + CardDefaults.outlinedCardBorder() + } + ) { + content() + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle grid.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle grid.kt new file mode 100644 index 0000000..f93e6b4 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle grid.kt @@ -0,0 +1,79 @@ +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 ch.dissem.yaep.domain.Grid + +@Composable +fun PuzzleGrid( + modifier: Modifier = Modifier.Companion, + grid: Grid, + onUpdate: () -> Unit +) { + Column(modifier = modifier) { + for (row in grid) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .wrapContentHeight() + ) { + val allOptions = row.options + for (item in row) { + var selection by remember(item) { mutableStateOf(item.selection) } + val options = remember(item) { + allOptions.map { Toggleable(it, item.options.contains(it)) } + } + LaunchedEffect(item) { + item.optionsChangedListeners.add { enabled -> + options.forEach { it.enabled = enabled.contains(it.item) } + } + item.selectionChangedListeners.add { + selection = it + onUpdate() + } + } + Selector( + modifier = Modifier.Companion + .padding(8.dp) + .weight(1f), + options = options, + onOptionRemoved = { + grid.snapshot() + item.options.remove(it) + row.cleanupOptions() + }, + onOptionAdded = { + item.options.add(it) + }, + selectedItem = selection, + onSelectItem = { + if (it != null) { + grid.snapshot() + item.selection = it + row.cleanupOptions() + } else { + while (item.selection != null) { + if (!grid.undo()) break + } + options.forEach { + it.enabled = item.options.contains(it.item) + } + } + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/widget status.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/widget status.kt new file mode 100644 index 0000000..dd1e5db --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/widget status.kt @@ -0,0 +1,44 @@ +import androidx.compose.foundation.border +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp + +class StatusManager { + lateinit var focused: StatusHolder + private set + + fun focusNext() { + focused = focused.next + } + + fun focusPrevious() { + focused = focused.previous + } +} + +class StatusHolder(private val manager: StatusManager) { + val hasFocus: Boolean + get() = manager.focused == this + var isSelected: Boolean = false + + var previous: StatusHolder = this + private set + var next: StatusHolder = this + set(value) { + field = value + value.previous = this + } +} + +@Composable +fun Modifier.status(holder: StatusHolder): Modifier = if (holder.hasFocus) { + border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = RectangleShape + ) +} else { + this +} \ No newline at end of file