Add keyboard control (WIP, broken)

This commit is contained in:
2025-07-09 21:43:46 +02:00
parent e0de7be857
commit 1665ff1609
5 changed files with 43 additions and 74 deletions

View File

@@ -11,13 +11,13 @@ 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.input.key.Key
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 ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.Game
import focus import focus
import ch.dissem.yaep.domain.HorizontalClue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -58,9 +58,11 @@ fun App(
AdaptiveGameLayout( AdaptiveGameLayout(
modifier = Modifier.blurOnFinished(isSolved), modifier = Modifier.blurOnFinished(isSolved),
grid = { grid = {
val focusable = remember { selectionManager.add() }
PuzzleGrid( PuzzleGrid(
modifier = Modifier modifier = Modifier
.focus(remember { selectionManager.add() }), .focus(focusable),
remember { focusable.createChild(Key.DirectionDown, Key.DirectionUp) },
grid = game.grid, grid = game.grid,
spacing = spacing, spacing = spacing,
selectDirectly = selectDirectly, selectDirectly = selectDirectly,
@@ -71,11 +73,11 @@ fun App(
) )
}, },
horizontalClues = { horizontalClues = {
val horizontalClueSelection = remember { selectionManager.add() } val focusable = remember { selectionManager.add() }
for (clue in horizontalClues) { for (clue in horizontalClues) {
HorizontalClue( HorizontalClue(
modifier = Modifier modifier = Modifier
.focus(horizontalClueSelection) .focus(focusable)
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,
@@ -84,11 +86,11 @@ fun App(
} }
}, },
verticalClues = { verticalClues = {
val verticalClueSelection = remember { selectionManager.add() } val focusable = remember { selectionManager.add() }
for (clue in verticalClues) { for (clue in verticalClues) {
VerticalClue( VerticalClue(
modifier = Modifier modifier = Modifier
.focus(verticalClueSelection) .focus(focusable)
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,

View File

@@ -5,13 +5,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -26,7 +21,6 @@ import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.Clue
import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.Grid
import ch.dissem.yaep.domain.HorizontalClue import ch.dissem.yaep.domain.HorizontalClue
import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.domain.NeighbourClue import ch.dissem.yaep.domain.NeighbourClue
import ch.dissem.yaep.domain.OrderClue import ch.dissem.yaep.domain.OrderClue
import ch.dissem.yaep.domain.SameColumnClue import ch.dissem.yaep.domain.SameColumnClue
@@ -49,56 +43,14 @@ class DisplayClue<C : Clue>(val clue: C) {
} }
} }
@Composable internal fun Modifier.forClue(clue: DisplayClue<out Clue>): Modifier = this
fun PuzzleClues(
modifier: Modifier = Modifier.Companion,
horizontalClues: List<DisplayClue<HorizontalClue>>,
verticalClues: List<DisplayClue<SameColumnClue<ItemClass<*>, 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<out Clue>): Modifier = this
.alpha(if (clue.isActive) 1f else 0.2f) .alpha(if (clue.isActive) 1f else 0.2f)
.padding(8.dp) .padding(8.dp)
.onEitherPointerAction { clue.isActive = !clue.isActive } .onEitherPointerAction { clue.isActive = !clue.isActive }
@Composable @Composable
fun HorizontalClue( fun HorizontalClue(
modifier: Modifier = Modifier.Companion, modifier: Modifier = Modifier,
clue: HorizontalClue, clue: HorizontalClue,
isClueViolated: Boolean isClueViolated: Boolean
) { ) {
@@ -109,29 +61,29 @@ fun HorizontalClue(
Row { Row {
when (clue) { when (clue) {
is NeighbourClue<*, *> -> { is NeighbourClue<*, *> -> {
DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
Image( Image(
modifier = Modifier.Companion.aspectRatio(1f).weight(1f), modifier = Modifier.aspectRatio(1f).weight(1f),
painter = painterResource(Res.drawable.neighbour), painter = painterResource(Res.drawable.neighbour),
contentDescription = null contentDescription = null
) )
DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)
} }
is OrderClue<*, *> -> { is OrderClue<*, *> -> {
DrawItem(modifier = Modifier.Companion.weight(1f), clue.left) DrawItem(modifier = Modifier.weight(1f), clue.left)
Image( Image(
modifier = Modifier.Companion.aspectRatio(1f).weight(1f), modifier = Modifier.aspectRatio(1f).weight(1f),
painter = painterResource(Res.drawable.order), painter = painterResource(Res.drawable.order),
contentDescription = null contentDescription = null
) )
DrawItem(modifier = Modifier.Companion.weight(1f), clue.right) DrawItem(modifier = Modifier.weight(1f), clue.right)
} }
is TripletClue<*, *, *> -> { is TripletClue<*, *, *> -> {
DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)
DrawItem(modifier = Modifier.Companion.weight(1f), clue.c) DrawItem(modifier = Modifier.weight(1f), clue.c)
} }
} }
} }
@@ -140,7 +92,7 @@ fun HorizontalClue(
@Composable @Composable
fun VerticalClue( fun VerticalClue(
modifier: Modifier = Modifier.Companion, modifier: Modifier = Modifier,
clue: SameColumnClue<*, *>, clue: SameColumnClue<*, *>,
isClueViolated: Boolean = false isClueViolated: Boolean = false
) { ) {
@@ -149,15 +101,15 @@ fun VerticalClue(
isClueViolated = isClueViolated isClueViolated = isClueViolated
) { ) {
Column { Column {
DrawItem(modifier = Modifier.Companion.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
DrawItem(modifier = Modifier.Companion.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)
} }
} }
} }
@Composable @Composable
fun ClueCard( fun ClueCard(
modifier: Modifier = Modifier.Companion, modifier: Modifier = Modifier,
isClueViolated: Boolean, isClueViolated: Boolean,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {

View File

@@ -1,5 +1,6 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import SelectionManager
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -12,24 +13,31 @@ 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.input.key.Key
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.Grid
import focus
@Composable @Composable
fun PuzzleGrid( fun PuzzleGrid(
modifier: Modifier = Modifier.Companion, modifier: Modifier = Modifier,
selectionManager: SelectionManager,
grid: Grid, grid: Grid,
onUpdate: () -> Unit onUpdate: () -> Unit
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
for (row in grid) { for (row in grid) {
val focusableRow = remember { selectionManager.add() }
Row( Row(
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
) { ) {
val allOptions = row.options val allOptions = row.options
val columnSelectionManager =
remember { focusableRow.createChild(Key.DirectionRight, Key.DirectionLeft) }
for (item in row) { for (item in row) {
val focusableItem = remember { columnSelectionManager.add() }
var selection by remember(item) { mutableStateOf(item.selection) } var selection by remember(item) { mutableStateOf(item.selection) }
val options = remember(item) { val options = remember(item) {
allOptions.map { Toggleable(it, item.options.contains(it)) } allOptions.map { Toggleable(it, item.options.contains(it)) }
@@ -44,7 +52,8 @@ fun PuzzleGrid(
} }
} }
Selector( Selector(
modifier = Modifier.Companion modifier = Modifier
.focus(focusableItem)
.padding(8.dp) .padding(8.dp)
.weight(1f), .weight(1f),
options = options, options = options,

View File

@@ -32,6 +32,11 @@ class SelectionManager(
var focused: Focusable? var focused: Focusable?
get() = focusedFlow.value get() = focusedFlow.value
set(value) { set(value) {
val previous = focusedFlow.value
if (previous != value) {
previous?.child?.isActive = false
value?.child?.isActive = true
}
focusedFlow.value = value focusedFlow.value = value
} }
@@ -78,7 +83,7 @@ class SelectionManager(
} }
class Focusable( class Focusable(
manager: SelectionManager, private val manager: SelectionManager,
) { ) {
val hasFocus: Flow<Boolean> = val hasFocus: Flow<Boolean> =
combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused -> combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused ->
@@ -106,7 +111,7 @@ class Focusable(
keyPrevious: Key? = null keyPrevious: Key? = null
): SelectionManager { ): SelectionManager {
child = SelectionManager(keyNext, keyPrevious) child = SelectionManager(keyNext, keyPrevious)
child!!.isActive = false child!!.isActive = manager.isActive && manager.focused == this
return child!! return child!!
} }
} }

View File

@@ -1,5 +1,6 @@
package ch.dissem.yaep.ui.desktop package ch.dissem.yaep.ui.desktop
import SelectionManager
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf