diff --git a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt index 35df9fd..6a75278 100644 --- a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -2,34 +2,16 @@ package ch.dissem.yaep.ui.common import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.HorizontalDivider 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.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha 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 ch.dissem.yaep.domain.generateGame +import ch.dissem.yaep.domain.* import org.jetbrains.compose.resources.painterResource import yaep.commonui.generated.resources.Res import yaep.commonui.generated.resources.neighbour @@ -66,15 +48,30 @@ fun PuzzleGrid( .fillMaxWidth() .wrapContentHeight() ) { + val allOptions = row.options for (item in row) { + var selection by remember { mutableStateOf(item.selection) } + val options = remember { allOptions.map { Toggleable(it, item.options.contains(it)) } } + LaunchedEffect(item) { + item.removedListeners.add { removed -> + options + .filter { removed.contains(it.item) } + .forEach { it.enabled = false } + } + item.selectionChangedListeners.add { + selection = it + } + } Selector( modifier = Modifier .padding(8.dp) .weight(1f), - category = row.category, - options = item.options, - selectedItem = item.selection, - onSelectItem = { item.selection = it } + options = options, + selectedItem = selection, + onSelectItem = { + item.selection = it + grid.cleanupOptions() + } ) } } @@ -169,4 +166,4 @@ fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>) { DrawItem(modifier = Modifier.weight(1f), clue.b) } } -} \ No newline at end of file +} diff --git a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt index 3b48880..9e0de46 100644 --- a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt +++ b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt @@ -1,7 +1,6 @@ package ch.dissem.yaep.ui.common -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -9,24 +8,28 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material3.OutlinedCard import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.ItemClass -import ch.dissem.yaep.domain.ItemClassCompanion import ch.dissem.yaep.ui.common.theme.emojiFontFamily import kotlin.math.min +@OptIn(ExperimentalFoundationApi::class) @Composable fun > Selector( modifier: Modifier = Modifier, - category: ItemClassCompanion, - options: List>, + options: List>>, selectedItem: Item?, - onSelectItem: (Item?) -> Unit + onSelectItem: (Item?) -> Unit, ) { if (selectedItem != null) { DrawItem(item = selectedItem, modifier = modifier.clickable { onSelectItem(null) }) @@ -40,8 +43,19 @@ fun > Selector( for (option in options) { item { DrawItem( - item = option, - modifier = Modifier.clickable { onSelectItem(option) } + item = option.item, + modifier = Modifier + .alpha(if (option.enabled) 1f else 0.1f) + .combinedClickable( + onClick = { onSelectItem(option.item) }, + onLongClick = { option.enabled = false } + ) + .onClick( + matcher = PointerMatcher.mouse(PointerButton.Secondary), + onClick = { + option.enabled = false + } + ) ) } } @@ -81,7 +95,10 @@ fun > DrawItem( drawText( textMeasurer = textMeasurer, text = emoji, - style = TextStyle(fontSize = fontSize, fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily), + style = TextStyle( + fontSize = fontSize, + fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily + ), topLeft = offset ) } @@ -89,3 +106,23 @@ fun > DrawItem( } } +class Toggleable(val item: T, enabled: Boolean = true) { + + var enabled: Boolean by mutableStateOf(enabled) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Toggleable<*>) return false + + if (item != other.item) return false + if (enabled != other.enabled) return false + + return true + } + + override fun hashCode(): Int { + var result = item.hashCode() + result = 31 * result + enabled.hashCode() + return result + } +} diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameCell.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameCell.kt index a5b2b14..76100c3 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameCell.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameCell.kt @@ -1,10 +1,20 @@ package ch.dissem.yaep.domain class GameCell>( - var selection: Item?, + selection: Item?, val solution: Item, - val options: MutableList> -) + options: Collection> +) { + val selectionChangedListeners = mutableListOf<(Item?) -> Unit>() + val removedListeners = mutableListOf<(Collection>) -> Unit>() + + var selection: Item? = selection + set(value) { + field = value + selectionChangedListeners.forEach { listener -> listener(value) } + } + val options = ObservableSet(options.toMutableSet()) { removedListeners.forEach { listener -> listener(it) } } +} fun > GameCell?.mayBe(item: Item, mayHaveSelection: Boolean = true) = this != null && @@ -14,3 +24,4 @@ fun > GameCell?.isA(item: Item) = this != null && selection == item fun > GameCell?.hasNoSelection() = this != null && this.selection == null + diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt index 32d8808..1bf65b6 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt @@ -7,8 +7,18 @@ class GameRow>( ) : List> by cells { fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } - fun updateOptions() { - val selections = mapNotNull { it.selection } - forEach { it.options.removeAll(selections) } + fun cleanupOptions() { + cells.forEach { cleanupOptions(it) } } + + private fun cleanupOptions(cell: GameCell) { + if (cell.options.size == 1 && cell.selection == null) { + cell.selection = cell.options.first() + filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell -> + otherCell.options.remove(cell.selection) + cleanupOptions(otherCell) + } + } + } + } diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt index c59287c..eaa7f9a 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt @@ -6,7 +6,7 @@ class Grid( ) : List>> by rows as List>> { val cells: List> - get() = rows.flatten() as List> + get() = rows.flatten() fun > indexOf(element: C): Int { return this[element.companion] @@ -27,6 +27,10 @@ class Grid( row.joinToString("") { it.selection?.symbol ?: " " } } } + + fun cleanupOptions() { + forEach { row -> row.cleanupOptions() } + } } fun List>>>.toGrid() = Grid( diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/ObservableSet.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/ObservableSet.kt new file mode 100644 index 0000000..101f11e --- /dev/null +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/ObservableSet.kt @@ -0,0 +1,29 @@ +package ch.dissem.yaep.domain + +class ObservableSet private constructor( + private val mutableSet: MutableSet, + private val onElementRemoved: (Set) -> Unit +) : MutableSet by mutableSet { + constructor(elements: Collection, onElementRemoved: (Set) -> Unit) : this( + elements.toMutableSet(), + onElementRemoved + ) + + override fun remove(element: E): Boolean { + return observeRemoval(setOf(element)) { mutableSet.remove(element) } + } + + override fun removeAll(elements: Collection): Boolean = + observeRemoval(elements.toSet(), mutableSet::removeAll) + + override fun retainAll(elements: Collection): Boolean = + observeRemoval(elements.toSet(), mutableSet::retainAll) + + private fun observeRemoval(values: Set, operation: (Set) -> Boolean): Boolean { + val removed = operation(values) + if (removed) { + onElementRemoved(values) + } + return removed + } +} diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt index d4c0822..09b2dca 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt @@ -74,7 +74,7 @@ internal fun solve( // .forEach { it.options.removeAll(groupOptions) } // } // } - grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } } + grid.cleanupOptions() } while (removedOptions) // If any cell has no items left, the puzzle has no solution. @@ -92,16 +92,6 @@ internal enum class PuzzleSolution { MULTIPLE_SOLUTIONS } -fun > GameRow.cleanupOptions(cell: GameCell) { - if (cell.options.size == 1 && cell.selection == null) { - cell.selection = cell.options.first() - filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell -> - otherCell.options.remove(cell.selection) - cleanupOptions(otherCell) - } - } -} - fun getAllClues(rows: List>>>): MutableSet { val clues = mutableSetOf() rows.forEach { row ->